Skip to content

2.编程须知

mc² edited this page May 19, 2020 · 1 revision

编程须知

注意事项

  • 不要在代码中执行sleep以及其他睡眠函数,这样会导致整个进程阻塞
  • exit/die是危险的,会导致Worker进程退出
  • 不支持set_exception_handler,必须使用try/catch方式处理异常
  • Worker进程不得共用同一个RedisMySQL等网络服务客户端,框架初始化位于onWorkerStart回调函数中,无须担心该问题。

类/函数重复定义

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

内存管理

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提供的异步客户端与普通的PHP变量不同,异步客户端在发起connect时底层会增加一次引用计数,在连接close时会减少引用计数。

包括swoole_clientswoole_mysqlswoole_redisswoole_http_client

function test()
{
	$client = new swoole_client(SWOOLE_TCP | SWOOLE_ASYNC);
	$client->on("connect", function($cli) {
		$cli->send("hello world\n");
	});
	$client->on("receive", function($cli, $data){
		echo "Received: ".$data."\n";
		$cli->close();
	});
	$client->on("error", function($cli){
		echo "Connect failed\n";
	});
	$client->on("close", function($cli){
		echo "Connection close\n";
	});
	$client->connect('127.0.0.1', 9501);
	return;
}
  • $client是局部变量,常规情况下return时会销毁。
  • 但这个$client是异步客户端在执行connect时swoole引擎底层会增加一次引用计数,因此return时并不会销毁。
  • 该客户端执行onReceive回调函数时进行了close或者服务器端主动关闭连接触发onClose,这时底层会减少引用计数,$client才会被销毁。

进程隔离

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

  • 不同的进程中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时,在其他工作进程内也是有效的