ระเบียบการทำงานร่วมกัน
webman ถูกพัฒนาขึ้นบนพื้นฐานของ workerman ดังนั้น webman สามารถใช้คุณสมบัติของการทำงานร่วมกันของ workerman ได้
การทำงานร่วมกันรองรับไดรเวอร์สามประเภทคือ Swoole
Swow
และ Fiber
ข้อกำหนดเบื้องต้น
- PHP >= 8.1
- Workerman >= 5.1.0 (
composer require workerman/workerman ~v5.1
) - webman-framework >= 2.1 (
composer require workerman/webman-framework ~v2.1
) - ติดตั้งส่วนขยาย swoole หรือ swow หรือ ติดตั้ง
composer require revolt/event-loop
(Fiber) - การทำงานร่วมกันจะถูกปิดโดยค่าเริ่มต้น ต้องตั้งค่า eventLoop เพื่อเปิดใช้งานการทำงานร่วมกันแยกต่างหาก
วิธีการเปิดใช้งาน
webman รองรับการเปิดใช้งานไดรเวอร์ต่างๆ สำหรับกระบวนการต่างๆ ดังนั้นคุณสามารถกำหนดค่าไดรเวอร์การทำงานร่วมกันใน config/process.php
ผ่าน eventLoop
:
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // ค่าเริ่มต้นคือว่าง ซึ่งจะเลือก Select หรือ Event โดยอัตโนมัติ ไม่เปิดใช้งานการทำงานร่วมกัน
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// การเปิดการทำงานร่วมกันจำเป็นต้องตั้งค่าเป็น Workerman\Events\Swoole::class หรือ Workerman\Events\Swow::class หรือ Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
]
// ... การกำหนดค่าอื่น ๆ ถูกละเว้น ...
];
คำแนะนำ
webman สามารถตั้งค่า eventLoop ที่แตกต่างกันสำหรับกระบวนการที่แตกต่างกัน ซึ่งหมายความว่าคุณสามารถเปิดใช้งานการทำงานร่วมกันเฉพาะในกระบวนการที่กำหนด
เช่น ในการกำหนดค่าในที่นี้ บริการที่ใช้พอร์ต 8787 จะไม่ได้เปิดใช้งานการทำงานร่วมกัน ในขณะที่บริการที่ใช้พอร์ต 8686 จะเปิดใช้งานการทำงานร่วมกัน ซึ่งทำให้สามารถทำการผสมผสานระหว่างการทำงานร่วมกันและไม่ร่วมกันได้ด้วยการส่งต่อผ่าน nginx
ตัวอย่างการทำงานร่วมกัน
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;
class IndexController
{
public function index(): Response
{
Coroutine::create(function(){
Timer::sleep(1.5);
echo "hello coroutine\n";
});
return response('hello webman');
}
}
เมื่อ eventLoop
เป็น Swoole
Swow
Fiber
webman จะสร้างการทำงานร่วมกันสำหรับแต่ละคำขอเพื่อรัน และในระหว่างการจัดการคำขอสามารถสร้างการทำงานร่วมกันใหม่เพื่อติดตามรหัสธุรกิจได้
ข้อจำกัดของการทำงานร่วมกัน
- เมื่อใช้ Swoole Swow เป็นไดรเวอร์ ธุรกิจที่พบกับ IO ที่ติดขัดจะมีการสลับการทำงานร่วมกันโดยอัตโนมัติ ทำให้สามารถดำเนินการโค้ดที่ซิงโครนัสให้เป็นแบบอะซิงโครนัสได้
- เมื่อใช้ไดรเวอร์ Fiber เมื่อพบกับ IO ที่ติดขัด การทำงานร่วมกันจะไม่สลับและกระบวนการจะเข้าสู่สถานะติดขัด
- เมื่อใช้การทำงานร่วมกัน ไม่สามารถดำเนินการกับทรัพยากรเดียวกันได้หลายการทำงาน เช่น การเชื่อมต่อฐานข้อมูล การดำเนินการไฟล์ เป็นต้น ซึ่งอาจทำให้เกิดการแข่งกันของทรัพยากร การใช้งานที่ถูกต้องคือการใช้เชื่อมต่อพูลหรือการล็อกเพื่อปกป้องทรัพยากร
- เมื่อใช้การทำงานร่วมกัน ไม่ควรเก็บข้อมูลสถานะที่เกี่ยวข้องกับคำขอในตัวแปรทั่วโลกหรือตัวแปรสถิติ เนื่องจากอาจทำให้เกิดมลพิษข้อมูลทั่วโลก การใช้งานที่ถูกต้องคือการใช้บริบทของการทำงานร่วมกัน
context
เพื่อเก็บและดึงข้อมูลเหล่านั้น
ข้อควรระวังอื่น ๆ
Swow จะทำการ hook ฟังก์ชันที่ติดขัด PHP โดยอัตโนมัติ แต่เนื่องจากการ hook นี้ส่งผลกระทบต่อพฤติกรรมเริ่มต้นบางประการของ PHP อาจเกิดบั๊กขึ้นเมื่อคุณไม่ได้ใช้ Swow แต่ติดตั้ง Swow
ดังนั้นแนะนำให้:
- หากโครงการของคุณไม่ได้ใช้ Swow โปรดอย่าติดตั้งส่วนขยาย Swow
- หากโครงการของคุณใช้ Swow โปรดตั้งค่า
eventLoop
เป็นWorkerman\Events\Swow::class
บริบทของการทำงานร่วมกัน
สิ่งแวดล้อมของการทำงานร่วมกันห้ามไม่ให้เก็บข้อมูลสถานะที่เกี่ยวข้องกับ คำขอ ในตัวแปรทั่วโลกหรือตัวแปรสถิติ เนื่องจากอาจทำให้เกิดมลพิษตัวแปรทั่วโลก เช่น
<?php
namespace app\controller;
use support\Request;
use Workerman\Timer;
class TestController
{
protected static $name = '';
public function index(Request $request)
{
static::$name = $request->get('name');
Timer::sleep(5);
return static::$name;
}
}
หมายเหตุ
ในสิ่งแวดล้อมการทำงานร่วมกันไม่ได้ห้ามการใช้ตัวแปรทั่วโลกหรือตัวแปรสถิติ แต่อย่างใด แต่ห้ามไม่ให้ใช้ตัวแปรทั่วโลกหรือตัวแปรสถิติเก็บข้อมูลสถานะที่เกี่ยวข้องกับ คำขอ
เช่น ตัวแปรทั่วโลกบางอย่าง การเชื่อมต่อฐานข้อมูล การใช้งานคลาสบางตัวที่เป็น singleton ฯลฯ ที่ต้องการแชร์ข้อมูลทั่วโลกสามารถเก็บในตัวแปรทั่วโลกหรือตัวแปรสถิติได้
เมื่อเราตั้งค่าจำนวนกระบวนการเป็น 1 และส่งคำขอสองคำขอต่อเนื่องกัน
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
เราหวังว่าผลลัพธ์ของคำขอทั้งสองจะเป็น lilei
และ hanmeimei
ตามลำดับ แต่จริง ๆ แล้วผลลัพธ์ที่ได้คือ hanmeimei
ทั้งคู่
เนื่องจากคำขอที่สองได้เขียนทับตัวแปรสถิติ $name
เมื่อคำขอแรกสิ้นสุดการนอนหลับและส่งคืน ตัวแปรสถิติ $name
ได้กลายเป็น hanmeimei
แล้ว
วิธีที่ถูกต้องคือการใช้ context ในการเก็บข้อมูลสถานะของคำขอ
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
Context::set('name', $request->get('name'));
Timer::sleep(5);
return Context::get('name');
}
}
คลาส support\Context
ใช้สำหรับเก็บข้อมูลบริบทของการทำงานร่วมกัน เมื่อการทำงานร่วมกันเสร็จสิ้น ข้อมูลที่เกี่ยวข้องกับบริบทจะถูกลบโดยอัตโนมัติ
ในสิ่งแวดล้อมการทำงานร่วมกัน เนื่องจากแต่ละคำขอเป็นการทำงานร่วมกันแยกต่างหาก ดังนั้นเมื่อคำขอเสร็จสิ้น ข้อมูลบริบทจะถูกทำลายโดยอัตโนมัติ
ในสิ่งแวดล้อมที่ไม่ใช่การทำงานร่วมกัน ข้อมูลบริบทจะถูกทำลายอัตโนมัติเมื่อคำขอสิ้นสุด
ตัวแปรท้องถิ่นจะไม่ทำให้เกิดมลพิษข้อมูล
<?php
namespace app\controller;
use support\Request;
use support\Context;
use Workerman\Timer;
class TestController
{
public function index(Request $request)
{
$name = $request->get('name');
Timer::sleep(5);
return $name;
}
}
เนื่องจาก $name
เป็นตัวแปรท้องถิ่น การทำงานร่วมกันไม่สามารถเข้าถึงตัวแปรท้องถิ่นได้ ดังนั้นการใช้ตัวแปรท้องถิ่นจึงปลอดภัยในการทำงานร่วมกัน
Locker ล็อก
บางครั้งบางโครงสร้างหรือธุรกิจไม่ได้พิจารณาถึงสิ่งแวดล้อมการทำงานร่วมกัน อาจทำให้เกิดปัญหาการแข่งกันทรัพยากรหรือลักษณะของข้อมูลที่เป็นอะตอม ในกรณีนี้สามารถใช้ Workerman\Locker
เพื่อทำการล็อกตำแหน่งเพื่อจัดการการรอคิวและป้องกันปัญหาการทำงานพร้อมกัน
<?php
namespace app\controller;
use Redis;
use support\Response;
use Workerman\Coroutine\Locker;
class IndexController
{
public function index(): Response
{
static $redis;
if (!$redis) {
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
}
// หากไม่ทำการล็อก จะทำให้เกิดข้อผิดพลาด "Socket#10 has already been bound to another coroutine#10" ใน Swoole
// ใน Swow อาจทำให้เกิด coredump
// ใน Fiber เนื่องจาก Redis ส่วนขยายเป็น I/O ที่ซิงโครนัสติดขัดจึงไม่มีปัญหา
Locker::lock('redis');
$time = $redis->time();
Locker::unlock('redis');
return json($time);
}
}
Parallel การดำเนินการพร้อมกัน
เมื่อเราต้องทำการดำเนินการหลายงานพร้อมกันและรับผลลัพธ์ สามารถใช้ Workerman\Parallel
เพื่อดำเนินการ
<?php
namespace app\controller;
use support\Response;
use Workerman\Coroutine\Parallel;
class IndexController
{
public function index(): Response
{
$parallel = new Parallel();
for ($i=1; $i<5; $i++) {
$parallel->add(function () use ($i) {
// ทำบางอย่าง
return $i;
});
}
$results = $parallel->wait();
return json($results); // Response: [1,2,3,4]
}
}
Pool การเชื่อมต่อพูล
การใช้การเชื่อมต่อเดียวกันในหลายการทำงานร่วมกันอาจทำให้ข้อมูลสับสน ดังนั้นจึงจำเป็นต้องใช้พูลเพื่อจัดการทรัพยากรการเชื่อมต่อเช่น ฐานข้อมูล Redis ฯลฯ
webman ได้จัดเตรียม webman/database webman/redis webman/cache webman/think-orm webman/think-cache เป็นต้น ซึ่งทั้งหมดนี้รวมการใช้พูลเชื่อมต่อและสนับสนุนการใช้งานในสิ่งแวดล้อมการทำงานร่วมกันและไม่ร่วมกัน
หากคุณต้องการปรับเปลี่ยนโครงสร้างบางอย่างที่ไม่มีพูลการเชื่อมต่อ คุณสามารถใช้ Workerman\Pool
เพื่อรองรับการดำเนินงาน ตามโค้ดด้านล่าง
ส่วนเชื่อมต่อฐานข้อมูล
<?php
namespace app;
use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;
class Db
{
private static ?Pool $pool = null;
public static function __callStatic($name, $arguments)
{
if (self::$pool === null) {
self::initializePool();
}
// ดึงการเชื่อมต่อจากบริบทการทำงานร่วมกัน ทำให้การทำงานร่วมกันเดียวกันใช้การเชื่อมต่อเดียวกัน
$pdo = Context::get('pdo');
if (!$pdo) {
// ดึงการเชื่อมต่อจากพูลการเชื่อมต่อ
$pdo = self::$pool->get();
Context::set('pdo', $pdo);
// เมื่อการทำงานร่วมกันสิ้นสุด จะคืนการเชื่อมต่ออัตโนมัติ
Coroutine::defer(function () use ($pdo) {
self::$pool->put($pdo);
});
}
return call_user_func_array([$pdo, $name], $arguments);
}
private static function initializePool(): void
{
// สร้างพูลการเชื่อมต่อ โดยมีจำนวนการเชื่อมต่อตั้งต้นอยู่ที่ 10
self::$pool = new Pool(10);
// ตั้งค่าผู้สร้างการเชื่อมต่อ (เพื่อความสะดวก จึงละไว้ซึ่งการอ่านไฟล์การกำหนดค่า)
self::$pool->setConnectionCreator(function () {
return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
});
// ตั้งค่าผู้ปิดการเชื่อมต่อ
self::$pool->setConnectionCloser(function ($pdo) {
$pdo = null;
});
// ตั้งค่าผู้ตรวจสอบการเต้น
self::$pool->setHeartbeatChecker(function ($pdo) {
$pdo->query('SELECT 1');
});
}
}
การใช้งาน
<?php
namespace app\controller;
use support\Response;
use app\Db;
class IndexController
{
public function index(): Response
{
$value = Db::query('SELECT NOW() as now')->fetchAll();
return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
}
}
ข้อมูลเพิ่มเติมเกี่ยวกับการทำงานร่วมกันและส่วนประกอบที่เกี่ยวข้อง
โปรดดูที่ เอกสารการทำงานร่วมกันของ workerman
การจัดการระหว่างการทำงานร่วมกันและไม่ร่วมกัน
webman สนับสนุนการจัดการระหว่างการทำงานร่วมกันและไม่ร่วมกัน เช่น การจัดการธุรกิจทั่วไปแบบไม่ร่วมกัน การจัดการธุรกิจ I/O ช้าแบบร่วมกัน ผ่านการส่งผ่านคำขอไปยังบริการที่แตกต่างกัน
เช่น ใน config/process.php
return [
'webman' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8787',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
'eventLoop' => '', // ค่าเริ่มต้นคือว่าง ซึ่งจะเลือก Select หรือ Event โดยอัตโนมัติ ไม่เปิดใช้งานการทำงานร่วมกัน
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
'my-coroutine' => [
'handler' => Http::class,
'listen' => 'http://0.0.0.0:8686',
'count' => 1,
'user' => '',
'group' => '',
'reusePort' => false,
// การเปิดการทำงานร่วมกันจำเป็นต้องตั้งค่าเป็น Workerman\Events\Swoole::class หรือ Workerman\Events\Swow::class หรือ Workerman\Events\Fiber::class
'eventLoop' => Workerman\Events\Swoole::class,
'context' => [],
'constructor' => [
'requestClass' => Request::class,
'logger' => Log::channel('default'),
'appPath' => app_path(),
'publicPath' => public_path()
]
],
// ... การกำหนดค่าอื่น ๆ ถูกละเว้น ...
];
จากนั้นให้ตั้งค่า nginx เพื่อส่งต่อคำขอไปยังบริการที่แตกต่างกัน
upstream webman {
server 127.0.0.1:8787;
keepalive 10240;
}
# เพิ่มอีกหนึ่ง upstream 8686
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# คำขอที่เริ่มต้นด้วย /tast จะใช้พอร์ต 8686 โปรดเปลี่ยน /tast เป็นคำนำที่คุณต้องการใช้
location /tast {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://task;
}
# คำขออื่น ๆ จะไปยังพอร์ต 8787 เดิม
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_http_version 1.1;
proxy_set_header Connection "";
if (!-f $request_filename){
proxy_pass http://webman;
}
}
}