ระเบียบการทำงานร่วมกัน

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;
      }
  }
}