前書き
本記事は非同期について大雑把に綴っています。
対象としては、主にPocketMine-MPを利用している開発者の方々へ向けたお話となります。
1. 非同期?
疑問
しばしば目にする単語ではありますが、イマイチ得体の知れないものでしょう。他の人のプラグインをのぞいてみると、AsyncTaskなんかを使っていたり。
非同期ってなんなんだろう...
答え
非同期とはズバリ、メインスレッドに対して処理や通信の内容が別の場所で実行されるものです。
下記のコードを参考にしてみましょう。greet() escape() attack() 全ての関数が非同期で実行されるなら、どれも他の関数の実行結果を待たずに処理が開始されることでしょう。
greet();
escape();
attack();
つまり?
メインスレッドで重い処理をしてしまうと、その処理が終わるまで次の処理に移行できないことが問題です。役割を別の人に分担できればその分早く処理が終わりますね。
Blockを設置するという部分を別の場所にやらせておけば、設置している時間に待たされなくて済みますね。WEditなどで大量のブロックを設置する際、別の作業を並行して進めることが可能です。
2. スレッド?
phpでの非同期処理にはpthreadsが今まで幅広く使われてきました。メインスレッドに対して処理を待たずに実行される為、非同期として実行されます。したがってスレッドベースのこれらはマルチスレッドと呼ばれています。
・Threaded
スレッドによって実行されるものを表す。pthreadsが実行できる機能のもと。
・Thread
runメソッドを実装することでスレッドが実装できる。要は非同期処理のコア。処理が終わればスレッドは終了する。ネイティブスレッドを毎回生成して破棄するからコストが高い。
・Worker
インスタンスが破棄されるか、Worker::shutdown() が呼ばれるまで永続的にスレッドを使いまわすもの。
・Pool
Workerのグループを作るもの。与えたThreadedを分散させ(手の空いてるWorkerに割り当て)て並列処理してくれる。
もっと詳しく
AsyncTaskはThreadedのラッパークラスで、スレッドによって実行されていたタスクの終了を知らせる機能がついています。
・AsyncTask::onComplietion()
重い処理を別のスレッドに任せることができれば、仕事が楽になります。
この調子で重いデータをたくさん扱ってもらいましょう!
罠
などとは言いきれません。Threadには重大な制約があります。
「Threadにはリソース型が渡せない」
使用されているOSによりますが、とにかく実現が難しいです。
string や int と言ったプリミティブ型は、別のスレッドから別のスレッドに渡す事が可能です。しかし Player や Block といったリソース型は渡すことが出来ません。
勿論 Server::getInstance() もマルチスレッドでは実行できません。ナンテコッタイ!
救済その1
リソース型が持ち越せないということは、DBを扱う際 SQLite3 や mysqli のようなORMのオブジェクトも渡せないという事です。
考えうる対抗策としては、DBへのアクセスを永久にスレッドへ閉じ込めるものです。その為、再帰処理を実装しなければなりません。
しかし、例えば while(true) で閉じ込めると中身は高速で同じことを繰り返すため wait/notify や sleep()を使う対策が現実的でしょう。
救済その2
他にも 「メモリを犠牲にしてトランザクションにしてみる」 のも良いかもしれません。DBへのアクセスを最小限にし、データをメモリでずっと管理しておく方法です。サーバーがシャットダウンしたときにまとめて永久化する必要がありますが...
ライブラリ
モジュールのmysqli、 SQLite3には非同期でクエリを投げてくれるオプションがないわけではありませんが、使用するなら他に使いやすいライブラリやフレームワークを探したほうが良いでしょう。
非同期でDBにアクセスできるし、SQLiteやMySQLとかにも対応してる。AwaitGeneratorも使えるといいね。
3. 使い方を教えてくれよ
実践
それではAsyncTaskを使って実装してみましょう。
class K extends AsyncTask {
__construct(
private string $text
) {}
public function onRun() {
echo "onRun: ".$this->text.PHP_EOL;
}
public function onCompletion() {
echo "onCompletion: ".$this->text.PHP_EOL;
}
}
解説
この場合の実行結果はどうなるでしょうか?
まず、コンストラクタにtextという文字列が渡されています。次にonRunが実行され、コンソールに文字列が出力されます。最後に、スレッドが処理を終えた関数onCompletionが実行され、これもまたコンソールに文字列が出力されます。
もっと
色んな部分にマルチスレッドを応用できます。例えばDiscordPHPは一つのスレッドを占領しなければなりません。
他にも、curlなどといった外部との通信で時間がかかる処理も非同期で実行するのが無難でしょう。
現時点(2021 12/15)ではPHP8.1がリリースされております。その中で注目されているのが Fibersです。これはどこからでも処理を再開できるグリーンスレッドを提供しています。
後書き
ここまでみてくれてありがとう!
アドベントカレンダー初参加でした、超おもしろかったです!
開催してくれた @WhiteGrouse さん、参加してくれた皆、読んでくれたオマエ!ありがとう!
他人の実装を参考にしたり意見を聞いたりすると楽に実装できたりするので、より良い実装を取り入れてみましょう。ちなみにpthreadsよりparallelのほうが推奨されてるっぽい(そりゃそうだ)。
二回見直して修正したけど結局内容の勘違いがあって指摘してもらった、恥ずかしい。
そして今日は僕の誕生日!おめでとう!おんぎゃあ!
