web-dev-qa-db-ja.com

std :: lock_guardの例、それが機能する理由の説明

私のプロジェクトでは、非常によく書かれている可能性のあるリソース上のスレッド間の通信を必要とするポイントに達しているため、同期は必須です。ただし、基本レベル以外の同期については本当に理解していません。

このリンクの最後の例を検討してください。 http://www.bogotobogo.com/cplusplus/C11/7_C11_Thread_Sharing_Memory.php

#include <iostream>
#include <thread>
#include <list>
#include <algorithm>
#include <mutex>

using namespace std;

// a global variable
std::list<int>myList;

// a global instance of std::mutex to protect global variable
std::mutex myMutex;

void addToList(int max, int interval)
{
    // the access to this function is mutually exclusive
    std::lock_guard<std::mutex> guard(myMutex);
    for (int i = 0; i < max; i++) {
        if( (i % interval) == 0) myList.Push_back(i);
    }
}

void printList()
{
    // the access to this function is mutually exclusive
    std::lock_guard<std::mutex> guard(myMutex);
    for (auto itr = myList.begin(), end_itr = myList.end(); itr != end_itr; ++itr ) {
        cout << *itr << ",";
    }
}

int main()
{
    int max = 100;

    std::thread t1(addToList, max, 1);
    std::thread t2(addToList, max, 10);
    std::thread t3(printList);

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

この例は、3つのスレッド(2つのライターと1つのリーダー)が共通のリソース(リスト)にアクセスする方法を示しています。

2つのグローバル関数が使用されます。1つは2つのライタースレッドで使用され、もう1つはリーダースレッドで使用されます。両方の関数は、lock_guardを使用して、同じリソースであるリストをロックダウンします。

今、私が頭をかき回すことができないものです:リーダーは、2つのライタースレッドとは異なるスコープでロックを使用しますが、それでも同じリソースをロックダウンします。これはどのように機能しますか?ミューテックスに関する私の限られた理解は、ライター関数に役立ちます。まったく同じ関数を使用する2つのスレッドがあります。私が理解できるのは、あなたが保護区域に入ろうとしているときにチェックが行われ、他の誰かがすでに中にいるなら、あなたは待つことです。

しかし、スコープが異なる場合はどうでしょうか?これは、プロセス自体よりも強力なメカニズムがあることを示し、「ランタイム」スレッドの実行をブロックするランタイム環境があります。しかし、c ++にはそのようなことはないと思っていました。だから私は途方に暮れています。

ここで何が起こっているのでしょうか?

15
Deviatore

myMutexはグローバルであり、myListを保護するために使用されます。 guard(myMutex)は単にロックを有効にし、ブロックからの出口が破壊を引き起こし、ロックを無効にします。 guardは、ロックを有効または無効にする便利な方法です。

これにより、mutexはデータを保護しません。 a wayを提供するだけでデータを保護します。データを保護するのは設計パターンです。したがって、リストを次のように変更する独自の関数を作成すると、mutexはそれを保護できません。

void addToListUnsafe(int max, int interval)
{
    for (int i = 0; i < max; i++) {
        if( (i % interval) == 0) myList.Push_back(i);
    }
}

ロックは、データにアクセスする必要のあるすべてのコードがアクセス前にロックに関与し、完了後にロックが解除された場合にのみ機能します。すべてのアクセスの前後にロックを有効または無効にするこのデザインパターンがデータを保護します(myList

なぜmutexを使用するのか、なぜboolを使用しないのか疑問に思うでしょう。はい、できますが、bool変数が以下のリストを含むがこれらに限定されない特定の特性を示すことを確認する必要があります。

  1. 複数のスレッド間でキャッシュ(揮発性)されない。
  2. 読み取りと書き込みはアトミック操作になります。
  3. ロックは、複数の実行パイプライン(論理コアなど)がある状況を処理できます。

「パフォーマンスの低下」を犠牲にして、「より良いロック」(プロセス間、スレッド間、マルチプロセッサ、シングルプロセッサなど)を提供するさまざまなsynchronizationメカニズムがあるため、常にロックメカニズムを選択する必要があります。あなたの状況にちょうど十分です。

15
Vikhram

関連する行を見てみましょう。

std::lock_guard<std::mutex> guard(myMutex);

lock_guardglobal mutex myMutexを参照していることに注意してください。つまり、3つのスレッドすべてに対して同じミューテックスです。 lock_guardが行うことは、基本的に次のとおりです。

  • 構築時に、myMutexをロックし、それへの参照を保持します。
  • 破壊されると(つまり、ガードのスコープが離れると)、myMutexのロックが解除されます。

ミューテックスは常に同じものであり、スコープとは関係ありません。 lock_guardのポイントは、ミューテックスのロックとロック解除を簡単にすることです。たとえば、手動でlock/unlockを実行したが、関数が途中で例外をスローした場合、unlockステートメントに到達することはありません。そのため、手動でyoを行うと、ミューテックスがalwaysロック解除されていることを確認する必要があります。一方、lock_guardオブジェクトは、関数の終了方法に関係なく、関数が終了するたびに自動的に破棄されます。

17
mindriot

これがまさにロックの機能です。スレッドがロックを取得する場合、コード内のどこにあるかに関係なく、別のスレッドがロックを保持している場合、スレッドは順番を待つ必要があります。スレッドがロックを解放すると、コード内のどこにあるかに関係なく、別のスレッドがそのロックを取得する場合があります。

ロックはコードではなくデータを保護します。保護されたデータにアクセスするすべてのコードがロックを保持している間に、同じデータにアクセスする可能性があるanyコードから他のスレッドを除外することにより、保護されたデータにアクセスします。

2
David Schwartz