マルチタスクやマルチスレッド環境向けのミドルウェアでこんな仕組みをよく見かけます。
サービス関数(同期版)
int FooFunc(int Param1, int Param2, void* Result);
サービス関数(非同期版)
int FooFunc_A(int Param1, int Param2, void* Result);
両者の処理は同じものですが、_A付きを呼ぶと、呼び出し元のコンテキストと異なるコンテキスト(別タスクや別スレッド)で処理が実行される点が異なります。
FooFunc_Aの中では、処理を一意に決めるコード(処理番号や処理ID、あるいは処理名)と引数を固めてポンと別タスクに投げつけます。投げるには、メールスロットや共有メモリなどが利用されます。投げつけたら、結果が通知されるまで待ちます。このとき、rtosが提供する、できるだけCPUパワーを無駄にしないように待ちます。
引き受け側のタスクは、処理を実行して結果をResultのさすメモリに格納します。それから呼び出し元のタスクに通知します。呼び出しもとのタスクは結果を受け取り、関数の返却値として処理を続行します。
ちょっと図を書かないとワケが分からないかもしれません。
名前には非同期版と付いているのですが、呼び出し元のタスクの処理の書き方としては、同期処理です。非同期に甘くかぶれた人から見れば、
そんなのに何の意味があるの?
という感想しかないかもしれません。
今日はちょっと詳しく書けませんが、例えば、引き受け側で処理のバランスを取ったり、複数のタスクからの処理要求をシリアライズして、まとめて最適化実行したりするのに使えます。あるいはマルチコアでひとつの処理を複数のコアで分担させることすら可能です。呼び出しもとの実装の見た目はそのままで。
最大のメリットは、呼び出し元の処理の書き方は同期処理風に書いておいて、つまりマルチタスク・マルチスレッドは意識しないで書けて、あとで別タスクに処理を分けたりすることが容易になることだと思います。ダラダラとした長ったらしい処理を書くときに、いちいち非同期待ち合わせやタスク間通信の仕組みを書いていたら本質が見えなくなりますからね。
最適なタスク分割は、そんな”非同期版”を使ったくらいでは成し遂げられないとおっしゃるかもしれません。もちろんその通りです。ですが、これで充分な用途がほとんどなのも事実です。
この仕組みがあれば処理の本筋を書く人と、デッドロックがどうのこうのということを考える人が、明確に分業可能であることです。非同期版関数の中で厄介ごとは隠蔽化できるかわりに、フクザツなタスク関係は表現できにくくなります。しかしそんな凝った仕組みをいちいち作りこむから、納期は遅れ、重大バグがいつまで経っても摘出できないのです。
そんなわけでこのフツウの人のためのフツウの非同期処理フレームワークが一般的になって久しいわけです。ところが悲しいかな実装上の欠陥も一般的になっています。あくまでも機能や考え方は教科書通りで問題がないのですが・・・
欠陥の最大のものは、デバッグ困難なことです。
同期処理つまり関数呼び出しやサブルーチン呼び出しの連なりはデバッグが容易です。ある処理が実行されないとき、それを呼び出している処理は、ソースをケンサクすれば出てきます。各種タグジャンプ機能をエディタで利用することもできます。VisualStudioのような統合開発環境においてなら「定義場所へジャンプ」ですよね。
ところが非同期処理版_Aの呼び出しは、各種エディタでは追いかけることが出来ません。追いかけるためにはミドルウェアやフレームワークを理解し、処理番号や処理名と実際に処理が実装されている箇所との関連付けを"あらかじめ知っている必要が"あるのです。その関連付け自体をいじるような人、例えば、処理名と処理関数の割付テーブルを変更するような人、はこの困難の最大の犠牲者かもしれません。
この欠点はrtosに限らず、OSの非同期通信機構を用いるときに常につきまとう問題です。そしてこのデバッグ困難性こそが、rtos環境や非同期処理の生産性の低さの最大の原因であると思うのです。
デバッグのためのインフラをうまく作りこんでおかないと、いくらよい仕組みであっても、いつまで経っても安定して動かないシステムの不安定要素になってしまいます。
アーキテクトのみなさんは、一枚岩を磨き上げるのがお好きなようですが、使いやすいシステムというのは、乗用車のように、レバー一ひねりで燃料供給口や、ボンネットがパカっと開く仕組みを備えているモノなのでです。あるいは、天井裏には作業員のための通路があるものなのです。
裏口はいつでも開けておけ
というわけです。
その辺を踏まえて、宜しくご検討をお願い致します。
コメント