코루틴
webman은 workerman을 기반으로 개발되었기 때문에 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를 드라이버로 사용할 때, 비즈니스에서 블로킹 I/O를 만나면 코루틴이 자동으로 전환되어 동기 코드를 비동기적으로 실행할 수 있습니다.
- Fiber 드라이버를 사용할 때 블로킹 I/O를 만나면 코루틴이 전환되지 않고 프로세스가 블로킹 상태에 들어갑니다.
- 코루틴을 사용할 때, 같은 자원(예: 데이터베이스 연결, 파일 작업 등)에 대해 여러 코루틴이 동시에 작업할 수 없습니다. 이는 자원 경쟁을 야기할 수 있으므로, 연결 풀 또는 잠금을 사용하여 자원을 보호해야 합니다.
- 코루틴을 사용할 때, 요청 관련 상태 데이터를 전역 변수 또는 정적 변수에 저장하면 안 됩니다. 이는 전역 데이터 오염을 일으킬 수 있으므로, 코루틴 컨텍스트
context
를 사용하여 이를 저장해야 합니다.
기타 주의 사항
Swow는 PHP의 블로킹 함수에 자동으로 훅을 거는데, 이러한 훅이 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;
}
}
주의
코루틴 환경에서는 전역 변수나 정적 변수를 사용하는 것이 금지된 것이 아니라, 전역 변수나 정적 변수에 요청 관련 상태 데이터를 저장하는 것이 금지된 것입니다.
전역 설정, 데이터베이스 연결, 일부 클래스의 싱글턴 등 전역적으로 공유해야 하는 객체 데이터는 전역 변수 또는 정적 변수를 사용하여 저장하는 것이 추천됩니다.
프로세스 수를 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
클래스는 코루틴 컨텍스트 데이터를 저장하는 데 사용되며, 코루틴이 완료되면 해당 context 데이터는 자동으로 삭제됩니다.
코루틴 환경에서는 각 요청이 개별 코루틴이므로, 요청이 완료될 때 context 데이터는 자동으로 파기됩니다.
비코루틴 환경에서는 요청이 종료되면 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);
}
// 잠금을 추가하지 않으면 Swoole에서 "Socket#10 has already been bound to another coroutine#10"와 유사한 오류가 발생할 수 있습니다.
// Swow에서 코어 덤프가 발생할 수 있습니다.
// 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); // 응답: [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 비즈니스를 처리하도록 구성하여 nginx가 다른 서비스로 요청을 전달합니다.
예를 들어 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;
}
# 새로 8686 upstream 추가
upstream task {
server 127.0.0.1:8686;
keepalive 10240;
}
server {
server_name webman.com;
listen 80;
access_log off;
root /path/webman/public;
# /task로 시작하는 요청은 8686 포트로 전달되며, 실제 상황에 맞게 /task를 변경하십시오.
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;
}
}
}