web-dev-qa-db-ja.com

コードのデッドロックを防ぐためのロック戦略と手法

コードのデッドロックを防ぐための一般的な解決策は、どのスレッドがリソースにアクセスしているかに関係なく、ロックのシーケンスが共通の方法で発生することを確認することです。

たとえば、スレッドT1とT2が与えられ、T1がリソースAにアクセスし、次にBとT2がリソースBにアクセスし、次にAにアクセスします。必要な順序でリソースをロックすると、デッドロックが発生します。簡単な解決策は、特定のスレッドがリソースを使用する順序に関係なく、AをロックしてからBをロックすることです。

問題のある状況:

Thread1                         Thread2
-------                         -------
Lock Resource A                 Lock Resource B
 Do Resource A thing...          Do Resource B thing...
Lock Resource B                 Lock Resource A
 Do Resource B thing...          Do Resource A thing...

考えられる解決策:

Thread1                         Thread2
-------                         -------
Lock Resource A                 Lock Resource A
Lock Resource B                 Lock Resource B
 Do Resource A thing...          Do Resource B thing...
 Do Resource B thing...          Do Resource A thing...

私の質問は、デッドロック防止を保証するためにコーディングで使用されている他の手法、パターン、または一般的な方法は何ですか?

30
Xander Tulip

あなたが説明するテクニックは一般的であるだけではありません:それは常に機能することが証明されている1つのテクニックです。ただし、C++でスレッドコードをコーディングするときに従う必要のあるルールは他にもいくつかありますが、その中で最も重要なものは次のとおりです。

  • 仮想関数を呼び出すときにロックを保持しないでください:コードを書いているときに、呼び出される関数とその機能、コードの進化、および仮想関数がわかっている場合でも、オーバーライドされる必要があるため、最終的には、それが何をするのか、他のロックがかかるかどうかがわかりません。つまり、保証された順序が失われます。ロックの
  • 競合状態に注意してください:C++では、特定のデータがスレッド間で共有され、そのデータで何らかの同期を使用しない場合、何も通知されません。この一例は、C++ラウンジのSOチャットで数日前にLucによって、この例として投稿されました(この投稿の最後にあるコード):同期しようとしていますたまたま近所にあるelseは、コードが正しく同期されていることを意味するわけではありません。
  • 非同期動作を非表示にしてみてください:あなたは通常ソフトウェアのアーキテクチャで並行性を非表示にして、ほとんどの呼び出しコードが勝つようにしますそこにスレッドがあるかどうかは気にしないでください。これにより、アーキテクチャーの操作が簡単になります。特に、並行性に慣れていない人にとってはそうです。

しばらく続けることもできますが、私の経験では、スレッドを操作する最も簡単な方法は、可能性のあるすべての人によく知られているパターンを使用することです。プロデューサー/コンシューマーパターンなどのコードを操作します。説明は簡単で、スレッドが相互に通信できるようにするために必要なツール(キュー)は1つだけです。結局のところ、2つのスレッドが互いに同期されるonlyの理由は、それらが通信できるようにするためです。

より一般的なアドバイス:

  • ロックを使用した並行プログラミングの経験を積むまでは、ロックフリープログラミングを試してはいけません。これは、足を吹き飛ばしたり、非常に奇妙なバグに遭遇したりする簡単な方法です。
  • 共有変数の数と、それらの変数にアクセスする回数を最小限に抑えます。
  • 順序が逆になる方法が見当たらない場合でも、常に同じ順序で発生する2つのイベントを当てにしないでください。
  • より一般的には、タイミングを当てにしないでください。特定のタスクに常に特定の時間がかかるとは思わないでください。

次のコードは失敗します。

#include <thread>
#include <cassert>
#include <chrono>
#include <iostream>
#include <mutex>

void
nothing_could_possibly_go_wrong()
{
    int flag = 0;

    std::condition_variable cond;
    std::mutex mutex;
    int done = 0;
    typedef std::unique_lock<std::mutex> lock;

    auto const f = [&]
    {
        if(flag == 0) ++flag;
        lock l(mutex);
        ++done;
        cond.notify_one();
    };
    std::thread threads[2] = {
        std::thread(f),
        std::thread(f)
    };
    threads[0].join();
    threads[1].join();

    lock l(mutex);
    cond.wait(l, [done] { return done == 2; });

    // surely this can't fail!
    assert( flag == 1 );
}

int
main()
{
    for(;;) nothing_could_possibly_go_wrong();
}
31
rlc

デッドロックの回避に関しては、ロックの一貫した順序が最初で最後のWordです。

ロックレスプログラミング(スレッドがロックを待機しないため、サイクルの可能性がない)などの関連する手法がありますが、これは実際には「一貫性のないロック順序を回避する」ルールの特殊なケースです。すべてのロックを回避することにより、一貫性のないロックを回避します。残念ながら、ロックレスプログラミングには独自の問題があるため、万能薬でもありません。

範囲を少し広げたい場合は、デッドロックが発生したときにそれを検出する方法(何らかの理由でデッドロックを回避するようにプログラムを設計できない場合)と、デッドロックが発生したときにデッドロックを解除する方法があります(たとえば、常にタイムアウトでロックするか、デッドロックされたスレッドの1つにLock()コマンドを強制的に失敗させるか、デッドロックされたスレッドの1つを強制終了するだけです。しかし、そもそもデッドロックが発生しないようにすることよりも、それらはすべてかなり劣っていると思います。

(ところで、プログラムに潜在的なデッドロックがあるかどうかを自動で確認する方法が必要な場合は、valgrindのhelgrindツールを確認してください。コードのロックパターンを監視し、不整合があれば通知します-非常に便利です)

14
Jeremy Friesner

もう1つの手法は、トランザクションプログラミングです。ただし、これは通常、特殊なハードウェアを使用するため、あまり一般的ではありません(現在、そのほとんどは研究機関でのみ使用されています)。

各リソースは、異なるスレッドからの変更を追跡します。 (使用している)すべてのリソースに変更をコミットする最初のスレッドは、(それらのリソースを使用している)他のすべてのスレッドに勝ち、ロールバックされて、新しいコミットされた状態のリソースで再試行します。

主題を読むための単純な出発点は トランザクションメモリ です。

6
Martin York

あなたはデザインレベルについて質問していますが、私はいくつかのより低いレベルのプログラミングプラクティスを追加します。

  • 各関数(メソッド)をブロッキング非ブロッキング、または不明なブロッキング動作を持つものとして分類します。
  • blocking関数は、ロックを取得する関数、低速のシステムコールを呼び出す関数(実際にはI/Oを実行することを意味します)、またはblocking関数を呼び出す関数です。
  • 関数が非ブロッキングであることが保証されているかどうかは、その前提条件や例外安全性の程度と同様に、その関数の仕様の一部です。したがって、そのように文書化する必要があります。 Java私は注釈を使用します; Doxygenを使用して文書化されたC++では、関数のヘッダー解説で形式的なフレーズを使用します。
  • 指定ではない関数を呼び出して、ロックを保持している間は非ブロックにすることを検討してください。
  • このような危険なコードをリファクタリングして、危険を排除するか、危険をコードの小さなセクションに集中させます(おそらくそれ自体の機能内で)。
  • 残りの危険なコードについては、コードの解説でコードが実際に危険ではないことを非公式に証明してください。
3
Raedwald

あなたが言及した既知のシーケンスの解決策に代わるものではありませんが、Andrei Alexandrescuは、ロックの取得が意図されたメカニズムを通じて行われるコンパイル時チェックのいくつかの手法について書いています。 http://www.informit.com/articles/article.aspx?p=25298 を参照してください

3
Tony Delroy