PHP
マルチスレッド
pthreads

PHPで超簡単に非同期処理をするPromiseを作ってPackagistに公開した話

Promiseを作った背景

PHP で超絶完結にマルチスレッド処理を行いたくて、つくりました。というかカジュアルにスレッド使えないの辛くない? :sob:

PHPのスレッドに関しては PHP の 拡張機能である pthreads 3 が刷新されマルチスレッドが非常に扱いやすくなったものの、とはいえ扱いづらさは少なからず残っているためどうにかしたいっていう思いがありました。
また、JavaScriptのPromiseライクで扱えたらどれだけ楽かなといったのもあります。

構想

Promiseを作るにあたってこう動かせたらいいなぁっていうざっくりとした構想を書き示しておきます。
(私が基本的にものを作るときはシンプルになるべく動かしたい形を考えてから作るようにしています。Using Driven的な笑)

$promise = (new Promise(function ($resolve, $reject) {
    // なんか
    $resolve();
}))->then(function () {
    // なんか
})->catch(function () {
    // なんか
})

Promise::all($promise)->then(function () {
    // なんか
});

最終的に、上記のような形にすることができました。

構想を実現するにあたって

  • Promise, then, catchに渡されるクロージャを非同期で動かす必要がある
    • pthreads 3 よりクロージャをスレッド生成時に渡せるようになったので要件が満たせました。
  • catchという予約語を名称に使いたい
    • PHPのアップデートにより可能になっています。(他の予約語も可能です)
    • __call 使えば実現可能ではあったもののコード自体が汚くなるのは自明だったので、少し懸念点でした。

要件は満たせていたので、実装に踏み切りました。

設計

どこをメインコンテキストで動かして、どこをスレッドで動かすのか明確にするため図を作り設計を進めていきました。

The Promise flows

具体的な流れとして

  • メインスレッドで Promise を呼び出し、別スレッドで動かす PromiseContext を用意します。
  • PromiseContextthen, catch を実装しており、それぞれ PromiseContext での処理が終わるまで待機状態(Threaded::wait())とし、 PromiseContext が保有及び処理によって切り替わるステータスをもとに then, catch のいずれかを実行します。
  • 最終的に処理が完了したらPromiseを返し、以降同様の処理が続きます。
  • 使用するユーザーはスレッドが終了したかどうかを考えずに楽にスレッドを扱うため Safety Manager という機能を組み込んでいます。
    • Safety Manager はスレッドが安全に終了することを保証するためのこのPromiseの機能です。

導入要件や提供しているメソッドや仕組みは実際のGitHubリポジトリを参照してみてください。

とりあえず初めてみる

pthreads拡張のインストール

pthreads拡張のインストールは少し癖があるので、下記に私の方で動作検証に使っているDockerfileを記載しますのでdocker execなどを用いてお試しください。

FROM centos:7

# Setup
RUN yum -y install epel-release wget
RUN cd /tmp && wget http://jp2.php.net/get/php-7.2.7.tar.gz/from/this/mirror -O php-7.2 && tar zxvf php-7.2

# Dependencies installation
RUN yum -y install git gcc gcc-c++ make libxml2-devel libicu-devel openssl-devel

# PHP installation
RUN cd /tmp/php-7.2.7 && \
    ./configure --enable-maintainer-zts --enable-pcntl --enable-intl --enable-zip --enable-pdo --enable-sockets --with-openssl && \
    make && \
    make install

# pthreads installation
RUN yum -y install autoconf
RUN cd /tmp && git clone https://github.com/krakjoe/pthreads.git && cd pthreads && \
    phpize && \
    ./configure && \
    make && \
    make install

# Add an extension to php.ini
RUN echo extension=pthreads.so >> /usr/local/lib/php.ini

Promiseの準備

php-promise/promise を composer requireします。

$ composer require php-promise/promise:dev-master

Promiseを走らせてみます。

$promises = [];
$promises[] = (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
    sleep(5);
    $resolve("1個目のPromise\n");
}))->then(function ($message) {
    echo $message;
});

$promises[] = (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
    sleep(5);
    $resolve("2個目のPromise\n");
}))->then(function ($message) {
    echo $message;
});

Promise::all($promises);

上記は 5秒後1個目のPromise2個目のPromise が出力されます。
Resolver 及び Rejecter が実装された $resolve$rejectはそれぞれPromiseの処理完了時の then, catchを呼び出すために提供されています。

複数のPromiseを動かす

下記のようにすると複数のPromiseの処理を行うことができます。

Promise::all([
    (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
        // なんか
    })),
    (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
        // なんか
    })),
    (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
        // なんか
    })),
    (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
        // なんか
    })),
    (new \Promise\Promise(function (Resolver $resolve, Rejecter $reject) {
        // なんか
    })),
]);

最後に

これを使えば非同期処理楽ちん!PHPでカジュアルにスレッド使おうな :rolling_eyes: っていうのを目指していきます :pray: :sob:

今後は async, await あたりの実装、libevent, pcntlを使ったpthreadsの導入が難しい環境でも使えるようにする(できれば)っていうのをやっていきます(できれば) :sheep: