私が管理している私の新しいチームでは、コードの大部分はプラットフォーム、TCPソケット、httpネットワークコードです。すべてC++です。そのほとんどは、チームを去った他の開発者からのものです。チームの現在の開発者は非常に頭が良いですが、経験の点ではほとんどジュニアです。
私たちの最大の問題は、マルチスレッドの同時実行のバグです。ほとんどのクラスライブラリは、いくつかのスレッドプールクラスを使用して非同期になるように記述されています。クラスライブラリのメソッドは、多くの場合、実行時間の長いタスクを1つのスレッドからスレッドプールにエンキューし、そのクラスのコールバックメソッドが別のスレッドで呼び出されます。その結果、不適切なスレッドの仮定に関連するEdgeケースのバグがたくさんあります。これにより、同時実行の問題から保護するための重要なセクションとロックだけではない微妙なバグが発生します。
これらの問題をさらに難しくしているのは、修正しようとする試みがしばしば正しくないことです。チームが(またはレガシーコード自体の内部で)試行しているいくつかの間違いには、次のようなものがあります。
よくある間違い#1-共有データの周りにロックをかけるだけで同時実行の問題を修正しますが、メソッドが予期した順序で呼び出されない場合に何が起こるかを忘れます。これは非常に簡単な例です:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
したがって、OnHttpNetworkRequestCompleteが発生しているときにShutdownが呼び出される可能性があるというバグがあります。テスターがバグを見つけ、クラッシュダンプをキャプチャして、バグを開発者に割り当てます。次に、このようにしてバグを修正します。
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
上記の修正は、さらに微妙なEdgeケースがあることに気づくまでは問題ありません。 Shutdownが呼び出された場合beforeOnHttpRequestCompleteが呼び出された場合はどうなりますか?私のチームが持っている実際の例はさらに複雑であり、コードレビュープロセス中にEdgeのケースを見つけるのはさらに困難です。
よくある間違い#2-盲目的にロックを終了してデッドロックの問題を修正し、他のスレッドが完了するのを待ってからロックに再度入る-しかし、オブジェクトが他のオブジェクトによって更新されたばかりのケースを処理せず糸!
Common Mistake#-オブジェクトは参照カウントされていますが、シャットダウンシーケンスはそのポインターを「解放」します。しかし、まだ実行中のスレッドがインスタンスを解放するのを待つのを忘れています。そのため、コンポーネントは正常にシャットダウンされ、それ以上の呼び出しを予期していない状態のオブジェクトに対して、疑似コールバックまたは遅延コールバックが呼び出されます。
他のEdgeケースもありますが、一番下の行はこれです:
マルチスレッドプログラミングは、賢い人でも簡単に理解できます。
これらの間違いを見つけたとき、私はより適切な修正を開発するために各開発者とエラーを話し合うことに時間を費やしています。しかし、「正しい」修正が触れることを含む膨大な量のレガシーコードのために、彼らはそれぞれの問題を解決する方法についてしばしば混乱していると思います。
間もなく出荷され、適用しているパッチは次のリリースでも保持されると確信しています。その後、コードベースを改善し、必要に応じてリファクタリングする時間を確保します。すべてを書き直すだけの時間はありません。そして、コードの大部分はそれほど悪いものではありません。しかし、スレッドの問題を完全に回避できるようにコードをリファクタリングしたいと考えています。
私が検討しているアプローチの1つはこれです。重要なプラットフォーム機能ごとに、すべてのイベントとネットワークコールバックがマーシャリングされる専用のシングルスレッドを用意します。メッセージループを使用したWindowsのCOMアパートメントスレッドに似ています。長いブロック操作は引き続きワークプールスレッドにディスパッチされますが、完了コールバックはコンポーネントのスレッドで呼び出されます。コンポーネントが同じスレッドを共有する可能性さえあります。次に、スレッド内で実行されるすべてのクラスライブラリを、シングルスレッドの世界を想定して記述できます。
その道を進む前に、マルチスレッドの問題に対処するための他の標準的な手法や設計パターンがあるかどうかにも非常に興味があります。そして私は強調しなければなりません-ミューテックスとセマフォの基本を説明する本を超えた何か。どう思いますか?
また、リファクタリングプロセスに向けた他のアプローチにも興味があります。次のいずれかを含みます。
糸の周りのデザインパターンに関する文献や論文。ミューテックスとセマフォの紹介を超えた何か。他のスレッドからの非同期イベントを処理するようにオブジェクトモデルを設計する方法だけでも、大規模な並列処理は必要ありません正しく。
さまざまなコンポーネントのスレッディングを図式化して、ソリューションの研究と進化を容易にする方法。 (つまり、オブジェクトおよびクラス全体のスレッドを議論するためのUMLの同等物)
マルチスレッドコードの問題について開発チームを教育する。
あなたはどうしますか?
あなたのコードには、それだけではなく、重要なother問題があります。ポインタを手動で削除しますか? cleanup
関数を呼び出しますか?ああ。また、質問のコメントで正確に指摘されているように、ロックにRAIIを使用しないでください。これは、もう1つのかなり壮大な失敗であり、DoSomethingImportant
が例外をスローすると、恐ろしいことが起こります。
このマルチスレッド化されたバグが発生しているという事実は、コアの問題の兆候です-コードに非常に悪いセマンティクスanyスレッド化状況があり、完全に信頼できないツールと元イディオム。私があなただったら、それがシングルスレッドで機能することは言うまでもありません。
よくある間違い#3-オブジェクトが参照カウントされている場合でも、シャットダウンシーケンスはそのポインターを「解放」します。しかし、まだ実行中のスレッドがインスタンスを解放するのを待つのを忘れています。そのため、コンポーネントは正常にシャットダウンされ、それ以上の呼び出しを予期していない状態のオブジェクトに対して、疑似コールバックまたは遅延コールバックが呼び出されます。
参照カウントの全体的なポイントは、スレッドはすでにそのインスタンスを解放しているです。そうでない場合は、スレッドがまだ参照しているため、破棄できません。
使用する std::shared_ptr
。すべてのスレッドが解放されると(そしてnobodyなので、関数へのポインタがないため、関数を呼び出すことができます)、thenデストラクタが呼び出されます。これは安全が保証されています。
次に、IntelのスレッドビルディングブロックやMicrosoftのParallel Patterns Libraryなどの実際のスレッドライブラリを使用します。独自のコードを書くことは時間がかかり、信頼性が低く、コードには必要のないスレッドの詳細がたくさんあります。独自のロックを行うことは、独自のメモリ管理を行うのと同じくらい悪いことです。彼らはすでにあなたの使用のために正しく機能する多くの汎用の非常に有用なスレッド化イディオムを実装しています。
他のポスターは、核となる問題を修正するために何をすべきかについてよくコメントしています。この投稿は、適切な方法ですべてをやり直す時間を購入するのに十分なだけ、レガシーコードにパッチを適用するという、より差し迫った問題に関係しています。言い換えれば、-これは正しい方法ではありません物事を実行するための手段です。
重要なイベントを統合するというあなたのアイデアは良い出発点です。私は、順序依存性がある場合は常に、単一のディスパッチスレッドを使用してすべての主要な同期イベントを処理することになるでしょう。スレッドセーフなメッセージキューをセットアップし、現在同時実行に影響されやすい操作(割り当て、クリーンアップ、コールバックなど)を実行する場合は、代わりにそのスレッドにメッセージを送信して、操作を実行またはトリガーさせます。この1つのスレッドがすべてのワークユニットの開始、停止、割り当て、およびクリーンアップを制御するという考えです。
ディスパッチスレッドはnotで説明した問題を解決します。問題を1か所に統合するだけです。それでも、予期しない順序で発生するイベント/メッセージについて心配する必要があります。重要な実行時間のあるイベントは、他のスレッドに送信する必要があるため、共有データの同時実行性にはまだ問題があります。これを軽減する1つの方法は、参照によるデータの受け渡しを回避することです。可能な限り、ディスパッチメッセージのデータは、受信者が所有するコピーにする必要があります。 (これは、他の人が述べたように、データを不変にするという流れに沿っています。)
このディスパッチアプローチの利点は、ディスパッチスレッド内に、少なくとも特定の操作が順次発生していることを知っている、一種の安全な避難所があることです。欠点は、ボトルネックと余分なCPUオーバーヘッドが発生することです。最初はこれらのどちらかについて心配しないことをお勧めします。ディスパッチスレッドにできるだけ多く移動することにより、最初に正しい操作の測定値を取得することに焦点を当てます。次に、プロファイリングを行って、何が最もCPU時間を消費しているかを確認し、正しいマルチスレッド化手法を使用してディスパッチスレッドからシフトアウトを開始します。
繰り返しますが、私が説明しているのは、物事を行うための正しい方法ではありませんが、商業的な納期を満たすのに十分小さい増分で正しい方法にあなたを移動させることができるプロセスです。
示されているコードに基づいて、WTFの山があります。不十分に書かれたマルチスレッドアプリケーションを段階的に修正することは、不可能ではないにしても非常に困難です。大幅な手直しなしではアプリケーションは信頼できないことを所有者に伝えます。共有オブジェクトと対話しているコードのすべてのビットを検査および再処理することに基づいて、それらに見積もりを与えます。最初に彼らに検査のための見積もりを与えます。その後、リワークの見積もりを出すことができます。
コードを作り直すときは、間違いなく正しいようにコードを書くことを計画する必要があります。それを行う方法がわからない場合は、そうする人を見つけるか、同じ場所に行き着くでしょう。
アプリケーションのリファクタリングに専念できる時間があれば、アクターモデルを確認することをお勧めします(例: Theron 、 Casablanca 、 libcppa 、 [〜#〜] caf [〜#〜] C++実装の場合)。
アクターは、同時に実行され、非同期メッセージ交換を使用してのみ相互に通信するオブジェクトです。したがって、スレッド管理、ミューテックス、デッドロックなどのすべての問題は、アクター実装ライブラリによって処理され、オブジェクト(アクター)の動作の実装に集中できます。
あなたのための1つのアプローチは、最初にトピックに対していくつかの reading を実行し、おそらく1つまたは2つのライブラリを見て、アクターモデルをコードに統合できるかどうかを確認することです。
私はこのモデル(の簡略版)を私のプロジェクトで数か月間使用しており、その堅牢性に驚いています。
よくある間違い#1-共有データの周りにロックをかけるだけで同時実行の問題を修正しますが、メソッドが予期した順序で呼び出されない場合に何が起こるかを忘れます。これは非常に簡単な例です:
ここでの間違いは「忘れる」ではなく、「修正しない」ことです。予期しない順序で何かが起こっている場合、問題があります。あなたはそれを回避しようとする代わりにそれを解決すべきです(何かにロックを掛けることは通常回避策です)。
アクターモデル/メッセージングをある程度適合させ、関心を分離するようにしてください。 Foo
の役割は明らかに、ある種のHTTP通信を処理することです。これを並行して行うようにシステムを設計する場合、オブジェクトのライフサイクルを処理し、それに応じて同期にアクセスする必要があるのは、その上の層です。
多数のスレッドが同じ可変データを操作するようにするのは困難です。しかし、それが必要になることもまれです。これを必要とするすべての一般的なケースは、すでにより管理しやすい概念に抽象化されており、主要な命令型言語について何度も実装されています。あなたはそれらを使用する必要があります。
あなたの問題はかなり悪いですが、C++の不適切な使用の典型です。コードレビューはこれらの問題のいくつかを修正します。 30分、1組の眼球は結果の90%を示します(これについての引用はグーグル可能です)
#1問題ロックのデッドロックを防ぐために、厳密なロック階層があることを確認する必要があります。
Autolockをラッパーとマクロで置き換えると、これを行うことができます。
ラッパーの後ろに作成されたロックの静的なグローバルマップを保持します。マクロを使用して、finenameと行番号の情報をAutolockラッパーコンストラクターに挿入します。
静的なドミネーターグラフも必要です。
ロックの内側では、ドミネーターグラフを更新する必要があります。順序の変更があった場合は、エラーをアサートして中止します。
広範囲にわたるテストの後、潜在的なデッドロックのほとんどを取り除くことができます。
コードは学生の練習問題として残されています。
その後、問題#2は解消されます(ほとんど)
あなたのアーキテクチュアルなソリューションが機能します。私は以前、使命と生命の重要なシステムでそれを使用しました。私の考えはこれです
パブリック変数またはゲッターを介してデータを共有しないでください。
外部イベントは、マルチスレッドディスパッチを介して、1つのスレッドによって処理されるキューに送られます。これで、イベント処理に関する理由を並べ替えることができます。
スレッドをまたぐデータ変更は、スレッドセーフなキューに入り、1つのスレッドによって処理されます。サブスクリプションを作成します。これで、データフローに関する理由を並べ替えることができます。
データが町を越えて移動する必要がある場合は、データキューに公開します。それはそれをコピーしてサブスクライバーに非同期で渡します。また、プログラム内のすべてのデータ依存関係を壊します。
これはかなり安い俳優モデルです。ジョルジオのリンクが役立ちます。
最後に、オブジェクトのシャットダウンに関する問題
参照カウントでは、50%を解決しました。残りの50%は、カウントコールバックを参照することです。コールバックホルダーに参照を渡します。その後、シャットダウン呼び出しは、refcountでゼロカウントを待つ必要があります。複雑なオブジェクトグラフを解決しません。それは実際のガベージコレクションに組み込まれています。 (これはins Javaは、finalize()がいつ呼び出されるか、または呼び出されるかどうかについて何の約束もしないためです。その方法でプログラミングから抜け出すためです。)
将来の探検家のために:アクターモデルに関する回答を補足するために、CSP( communicating順次プロセス )を追加します。これには、プロセス計算のより大きなファミリーにノードを含めます。CSPはアクターに似ています。モデルですが、分割方法が異なります。あなたはまだたくさんのスレッドを持っていますが、それらは具体的にはお互いにではなく特定のチャネルを介して通信します。どちらのプロセスも、どちらかが発生する前に、それぞれ送受信する準備ができている必要があります。 CSPコードが正しいことを証明するための 形式化された言語 もあります。私はまだCSPの使用にかなり移行していますが、いくつかのプロジェクトで数か月間使用していますが、これは大幅に簡略化されています。
ケント大学にはC++実装があります( https://www.cs.kent.ac.uk/projects/ofa/c++csp/ 、 https:// github.com/themasterchef/cppcsp2 )。
あなたの例を見てください:Foo :: Shutdownが実行を開始するとすぐに、OnHttpRequestCompleteを呼び出して実行することができなくなっているはずです。これは実装とは何の関係もなく、機能しないだけです。
また、OnHttpRequestCompleteへの呼び出しが実行されている間(間違いなくtrue)、Foo :: Shutdownは呼び出し可能であってはならず、OnHttpRequestCompleteへの呼び出しが未解決の場合はおそらく呼び出してはいけないと主張することもできます。
正しくするための最初のことは、ロックなどではなく、何が許可されているかどうかのロジックです。単純なモデルは、クラスに0個以上の不完全な要求、まだ呼び出されていない0個以上の完了、実行中の0個以上の完了、およびオブジェクトがシャットダウンするかどうかを指定することです。
Foo :: Shutdownは、完了の実行を完了し、可能であればシャットダウンできるようになるまで不完全な要求を実行し、それ以上の完了の開始を許可せず、より多くの要求の開始を許可しないことが期待されます。
何をする必要があるか:正確にと言って、関数に仕様を追加します。 (たとえば、Shutdownが呼び出された後、httpリクエストの開始が失敗する場合があります)。次に、仕様を満たすように関数を記述します。
ロックは、シェア変数の変更を制御するために可能な限り短い時間にのみ使用するのが最適です。したがって、ロックによって保護されている変数「performingShutDown」がある可能性があります。
糸の周りのデザインパターンに関する文献や論文。ミューテックスとセマフォの紹介を超えた何か。他のスレッドからの非同期イベントを正しく処理するためにオブジェクトモデルを設計する方法だけで、大規模な並列処理も必要ありません。
私は現在これを読んでいて、C++で発生する可能性のあるすべての問題とその回避方法を説明しています(新しいスレッドライブラリを使用していますが、グローバルな説明はあなたのケースに当てはまると思います): http:// www.Amazon.com/C-Concurrency-Action-Practical-Multithreading/dp/1933988770/ref=sr_1_1?ie=UTF8&qid=1337934534&sr=8-1
さまざまなコンポーネントのスレッディングを図式化して、ソリューションの研究と進化を容易にする方法。 (つまり、オブジェクトおよびクラス全体のスレッドを議論するためのUMLの同等物)
私は単純化されたUMLを個人的に使用しており、メッセージは非同期で行われると想定しています。また、これは「モジュール」にも当てはまりますが、モジュール内では知りたくありません。
マルチスレッドコードの問題について開発チームを教育する。
本は役に立ちますが、私は、エクササイズ/プロトタイピングと経験豊富なメンターがベターだと思います。
あなたならどうしますか?
プロジェクトで並行処理の問題を理解していない人に作業を行わせることは完全に避けます。しかし、そうすることはできないと思うので、特定のケースでは、チームがより教育を受けていることを確認する以外に、私にはわかりません。
あなたはすでに問題を認め、積極的に解決策を探しているところです。これが私がすることです:
あなたならどうしますか?
正直に言うと;私はすぐに逃げるだろう。
同時実行の問題は[〜#〜] nasty [〜#〜]です。何かが数か月間完全に機能し、その後(いくつかの特定のタイミングにより)顧客の顔が突然爆発し、何が起こったのかを理解する方法がなく、ニース(再現可能な)バグレポートが表示される見込みがなく、方法がないそれは、ソフトウェアとは何の関係もないハードウェアの不具合ではないことを確認するためです。
同時実行性の問題を回避するには、設計段階で開始する必要があります。つまり、正確にどのように実行するか(「グローバルロック順序」、アクターモデルなど)から始めます。これは、次のリリース後にすべてが自己破壊しないことを期待して、狂ったパニックで修正しようとするものではありません。
ここでは冗談ではありません。あなた自身の言葉( "そのほとんどは、チームを去った他の開発者に由来します。チームの現在の開発者は非常に賢いですが、経験の点ではほとんどジュニアです。 ")は、人々が私が提案していることをすでにすべて行ったことを示しています。