当前位置: 首页 > 文档资料 > Swoole 中文文档 >

编程须知

优质
小牛编辑
127浏览
2023-12-01

此章节会详细介绍协程编程与同步编程的不同之处以及需要注意的事项。

注意事项

  • 不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞;协程中可以使用Co::sleep()或在一键协程化后使用sleep;参考:sleep/usleep的影响
  • exit/die是危险的,会导致Worker进程退出;参考:exit/die函数的影响
  • 可通过register_shutdown_function来捕获致命错误,在进程异常退出时做一些清理工作;参考:捕获Server运行期致命错误
  • PHP代码中如果有异常抛出,必须在回调函数中进行try/catch捕获异常,否则会导致工作进程退出;参考:捕获异常和错误
  • 不支持set_exception_handler,必须使用try/catch方式处理异常;
  • Worker进程不得共用同一个RedisMySQL等网络服务客户端,Redis/MySQL创建连接的相关代码可以放到onWorkerStart回调函数中。参考 是否可以共用1个redis或mysql连接

协程编程

使用Coroutine特性,请认真阅读 协程编程须知

并发编程

请务必注意与同步阻塞模式不同,协程模式下程序是并发执行的,在同一时间内Server会存在多个请求,因此应用程序必须为每个客户端或请求,创建不同的资源和上下文。否则不同的客户端和请求之间可能会产生数据和逻辑错乱。

类/函数重复定义

新手非常容易犯这个错误,由于Swoole是常驻内存的,所以加载类/函数定义的文件后不会释放。因此引入类/函数的php文件时必须要使用include_oncerequire_once,否则会发生cannot redeclare function/class 的致命错误。

内存管理

!> 编写Server或其他常驻进程时需要特别注意。

PHP守护进程与普通Web程序的变量生命周期、内存管理方式完全不同。Server启动后内存管理的底层原理与普通php-cli程序一致。具体请参考Zend VM内存管理方面的文章。

局部变量

在事件回调函数返回后,所有局部对象和变量会全部回收,不需要unset。如果变量是一个资源类型,那么对应的资源也会被PHP底层释放。

function test()
{
    $a = new Object;
    $b = fopen('/data/t.log', 'r+');
    $c = new swoole_client(SWOOLE_SYNC);
    $d = new swoole_client(SWOOLE_SYNC);
    global $e;
    $e['client'] = $d;
}
  • $a, $b, $c 都是局部变量,当此函数return时,这3个变量会立即释放,对应的内存会立即释放,打开的IO资源文件句柄会立即关闭。
  • $d 也是局部变量,但是return前将它保存到了全局变量$e,所以不会释放。当执行unset($e['client'])时,并且没有任何其他PHP变量仍然在引用$d变量,那么$d就会被释放。

全局变量

PHP中,有3类全局变量。

  • 使用global关键词声明的变量
  • 使用static关键词声明的类静态变量、函数静态变量
  • PHP的超全局变量,包括$_GET$_POST$GLOBALS

全局变量和对象,类静态变量,保存在Server对象上的变量不会被释放。需要程序员自行处理这些变量和对象的销毁工作。

class Test
{
    static $array = array();
    static $string = '';
}

function onReceive($serv, $fd, $reactorId, $data)
{
    Test::$array[] = $fd;
    Test::$string .= $data;
}
  • 在事件回调函数中需要特别注意非局部变量的array类型值,某些操作如 TestClass::$array[] = "string" 可能会造成内存泄漏,严重时可能发生爆内存,必要时应当注意清理大数组。

  • 在事件回调函数中,非局部变量的字符串进行拼接操作是必须小心内存泄漏,如 TestClass::$string .= $data,可能会有内存泄漏,严重时可能发生爆内存。

解决方法

  • 同步阻塞并且请求响应式无状态的Server程序可以设置max_requesttask_max_request,当 Worker进程 / Task进程 结束运行时或达到任务上限后进程自动退出,该进程的所有变量/对象/资源均会被释放回收。
  • 程序内在onClose或设置定时器及时使用unset清理变量,回收资源。

进程隔离

进程隔离也是很多新手经常遇到的问题。修改了全局变量的值,为什么不生效?原因就是全局变量在不同的进程,内存空间是隔离的,所以无效。

所以使用Swoole开发Server程序需要了解进程隔离问题,Swoole\Server程序的不同Worker进程之间是隔离的,在编程时操作全局变量、定时器、事件监听,仅在当前进程内有效。

  • 不同的进程中PHP变量不是共享,即使是全局变量,在A进程内修改了它的值,在B进程内是无效的
  • 如果需要在不同的Worker进程内共享数据,可以用RedisMySQL文件Swoole\TableAPCushmget等工具实现
  • 不同进程的文件句柄是隔离的,所以在A进程创建的Socket连接或打开的文件,在B进程内是无效,即使是将它的fd发送到B进程也是不可用的

示例:

$server = new Swoole\Http\Server('127.0.0.1', 9500);

$i = 1;

$server->on('Request', function ($request, $response) {
    global $i;
    $response->end($i++);
});

$server->start();

在多进程的服务器中,$i变量虽然是全局变量(global),但由于进程隔离的原因。假设有4个工作进程,在进程1中进行$i++,实际上只有进程1中的$i变成2了,其他另外3个进程内$i变量的值还是1

正确的做法是使用Swoole提供的Swoole\AtomicSwoole\Table数据结构来保存数据。如上述代码可以使用Swoole\Atomic实现。

$server = new Swoole\Http\Server('127.0.0.1', 9500);

$atomic = new Swoole\Atomic(1);

$server->on('Request', function ($request, $response) use ($atomic) {
    $response->end($atomic->add(1));
});

$server->start();

!> Swoole\Atomic数据是建立在共享内存之上的,使用add方法加1时,在其他工作进程内也是有效的

Swoole提供的TableAtomicLock组件是可以用于多进程编程的,但必须在Server->start之前创建。另外Server维持的TCP客户端连接也可以跨进程操作,如Server->sendServer->close

stat缓存清理

PHP底层对stat系统调用增加了Cache,在使用statfstatfilemtime等函数时,底层可能会命中缓存,返回历史数据。

可以使用 clearstatcache 函数清理文件stat缓存。

mt_rand随机数

Swoole中如果在父进程内调用了mt_rand,不同的子进程内再调用mt_rand返回的结果会是相同的,所以必须在每个子进程内调用mt_srand重新播种。

!> shufflearray_rand等依赖随机数的PHP函数同样会受到影响

示例:

mt_rand(0, 1);

//开始
$worker_num = 16;

//fork 进程
for($i = 0; $i < $worker_num; $i++) {
    $process = new Swoole\Process('child_async', false, 2);
    $pid = $process->start();
}

//异步执行进程
function child_async(Swoole\Process $worker) {
    mt_srand(); //重新播种
    echo mt_rand(0, 100).PHP_EOL;
    $worker->exit();
}

捕获异常和错误

可捕获的异常/错误

PHP大致有三种类型的可捕获的异常/错误

  1. ErrorPHP内核抛出错误的专用类型,如类不存在,函数不存在,函数参数错误,都会抛出此类型的错误,PHP代码中不应该使用Error类来作为异常抛出
  2. Exception:应用开发者应该使用的异常基类
  3. ErrorException:此异常基类专门负责将PHPWarning/Notice等信息通过set_error_handler转换成异常,PHP未来的规划必然是将所有的Warning/Notice转为异常,以便于PHP程序能够更好更可控地处理各种错误

!> 以上所有类都实现了Throwable接口,也就是说,通过try {} catch(Throwable $e) {} 即可捕获所有可抛出的异常/错误

示例1:

try {
    test();
} 
catch(Throwable $e) {
    var_dump($e);
}

示例2:

try {
    test();
}
catch (Error $e) {
    var_dump($e);
}
catch(Exception $e) {
    var_dump($e);
}

不可捕获的致命错误和异常

PHP错误的一个重要级别,如异常/错误未捕获时、内存不足时或是一些编译期错误(继承的类不存在),将会以E_ERROR级别抛出一个Fatal Error,是在程序发生不可回溯的错误时才会触发的,PHP程序无法捕获这样级别的一种错误,只能通过register_shutdown_function在后续进行一些处理操作。

在协程中捕获运行时异常/错误

Swoole4协程编程中,某个协程的代码中抛出错误,会导致整个进程退出,进程所有协程终止执行。在协程顶层空间可以先进行一次try/catch捕获异常/错误,仅终止出错的协程。

Co\run(function () {
    go(function () {
        try {
            call_user_func($func);
        }
        catch (Error $e) {
            var_dump($e);
        }
        catch(Exception $e) {
            var_dump($e);
        }
    });

    //协程1的错误不影响协程2
    go(function () {
        Co::sleep(5);
        echo 2;
    });
});

捕获Server运行期致命错误

Server运行期一旦发生致命错误,那客户端连接将无法得到回应。如Web服务器,如果有致命错误应当向客户端发送Http 500错误信息。

在PHP中可以通过 register_shutdown_function + error_get_last 2个函数来捕获致命错误,并将错误信息发送给客户端连接。

具体代码示例如下:

$http = new Swoole\Http\Server("127.0.0.1", 9501);
$http->on('request', function ($request, $response) {
    register_shutdown_function(function () use ($response) {
        $error = error_get_last();
        var_dump($error);
        switch ($error['type'] ?? null) {
            case E_ERROR :
            case E_PARSE :
            case E_CORE_ERROR :
            case E_COMPILE_ERROR :
                // log or send:
                // error_log($message);
                // $server->send($fd, $error['message']);
                $response->status(500);
                $response->end($error['message']);
                break;
        }
    });
    exit(0);
});
$http->start();

使用影响

sleep/usleep的影响

在异步IO的程序中,不得使用sleep/usleep/time_sleep_until/time_nanosleep。(下文中使用sleep泛指所有睡眠函数)

  • sleep函数会使进程陷入睡眠阻塞
  • 直到指定的时间后操作系统才会重新唤醒当前的进程
  • sleep过程中,只有信号可以打断
  • 由于Swoole的信号处理是基于signalfd实现的,所以即使发送信号也无法中断sleep

Swoole提供的swoole_event_addswoole_timer_tickswoole_timer_afterSwoole\Process::signal、在进程sleep后会停止工作。Swoole\Server也无法再处理新的请求。

示例

$server = new Swoole\Server("127.0.0.1", 9501);
$server->set(['worker_num' => 1]);
$server->on('receive', function ($server, $fd, $from_id, $data) {
    sleep(100);
    $server->send($fd, 'Swoole: '.$data);
});
$server->start();

!> 在onReceive事件中执行了sleep函数,Server在100秒内无法再收到任何客户端请求。

exit/die函数的影响

Swoole程序中禁止使用exit/die,如果PHP代码中有exit/die,当前工作的Worker进程Task进程User进程、以及Swoole\Process进程会立即退出。

使用exit/dieWorker进程会因为异常退出,被master进程再次拉起,最终造成进程不断退出又不断启动和产生大量警报日志.

建议使用try/catch的方式替换exit/die,实现中断执行跳出PHP函数调用栈。

go(function () {
    try
    {
        exit(0);
    } catch (Swoole\ExitException $e)
    {
        echo $e->getMessage()."\n";
    }
});

!> Swoole\ExitException是Swoolev4.1.0版本及以上直接支持了在协程和Server中使用PHP的exit,此时底层会自动抛出一个可捕获的Swoole\ExitException,开发者可以在需要的位置捕获并实现与原生PHP一样的退出逻辑。具体使用参考退出协程;

异常处理的方式比exit/die更友好,因为异常是可控的,exit/die不可控。在最外层进行try/catch即可捕获异常,仅终止当前的任务。Worker进程可以继续处理新的请求,而exit/die会导致进程直接退出,当前进程保存的所有变量和资源都会被销毁。如果进程内还有其他任务要处理,遇到exit/die也将全部丢弃。

while循环的影响

异步程序如果遇到死循环,事件将无法触发。异步IO程序使用Reactor模型,运行过程中必须在reactor->wait处轮询。如果遇到死循环,那么程序的控制权就在while中了,reactor无法得到控制权,无法检测事件,所以IO事件回调函数也将无法触发。

!> 密集运算的代码没有任何IO操作,所以不能称为阻塞

实例程序

$server = new Swoole\Server("127.0.0.1", 9501);
$server->set(['worker_num' => 1]);
$server->on('receive', function ($server, $fd, $reactorId, $data) {
    $i = 0;
    while(1)
    {
        $i++;
    }
    $server->send($fd, 'Swoole: '.$data);
});
$server->start();

!> 在onReceive事件中执行了死循环,server无法再收到任何客户端请求,必须等待循环结束才能继续处理新的事件。