STL и многопоточность - PullRequest
       3

STL и многопоточность

3 голосов
/ 13 августа 2011

Мне известно, что мне нужно использовать мьютекс, когда я выполняю операции над одним контейнером STL внутри нескольких потоков. Однако я хочу знать, есть ли какие-либо исключения из этого правила. Пожалуйста, рассмотрите упрощенный сценарий, который я пытаюсь реализовать.

У меня есть несколько потоков, добавляющих элементы в контейнер, и операция окружена блокировкой / разблокировкой мьютекса. Затем потоки уведомляют каким-либо образом (например, используя eventfd на linux) отдельный поток, предназначенный для элементов отправки в этом контейнере. Я хочу получить доступ к первому элементу в контейнере без использования мьютекса. Пример кода на основе deque, но обратите внимание, что я могу использовать любой контейнер с возможностью очереди:

std::mutex     locker;
std:deque<int> int_queue;
int            fd; // eventfd
eventfd_t      buffer;
bool           some_condition;

Тема 1, 2, 3 и т. Д.

locker.lock ();
int_queue.push_back (1);
locker.unlock ();

eventfd_write (fd, 1);

Тема, посвященная элементам отправки:

while (true)
{
    bool some_condition (true);

    locker.lock ();
    if (int_quque.empty () == false)
    {
        locker.unlock ();
    }
    else
    {
        locker.unlock ();
        eventfd_read (fd, &buffer);
    }

    while (some_condition)
    {
        int& data (int_queue.front ());

        some_condition = some_operation (data); // [1]
    }

    locker.lock ();
    int_queue.pop ();
    locker.unlock ();
}

[1] Я буду делать some_operation () для элемента signle много раз, поэтому здесь я хочу избежать блокировки мьютекса. Это дорого.

Я хочу знать, может ли этот код вызвать какие-либо проблемы с синхронизацией или что-то в этом роде.

Ответы [ 5 ]

6 голосов
/ 13 августа 2011

Что вам нужно, это эталонная стабильность . То есть Вы можете использовать контейнеры таким образом, если ссылка на первый элемент не является недействительной, когда контейнер является push_back'd. И даже тогда вы захотите получить ссылку на передний элемент под замком.

Я более знаком с std::condition_variable для уведомления о событии, поэтому я буду использовать это:

#include <mutex>
#include <condition_variable>
#include <deque>

std::mutex              locker;
std::deque<int>         int_queue;
std::condition_variable cv;

void thread_1_2_3()
{
    // use lock_guard instead of explicit lock/unlock
    //    for exception safety
    std::lock_guard<std::mutex> lk(locker);
    int_queue_.push_back(1);
    cv.notify_one();
}

void dispatch()
{
    while (true)
    {
        bool some_condition = true;
        std::unique_lock<std::mutex> lk(locker);
        while (int_queue.empty())
            cv.wait(lk);
        // get reference to front under lock
        int& data = int_queue.front();
        lk.unlock();
        // now use the reference without worry
        while (some_condition)
            some_condition = some_operation(data);
        lk.lock();
        int_queue.pop_front();
    }
}

23.3.3.4 [deque.modifiers] говорит об этом push_back:

Вставка на любом конце deque делает недействительными все итераторы в deque, но не влияет на действительность ссылок на элементы оформления.

Это ключ к тому, чтобы позволить вам привязаться к этой ссылке за пределами замка. Если thread_1_2_3 начнет вставлять или стирать посередине, вы больше не сможете удерживать эту ссылку.

Вы не можете использовать vector таким образом. Но вы могли бы использовать list таким образом. Проверяйте каждый контейнер, который вы хотите использовать таким образом, для справочной стабильности.

3 голосов
/ 13 августа 2011

Я не могу видеть ваш вопрос или ваш код, но в целом контейнеры в стандартной библиотеке C ++ дают вам слабую гарантию того, что параллельный доступ к различным элементам является поточно-ориентированным. Обязательно поймите последствия и ограничения этого, хотя: если у вас есть контейнер с произвольным доступом или итераторы для элементов, и вы используете их только для чтения или изменения значения элемента, то до тех пор, пока вы делаете это в различные элементы, результат должен быть четко определен. Что не в порядке, так это изменение самого контейнера, поэтому любые операции стирания или вставки должны быть сериализованы (например, путем блокирования доступа ко всему контейнеру), и убедитесь, что вы понимаете итератор вашего контейнера и ссылаетесь на правила аннулирования, когда вы делаете это.

Для отдельных контейнеров вы можете сказать немного больше - например, вставить / удалить в древовидном контейнере, а вставить / удалить в середине контейнера произвольного доступа почти наверняка требуется глобальная блокировка. В векторе / деке вам нужно будет повторно получить итераторы. В списке вы можете избежать одновременного выполнения вставок в разных местах.

Любые глобальные операции, такие как size() и empty(), также должны быть сериализованы.

0 голосов
/ 14 августа 2011

Если вы хотите заблокировать свободную очередь, я также рекомендую вам посмотреть http://drdobbs.com/cpp/210604448?pgno=2

0 голосов
/ 14 августа 2011

Почему вы хотите это сделать? Почему потребительский поток не извлекает объект из блокировки, а затем обрабатывает его вне диапазона?

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

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

0 голосов
/ 13 августа 2011

Для данного конкретного примера это , а не безопасно

int& data (int_queue.front ());

Вы берете ссылку на первый элемент, он может быть перемещен другим потоком, добавляя элемент в очередь, вынуждая его перераспределять (запросы обычно реализуются как массивы с циклическим переносом). Если вы копируете значение, а не берете ссылку, в зависимости от реализации вам может сойти с рук. Если вы хотите сделать это, std :: deque не поставляется со стандартными «исключениями» из этого правила. Конечно, можно написать структуру данных, похожую на deque, где это было бы безопасно, но не гарантируется, что deque будет написан как (и так будет нелегко) как.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...