web-dev-qa-db-ja.com

C ++ 11で独自のリーダー/ライターロックをどのように実装しますか?

リーダー/ライターロックで保護する必要がある一連のデータ構造があります。 boost :: shared_lockは知っていますが、std :: mutex、std :: condition_variable、std :: atomicのいずれかまたは両方を使用してカスタム実装を行いたいので、どのように機能するかを理解します(後で調整します) 。

各データ構造(移動可能ですが、コピー不可)は、ロックをカプセル化するCommonsと呼ばれるクラスから継承します。パブリックインターフェイスを次のようにしたいと思います。

class Commons {
public:
    void read_lock();
    bool try_read_lock();
    void read_unlock();

    void write_lock();
    bool try_write_lock();
    void write_unlock();
};

...そのため、一部の人がそれを公に継承できます。

class DataStructure : public Commons {};

科学的なコードを書いているので、一般にデータの競合を回避できます。このロックは、おそらく私がおそらく後で行う間違いに対する保護手段です。したがって、私の優先順位は低い読み取りオーバーヘッドなので、正しく実行されているプログラムをあまり妨げません。各スレッドは、おそらく独自のCPUコアで実行されます。

リーダー/ライターのロックを見せてください(擬似コードは問題ありません)?私が今持っているのは、作家の飢vを防ぐ変種であるはずです。これまでの私の主な問題は、read_lockが実際にリーダーカウントをインクリメントしても安全かどうかをチェックする間のread_lockのギャップでした。

void Commons::write_lock() {
    write_mutex.lock();
    reading_mode.store(false);
    while(readers.load() > 0) {}
}

void Commons::try_read_lock() {
    if(reading_mode.load()) {
        //if another thread calls write_lock here, bad things can happen
        ++readers; 
        return true;
    } else return false;
}

私はマルチスレッドに慣れていないので、本当に理解したいと思っています。よろしくお願いします!

37
jack

Mutexと条件変数を使用した単純なリーダー/ライターロックの擬似コードを次に示します。 mutex APIは一目瞭然です。条件変数には、(原子的に!)ミューテックスをドロップし、条件が通知されるのを待つメンバwait(Mutex&)があると想定されます。状態は、ウェイクアップするonesignal()、またはすべてのウェイターをウェイクアップするsignal_all()で通知されます。

read_lock() {
  mutex.lock();
  while (writer)
    unlocked.wait(mutex);
  readers++;
  mutex.unlock();
}

read_unlock() {
  mutex.lock();
  readers--;
  if (readers == 0)
    unlocked.signal_all();
  mutex.unlock();
}

write_lock() {
  mutex.lock();
  while (writer || (readers > 0))
    unlocked.wait(mutex);
  writer = true;
  mutex.unlock();
}

write_unlock() {
  mutex.lock();
  writer = false;
  unlocked.signal_all();
  mutex.unlock();
}

ただし、この実装にはいくつかの欠点があります。

ロックが使用可能になるたびにすべてのウェイターを起こします

ほとんどのウェイターが書き込みロックを待機している場合、これは無駄です。ほとんどのウェイターは結局ロックの取得に失敗し、待機を再開します。単純にsignal()を使用しても機能しません。読み取りロックのロック解除を待っているすべてのユーザーをdoウェイクアップしたいからです。したがって、それを修正するには、読みやすさと書き込み可能性のために別々の条件変数が必要です。

公平ではありません。読者は作家を飢えさせる

保留中の読み取りおよび書き込みロックの数を追跡し、そこで保留中の書き込みロックが発生すると読み取りロックの取得を停止するか(読者を飢えさせます!)、すべてのリーダーまたはライターをランダムに起動することで修正できます別の条件変数を使用します。上記のセクションを参照してください)。

ロックは、要求された順序で処理されません

これを保証するには、実際の待機キューが必要です。あなたは例えばウェイターごとに1つの条件変数を作成し、ロックを解除した後、両方のキューの先頭ですべてのリーダーまたは単一のライターにシグナルを送ります。

純粋な読み取りワークロードでも、ミューテックスによる競合が発生します

これは修正が難しいです。 1つの方法は、アトミック命令を使用して読み取りまたは書き込みロックを取得することです(通常、比較と交換)。ロックが取得されたために取得に失敗した場合は、ミューテックスにフォールバックする必要があります。ただし、それを正しく行うのは非常に困難です。加えて、まだ競合があります-アトミックな命令は、特に多くのコアを備えたマシンでは、決して無料ではありません。

結論

同期プリミティブを正しく実装するのはhardです。効率的で公平な同期プリミティブの実装は、evenharderです。そして、それはほとんど報われません。 Linux上のpthread、たとえばフューテックスとアトミック命令の組み合わせを使用するリーダー/ライターロックが含まれているため、おそらく数日間の作業で思いつくものよりも優れています。

45
fgp

このクラスを確認

//
// Multi-reader Single-writer concurrency base class for Win32
//
// (c) 1999-2003 by Glenn Slayden ([email protected])
//
//


#include "windows.h"

class MultiReaderSingleWriter
{
private:
    CRITICAL_SECTION m_csWrite;
    CRITICAL_SECTION m_csReaderCount;
    long m_cReaders;
    HANDLE m_hevReadersCleared;

public:
    MultiReaderSingleWriter()
    {
        m_cReaders = 0;
        InitializeCriticalSection(&m_csWrite);
        InitializeCriticalSection(&m_csReaderCount);
        m_hevReadersCleared = CreateEvent(NULL,TRUE,TRUE,NULL);
    }

    ~MultiReaderSingleWriter()
    {
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
        CloseHandle(m_hevReadersCleared);
        DeleteCriticalSection(&m_csWrite);
        DeleteCriticalSection(&m_csReaderCount);
    }


    void EnterReader(void)
    {
        EnterCriticalSection(&m_csWrite);
        EnterCriticalSection(&m_csReaderCount);
        if (++m_cReaders == 1)
            ResetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
        LeaveCriticalSection(&m_csWrite);
    }

    void LeaveReader(void)
    {
        EnterCriticalSection(&m_csReaderCount);
        if (--m_cReaders == 0)
            SetEvent(m_hevReadersCleared);
        LeaveCriticalSection(&m_csReaderCount);
    }

    void EnterWriter(void)
    {
        EnterCriticalSection(&m_csWrite);
        WaitForSingleObject(m_hevReadersCleared,INFINITE);
    }

    void LeaveWriter(void)
    {
        LeaveCriticalSection(&m_csWrite);
    }
};

試してみる機会はありませんでしたが、コードは問題ないようです。

6
Khachatur