前書き
本記事は非同期についてひたすら大雑把に綴っています。主にPocketMine-MPを利用している開発者の方々へ、またphp前提のお話となっております。
1. 非同期?
疑問
しばしば目にする単語ではありますがイマイチ概要のつかめない、得体の知れないものだと思います。他人のプラグインをのぞいてみると、AsyncTask
なんかを使っていたり。
非同期ってなんなんだろう...
答え
非同期とはズバリ、メインスレッドに対して処理や通信の内容が別の場所で実行されるものです。
下記のコードを参考にしてみましょう。echo "1"
が実行された後async()
という非同期関数が実行されると、この場合echo "2"
がasync()
の実行結果を待たずに実行されます。
echo "1";
async();
echo "2";
つまり?
要はメインスレッドで重い処理をしてしまうと、その処理が終わるまで次の処理に移行できないわけです。別の場所に役割を分担できればその分早く処理が終わりますね。
例えばWEditなんかを使っていると、Blockを大量に設置してしまった場合にBlockを全て置き終わるまで待たされてしまいます。
Blockを設置するという部分を別の場所にやらせておけば、設置している時間に待たされなくて済みますね。
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
です。これはどこからでも処理を再開できるグリーンスレッドを提供しています。
後書き
ここまでみてくれてありがとう!他人の実装を参考にしたり意見を聞いたりすると楽に実装できたりするので、より良い実装を取り入れてみましょう。ちなみにpthreadsよりparallelのほうが推奨されてるっぽい(そりゃそうだ)。
谢谢(ホンマにありがとう)
アドベントカレンダー初参加でした。超おもしろかったです。講義時間に文章を考えてました。開催してくれた @WhiteGrouse さん、参加してくれた皆、読んでくれたオマエ!ありがとう!
令和コソコソ噂話
二回見直して修正したけど結局内容の勘違いがあって指摘してもらった、恥ずかしい。
そして今日は僕の誕生日!おめでとう!おんぎゃあ!