浜松のWEBシステム開発・スマートフォンアプリ開発・RTK-GNSS関連の開発はお任せください
株式会社シーポイントラボ
TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:静岡県浜松市中区富塚町1933-1 佐鳴湖パークタウンサウス2F

【PHP】register_shutdown_function を一度登録した関数が消せる様にラッピング

 register_shutdown_function は PHP が終了した時に呼ばれるコールバックを登録する関数です。次の引用の様に外部からプロセスを殺された場合は実行されませんが、それ以外では実行されます。
PHP: register_shutdown_function – Manual

注意:

SIGTERM あるいは SIGKILL でプロセスが終了した場合は、シャットダウン関数を実行しません。
SIGKILL を横取りすることはできませんが、SIGTERM 用のハンドラは pcntl_signal()
で登録できます。ここで exit() を使えば、きれいに終わらせることができます。

 その都合上、何が何でも PHP 終了時には実行しなければならない後始末のコールバックを登録されることが多いです。例えば、作業中であることを示す .lock ファイルをあらかじめ作り、.lock ファイルの削除関数を register_shutdown_function に登録するとかそういった具合です。

 register_shutdown_function は便利なのですが、組み込みのままでは欠点もあります。というのも一度登録した関数を削除することができません。対になる unregister_shutdown_function みたいなものがないのです。これで何が起こるかというと次です。

for($i=0; $i<1e10; $i++) {
    register_shutdown_function(fn() => null);
}
/**
Fatal error: Allowed memory size of 67108864 bytes exhausted (tried to allocate 12288 bytes) in /in/3eV7N on line 4

Process exited with code 255.
*/

 登録し続けるとメモリが足りなくなります。↑の例は極端ですが、使用不能なメモリの領域を増やし続けてしまうというだけであまりよろしくありません。真に大量のメモリを必要とする処理が実行不能になったりする場合があります。マシンとPHPの設定によってはマシン自体の負荷がひどいことになり register_shutdown_function が関係ないプロセスにまで迷惑をかけます。そういったわけで register_shutdown_function そのものはそれほど気軽に使えません。そこで unregister_shutdown_function 相当の処理ができる方法に需要があります。
 unregister_shutdown_function 相当の実装は次でできます。

<?php

/**
 * register_shutdown_function をラッピングしたクラス。
 * register でシャットダウン時に呼ばれる関数を登録。
 * unregister で登録した関数を削除。
 */
class ShutdownFunctionStore
{
    /**
     * @var array シャットダウン時に実行される関数を登録する領域
     */
    public array $shutdownFnStack = [];

    public function __construct()
    {
        /**
         * シャットダウン時に実行される関数を定義。
         * シャットダウン時には $this->shutdownFnStack を回して残っている各コールバックを順に実行する。
         */
        $caller = function() {
            // この $this->shutdownFnStack はこの関数が"呼ばれた時点"の $this->shutdownFnStack を参照する
            foreach($this->shutdownFnStack as $callable) {
                if(is_callable($callable)) {
                    $callable();
                }
            }
        };
        // register_shutdown_function にこのクラスで受け持ったコールバックらを実行する関数を登録
        // ここでただ一つの関数を登録するのみなのでメモリに問題を起こさない
        register_shutdown_function($caller);
    }

    /**
     * シャットダウン時に実行される関数を登録
     * @param  callable  $callable
     * @return int|string 登録ID。これを unregister に渡して登録を除去する
     */
    public function register(callable $callable)
    {
        // このクラスのインスタンスのプロパティの配列の中に、渡された関数を追加
        // シャットダウン時にはこの配列が回され、中の関数らが実行される
        $this->shutdownFnStack[] = $callable;

        // 末尾のキー転じて今回追加された関数を格納した位置のキーを返す
        return array_key_last($this->shutdownFnStack);
    }

    /**
     * シャットダウン時に実行される関数を除去
     * @param  int|string  $id  register で登録した時の返り値
     */
    public function unregister($id): void
    {
        // 渡された場所に登録してあった関数を除去
        unset($this->shutdownFnStack[$id]);
    }
}

// 使用例
$shutdownFnStore = new ShutdownFunctionStore();

$j = 0;
$registerIds = [];
for($i = 0; $i < 1e5; $i++) {
    // シャットダウン時に実行される関数を登録
    $registerIds[] = $shutdownFnStore->register(function() use (&$j) {
        echo $j."\n";
        exit(0);
    });
    $j++;
    // 100個登録される度にシャットダウン時に実行される関数を登録から削除
    if($i % 100 === 0) {
        foreach($registerIds as $id){
            $shutdownFnStore->unregister($id);
        }
    }
}

 関数を一つ登録するだけで複数のコールバックを走らせられるようにし、複数のコールバックを管理する配列を用意します。そうするとその配列への追加と削除がそのまま register_shutdown_function と unregister_shutdown_function に相当する処理になります。この様にしておくと、とりあえずシャットダウン時に実行される後始末関数を登録、シャットダウンのタイミング抜きに正常に後始末が終わったのでシャットダウン時に実行する関数の登録を削除、といった処理も書けるようになります。

  • この記事いいね! (0)