Как бы вы реализовали свой собственный механизм чтения/записи в С++ 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 {};

Я пишу научный код и, как правило, избегаю гонок данных; этот замок в основном является защитой от ошибок, которые я, вероятно, сделаю позже. Таким образом, мой приоритет - низкие накладные расходы, поэтому я не слишком затрудняю работу программы с корректным запуском. Каждый поток, вероятно, будет работать на собственном ядре процессора.

Не могли бы вы показать мне (псевдокод в порядке) блокировки для чтения/записи? Теперь я должен быть вариантом, который предотвращает голод писателя. Моя основная проблема до сих пор была пробелом в read_lock между проверкой того, безопасно ли чтение для фактического увеличения количества считывателей, после чего write_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;
}

Я немного новичок в многопоточности, и мне очень хотелось бы это понять. Заранее благодарим за помощь!

Ответ 1

Здесь псевдокод для простой блокировки чтения/записи с использованием мьютекса и переменной условия. API-интерфейс мьютекса должен быть понятным. Предполагается, что переменные условия имеют член wait(Mutex&), который (атомарно!) Опускает мьютекс и ожидает, что условие будет сигнализировано. Условие сигнализируется либо с помощью signal(), который пробуждает одного официанта, либо 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() не работает, потому что вы хотите пробудить всех, ожидающих разблокировки блокировки чтения. Поэтому, чтобы исправить это, вам нужны отдельные переменные условия для удобства чтения и записи.

Нет справедливости. Читатели голодают авторы

Вы можете исправить это, отслеживая количество незавершенных блокировок чтения и записи и либо перестать получать блокировки чтения, когда есть незавершенные блокировки записи (хотя вы будете голодать читателям!), либо случайно пробуждаете либо всех читателей, либо один (если вы используете отдельную переменную условия, см. раздел выше).

Замки не выдаются в том порядке, в котором они запрошены

Чтобы гарантировать это, вам понадобится реальная очередь ожидания. Вы можете, например, создайте одну переменную условия для каждого официанта и сообщите всем читателям или одному автору, как во главе очереди, после освобождения блокировки.

Даже чистые рабочие нагрузки чтения вызывают конфликт из-за мьютекса

Это трудно исправить. Один из способов - использовать атомарные инструкции для получения блокировок чтения или записи (как правило, для сравнения и обмена). Если сбой сбора данных происходит из-за блокировки, вам придется вернуться к мьютексу. Однако делать это правильно довольно сложно. Плюс, до сих пор остается спор - атомарные инструкции далеки от бесплатных, особенно на машинах с большим количеством ядер.

Заключение

Реализация примитивов синхронизации правильно жестко. Реализация эффективных и справедливых примитивов синхронизации даже сложнее. И это почти никогда не окупается. pthreads на linux, например. содержит блокировку чтения/записи, которая использует комбинацию futexes и атомных инструкций и, таким образом, вероятно, превосходит все, что вы можете придумать за несколько дней работы.

Ответ 2

Отметьте этот класс:

//
// 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);
    }
};

У меня не было возможности попробовать, но код выглядит нормально.

Ответ 3

Вы можете реализовать блокировку Readers-Writers, следуя точному алгоритму Википедии здесь (я написал):

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

int g_sharedData = 0;
int g_readersWaiting = 0;
std::mutex mu;
bool g_writerWaiting = false;
std::condition_variable cond;

void reader(int i)
{
    std::unique_lock<std::mutex> lg{mu};
    while(g_writerWaiting)
        cond.wait(lg);
    ++g_readersWaiting;
    // reading
    std::cout << "\n reader #" << i << " is reading data = " << g_sharedData << '\n';
    // end reading
    --g_readersWaiting;
    while(g_readersWaiting > 0)
        cond.wait(lg);
    cond.notify_one();
}

void writer(int i)
{
    std::unique_lock<std::mutex> lg{mu};
    while(g_writerWaiting)
        cond.wait(lg);
    // writing
    std::cout << "\n writer #" << i << " is writing\n";
    g_sharedData += i * 10;
    // end writing
    g_writerWaiting = true;
    while(g_readersWaiting > 0)
        cond.wait(lg);
    g_writerWaiting = false;
    cond.notify_all();
}//lg.unlock()


int main()
{
    std::thread reader1{reader, 1};
    std::thread reader2{reader, 2};
    std::thread reader3{reader, 3};
    std::thread reader4{reader, 4};
    std::thread writer1{writer, 1};
    std::thread writer2{writer, 2};
    std::thread writer3{writer, 3};
    std::thread writer4{reader, 4};

    reader1.join();
    reader2.join(); 
    reader3.join();
    reader4.join();
    writer1.join();
    writer2.join();
    writer3.join();
    writer4.join();

    return(0);
}