Выбрать главу

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

4.1.2. Потокобезопасная очередь на базе условных переменных

Приступая к проектированию обобщенной очереди, стоит потратить некоторое время на обдумывание того, какие понадобятся операции. Именно так мы подходили к разработке потокобезопасного стека в разделе 3.2.3. Возьмем в качестве образца адаптер контейнера std::queue<> из стандартной библиотеки С++, интерфейс которого показан в листинге ниже.

Листинг 4.2. Интерфейс класса std::queue

template <class T, class Container = std::deque<T>>

class queue {

public:

 explicit queue(const Container&);

 explicit queue(Container&& = Container());

 template <class Alloc> explicit queue(const Alloc&);

 template <class Alloc> queue(const Container&, const Alloc&);

 template <class Alloc> queue(Container&&, const Alloc&);

 template <class Alloc> queue(queue&&, const Alloc&);

 void swap(queue& q);

 bool empty() const;

 size_type size() const;

 T& front();

 const T& front() const;

 T& back();

 const T& back() const;

 void push(const T& x);

 void push(T&& x);

 void pop();

 template <class... Args> void emplace(Args&&... args);

};

Если не обращать внимания на конструирование, присваивание и обмен, то останется три группы операций: опрос состояния очереди в целом (empty() и size()), опрос элементов очереди (front() и back()) модификация очереди (push(), pop() и emplace()). Ситуация аналогична той, что мы видели в разделе 3.2.3 для стека, поэтому возникают те же — внутренне присущие интерфейсу — проблемы с гонкой. Следовательно, front() и pop() необходимо объединить в одной функции — точно так же, как мы постудили с top() и pop() в случае стека. Но в коде в листинге 4.1 есть дополнительный нюанс: если очередь используется для передачи данных между потоками, то поток-получатель часто будет ожидать поступления данных. Поэтому включим два варианта pop(): try_pop() пытается извлечь значение из очереди, но сразу возвращает управление (с указанием ошибки), если в очереди ничего не было, a wait_and_pop() ждет, когда появятся данные. Взяв за образец сигнатуры функций из примера стека, представим интерфейс в следующем виде:

Листинг 4.3. Интерфейс класса threadsafe_queue

#include <memory>

template<typename T>

class threadsafe_queue {

public:

 threadsafe_queue();

 threadsafe_queue(const threadsafe_queue&);

 threadsafe_queue& operator=(

  const threadsafe_queue&) = delete; ←┐ Для простоты

 void push(T new_value);              │ запрещаем присваивание

 bool try_pop(T& value);       ← (1)

 std::shared_ptr<T> try_pop(); ← (2)

 void wait_and_pop(T& value);

 std::shared_ptr<T> wait_and_pop();

 bool empty() const;

};

Как и в случае стека, мы для простоты уменьшили число конструкторов и запретили присваивание. И, как и раньше, предлагаем по два варианта функций try_pop() и wait_for_pop(). Первый перегруженный вариант try_pop() (1) сохраняет извлеченное значение в переданной по ссылке переменной, а возвращаемое значение использует для индикации ошибки: оно равно true, если значение получено, и false — в противном случае (см. раздел А.2). Во втором перегруженном варианте (2) так поступить нельзя, потому что возвращаемое значение — это данные, извлеченные из очереди. Однако же можно возвращать указатель NULL, если в очереди ничего не оказалось.