【PHP7.4新機能】弱参照(WeakReference)をキャッシュに使うってどういうことよ、を考えてみた


先日、別の記事(【PHP7.4新機能】弱参照(WeakReference)とGCとメモリリークについて整理したよ!)で、PHP7.4から追加されたWeakReference(弱参照)のphp.netの記載「この機能は、キャッシュのようなデータ構造を実装するのに役立ちます。」が良く分からなかったという話を書いたのですが、「どういうところが分からなかったのか」という質問を知人からもらったので、そのあたりをまとめてみたいと思います。
かなりモヤモヤしているので、一旦世に晒すことで、このモヤっと感を成仏させたいと思います。

わからない理由

キャッシュとしての利用として、たとえば下記のようなコードを想定しましょう。

class Cache
{
    private array $cacheObjects;

    public function get($id)
    {
        if (!isset($this->cacheObjects[$id])) {
            return null;
        }
        return $this->cacheObjects[$id]->get();
    }

    public function set($id, $object)
    {
        $this->cacheObjects[$id] = WeakReference::create($object);
    }
}

これを使って嬉しいシーンが良く分からなかった。

なぜならば、PHPのGCの仕組み上、変数に割当たったメモリ領域は、その変数がスコープから外れた時に解放されます。ということは、キャッシュを上記プログラム例のように弱参照で実現するためには、実行しているプロセス中に、強参照を持った変数が生存中である必要がある、ということになります。
そうすると、その強参照を持った変数を直接参照すればよく、別途キャッシュを作る必要などないのでは?と思ったからです。

そこで、ある制約を定義して、それが具体的にどんなシーンがありえそうかを考えました。

・コストのかかる処理(DBアクセスなど)によってオブジェクトを生成するクラスXがある
・このオブジェクト(強参照)は、クラスXの内部にのみ閉じており、他のクラスからアクセスすることを許可しない。
・このオブジェクト(強参照)のライフサイクルは、クラスXが支配している。
・他のクラスからこのオブジェクトを参照する場合は、クラスXから弱参照のみ得ることができる。

うーん…。キャッシュでの利用を前提としたDBアクセスクラスとか…?例えば、定期的にDBにアクセスして最新の情報を読み出して、負荷軽減をするとか…?
長時間動き続けるバッチ処理やデーモンとして待ち受ける系の処理くらいでしか使わなさそう、かつ、なんだかすごい無理やり感はあるが、そういうクラスの可能性はゼロではない、ということで次に進みましょう。

おそらくこういう事らしい

色々調べたところ、「PHPにおける(※)弱参照のキャッシュへの活用」というのはどうやらこういうことを言いたいらしい。

  • キャッシュの元情報(DBなど)の変更があった場合、キャッシュ側の削除を行い、再キャッシュを行う必要がある。
  • キャッシュ側の削除が単純な場合は問題ないが、手続きが複雑であったり、キャッシュ側が複数存在している場合、全て削除を行う必要がある。
  • キャッシュ側の持ち方を弱参照にしておくことで、キャッシュ元のオブジェクトを消滅させた時に、キャッシュ側も連動して消滅させることができる。
  • これにより、よくある、キャッシュ側の更新漏れによって古い情報を使って処理してしまった、というようなバグを抑制することができる。

※PHPにおける…前述の通り、PHPの場合は参照カウンタ方式のGCのため不要になった領域は即時開放されます。しかし、他のGCアルゴリズム(マーク&スイープ方式)を採用している言語の場合は、不要になった領域でもGCが走るまで存在し続けるタイミングがあるので、この特性を使って「いつ消えてもいいんだけど、メモリ上に残っている間はキャッシュとして使える」というような弱参照の使い方があるようです。ただ、それもちょっとどうなんだろう?という疑問は残りますが。

では、実際にコードを書いて理解してみたいと思います。

$user = findUserById(100); // コストのかかるSQLを実行してキャッシュ元情報を取得 例:ユーザーID:100のユーザー情報を取得

$cache = new Cache();
$cache->set(100, $user);

/*(いろんな処理) */

// キャッシュから取得
$bar = $cache->get(100);

/*(いろんな処理) */

// キャッシュ元情報を削除した
deleteUser(100);

// 本来はキャッシュの削除処理が必要なのだが、キャッシュ側は弱参照なので$userをunsetするだけでキャッシュ側も消滅する
unset($user);

$baz = $cache->get(100); // すでに開放されているのでNULLが取れる

上記の例は極めてシンプルな例ですが、キャッシュの削除まわりが複雑な場合は、たしかに弱参照を使う意義はありそうです。
ただ…正直なところ、こういうトリッキーとも取れる手法を使ってキャッシュ制御するよりは、少々泥臭くてもゴリゴリやるほうがいいのではないかと思いました。

今なお、納得できず

  • 弱参照とは別に強参照を生存させる必然性があり、
  • かつ、キャッシュの削除などを手軽に行いたい

という、すごく限定的なシーンで初めて、「弱参照によるキャッシュ」の実現がありそうなので、ちょっと納得感が薄いです。
弱参照の主たる目的としては、前の記事に書かせて頂いた通り、循環参照によるメモリ領域の圧迫回避の方がしっくり来る、という理解をしています。

※この記事をご覧になった方で、PHPにおける弱参照の具体的な活用用途(特にキャッシュについて)ご存じの方がいましたら、教えてください…。

ではでは。