理解资源复用

PHP资源回收

在Stone中资源可以不在请求结束后销毁, 也可以在结束后自行销毁, 这取决于你如何使用这个资源。和C语言不一样,PHP有基于引用计数的内存回收机制, 当一个对象没有被引用时,对象就会被回收,并不需要你手动free。

我们写一个小例子确认一下:

首先, 我们临时调低一下linux一个进程能打开的最大文件数:

ulimit -n 8

设置一个进程最大打开文件句柄数为8后, 我们写这样一段php程序, 保存为/tmp/ulimit.php:

<?php

$i = 0;
while ($i < 10) {
    $fp = fopen('/tmp/file' . $i, 'w');
    $i++;
}

echo "ok\n";

运行这个php程序, 输出『ok』:

php /tmp/ulimit.php
ok

然后, 我们对这个程序做一个小的修改, 把原来的$fp变成一个数组:

<?php

$fp = [];
$i = 0;
while ($i < 10) {
    $fp[] = fopen('/tmp/file' . $i, 'w');
    $i++;
}

echo "ok\n";

再运行就得到这样的效果:

PHP Warning:  fopen(/tmp/file4): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
PHP Warning:  fopen(/tmp/file5): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
PHP Warning:  fopen(/tmp/file6): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
PHP Warning:  fopen(/tmp/file7): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
PHP Warning:  fopen(/tmp/file8): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
PHP Warning:  fopen(/tmp/file9): failed to open stream: Too many open files in /tmp/ulimit.php on line 6
ok

这证明了之前关于资源回收的观点, 第一个程序, fp不断被新的句柄覆盖, 旧的句柄没有被引用, php自动回收了这个资源, 即使我们并没有手动去fclose它。第二个程序中, 由于文件句柄被数组持有, 所以php无法回收,导致超出了进程最大打开文件数。

细心的朋友可能会问,明明最大文件数是8, 怎么从file4就开始报错了, 这是因为在linux中, 每一个进程都会自动打开3个文件句柄:输入, 输出, 错误。 同时, php执行ulimit.php文件, 也占用了一个句柄, 再加上file0-file3, 刚好8个文件句柄。

Server模式为什么相对安全?

再回头看Server模式, 核心逻辑都是一个process方法发起的, process结束后, 方法里申请的资源就会被释放。

<?php
class Handler implements RequestHandler
{
    private $data;

    public function process()
    {
        $fp = fopen('/tmp/123', 'w');
        $db = new PDO('mysql://xxxx');
        $obj = new stdclass;
    }

    public function onWorkerStart()
    {
        $this->data = 'xxxxx';
    }
}

如上面代码, $fp, $db, $obj在请求结束后就会被销毁, 但是$data不会, 因为$data被handler引用了, 而handler常驻在内存中。

之所以说Server模式『相对安全』,就是因为根据我们大多数程序员的使用习惯, 我们都是在运行时通过new的方式来创建对象,这样的做法在程序结束之后自然而然地被释放了。

Server模式真的一定安全吗?

先看下面这段代码, 我们模拟一下Server的执行, process方法里打开一些文件。

<?php
class Container
{
    private $fp = [];

    public function addFp($fp)
    {
        $this->fp[] = $fp;
    }
}

function process($i)
{
    $container = new Container;
    $container->addFp(fopen('/tmp/file' . $i, 'w'));
}

$i = 0;
while ($i < 10) {
    process($i);
    $i++;
}

echo "ok\n";

执行后正常输出ok, 这是因为fp虽然被container持有, 但是process结束后, container被自动释放, 由于fp被引用的container已经被释放,再也没有被其他任何对象引用, 所以fp自然也就释放了。

ok

我们稍微修改一下程序,把Container的方法设定为静态方法, 属性设置为静态属性, 再测试一下:

<?php
class Container
{
    private static $fp = [];

    public static function addFp($fp)
    {
        self::$fp[] = $fp;
    }
}

function process($i)
{
    Container::addFp(fopen('/tmp/file' . $i, 'w'));
}

$i = 0;
while ($i < 10) {
    process($i);
    $i++;
}

echo "ok\n";

这个时候输出如下:

PHP Warning:  fopen(/tmp/file4): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
PHP Warning:  fopen(/tmp/file5): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
PHP Warning:  fopen(/tmp/file6): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
PHP Warning:  fopen(/tmp/file7): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
PHP Warning:  fopen(/tmp/file8): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
PHP Warning:  fopen(/tmp/file9): failed to open stream: Too many open files in /tmp/ulimit.php on line 14
ok

原因是php中类是全局存在的, 始终有一个global的域引用着。 PHP中,静态变量被定义在类上, 所以这个变量不会销毁。那变量引用的文件句柄fp自然也不会销毁, 即使函数已经执行完毕。

温习下Laravel的控制反转

Laravel这个框架被设计得高度灵活, 整个框架的基于IOC容器建立的,应用启动后,app就是一个大的容器。我们简单分析下这行代码发生了什么, 为了简单, 我忽略了实现细节:

$user = Auth::user();

这是一段使用Laravel几乎每个人都会用到的代码,那这行代码发生了什么呢?

Auth是一个Facade类的别名:

'Auth' => Illuminate\Support\Facades\Auth::class,

顺藤摸瓜,Facade最终实例化Auth对象其实就是通过app实例化的:

   public static function __callStatic($method, $args)
    {
        $instance = static::getFacadeRoot();

        if (! $instance) {
            throw new RuntimeException('A facade root has not been set.');
        }

        switch (count($args)) {
            case 0:
                return $instance->$method();

            case 1:
                return $instance->$method($args[0]);

            case 2:
                return $instance->$method($args[0], $args[1]);

            case 3:
                return $instance->$method($args[0], $args[1], $args[2]);

            case 4:
                return $instance->$method($args[0], $args[1], $args[2], $args[3]);

            default:
                return call_user_func_array([$instance, $method], $args);
        }
    }

使用'auth'这个绑定:

    protected static function getFacadeAccessor()
    {
        return 'auth';
    }

'auth'? app怎么知道实例化哪个对象呢?其实在app启动时, 就通过ServiceProvider注册了:

    protected function registerAuthenticator()
    {
        $this->app->singleton('auth', function ($app) {
            // Once the authentication service has actually been requested by the developer
            // we will set a variable in the application indicating such. This helps us
            // know that we need to set any queued cookies in the after event later.
            $app['auth.loaded'] = true;

            return new AuthManager($app);
        });

        $this->app->singleton('auth.driver', function ($app) {
            return $app['auth']->guard();
        });
    }

这样,一个控制反转就完成了。 饶了这么大一圈, 简单的说就是:

产品需求: 世界和平

开发一:

$person = new SuperMan();
$person->killAllEnemies();

开发二:

$person = App::make('power');
$person->killAllEnemies();

开发一直接实例化一个超人, 控制权在调用者, 调用者决定自己具体需要什么。当某一天超人不在, 或者超人打不过敌人的时候, 需求就挂了。

开发二没有直接决定自己具体需要什么, 他只是告诉提供者, 我需要一个具有超能力的对象(面向接口编程),这样,控制权交还给提供者, 提供者发现超人不行的时候, 就派出超级赛亚人了 :)

控制反转后,提供者与调用者不再是强依赖的关系,大大提高了系统的灵活性。有点跑题了, 关于控制反转的话题, 有机会再单独聊。

我们的结论是:

$user = Auth::user();

这样一行代码, Auth对象被ioc容器引用, 被Facade类引用(为了避免反复触发魔术方法,提高效率), 所以, 函数结束后, 这个对象并不会被回收。

因此, Server模式下, 也不一定安全。

Stone如何解决这个问题

Auth对象之所以不被释放, 是因为请求结束后仍然被APP和Facade引用, 所以只需要解除引用就可以了。但是新的问题又来了: Stone怎么知道哪些实例需要释放, 哪些实例不需要释放呢?

其实Stone并没有什么魔法, 它释放实例基于一个很简单的原理, 在app加载完成后,将app建立快照,请求结束后再通过快照恢复app, 这样在请求时新建立的实例引用就会被解除。

比如:app初始化后, 容器里的实例有cookie,auth等, 把当前实例列表保存下来。请求中又新注册绑定了一些新的实例, 容器里的实例又增加了instance1, instance2等。请求结束后, 实例列表恢复成初始状态, 这样app对于instance1, instance2的引用就解除了。

在Stone中的做法:

请求开始之前:

            $this->boot();
            $this->injectInstance();
            $this->snapApp();

请求结束之后:

            $response = with($stack = $this->getStackedClient())->handle($request);
            $stack->terminate($request, $response);
            $this->restoreApp();

Facade的实现类似。

但是, 如果你在请求期间定义静态属性来保存引用, 这种情况下是没法自动释放的。 而且,我们也无法确定开发者到底是否需要自动释放。如果你能理解这些, 利用好Stone资源复用的能力,一定可以写出非常高效的程序, 反之,如果你不能很好理解,可能等待你的是一个个大坑。

这都取决于我们自己, 不是吗?

results matching ""

    No results matching ""