Техника безопасности.Я что-то пропустил? - PullRequest
9 голосов
/ 02 марта 2012

Я работаю с многопоточным кодом для игрового проекта, и немного устал от сортировки stdout vomit, созданного двумя потоками, использующими cout для отладки сообщений одновременно.Я провел некоторое исследование и смотрел на стену в течение часа или двух, прежде чем придумал «что-то».Следующий код использует SFML для хранения времени и потоков.Мьютексы SFML - это просто обернутые критические секции в окнах.

Заголовок:

#include <SFML\System.hpp>
#include <iostream>

class OutputStreamHack
{
    public:
    OutputStreamHack();
    ~OutputStreamHack();

    ostream& outputHijack(ostream &os);

    private:
    sf::Clock myRunTime;
    sf::Mutex myMutex;
};

static OutputStream OUTHACK;

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue);

Реализация:

#include <SFML\System.hpp>
#include <iostream>

#include "OutputStreamHack.h"

using namespace std;

OutputStreamHack::OutputStreamHack()
{
    myMutex.Unlock();
    myRunTime.Reset();
}

OutputStreamHack::~OutputStreamHack()
{
    myMutex.Unlock();
    myRunTime.Reset();
}

ostream& OutputStreamHack::outputHijack(ostream &os)
{

    sf::Lock lock(myMutex);
    os<<"<"<<myRunTime.GetElapsedTime()<<","<<GetCurrentThreadId()<<"> "<<flush;
    return os;
}

ostream& operator<<(ostream& os, const OutputStreamHack& inputValue)
{
    OUTHACK.outputHijack(os);
    return os;
}

Использование:

cout<<OUTHACK<<val1<<val2<<val3....<<endl;

ОКТаким образом, этот способ работает через перегруженный оператор вставки, который обеспечивает безопасность потока, блокируя итератор в статическом объекте, а затем сбрасывая буфер.Если я правильно понимаю этот процесс (я в основном программист-самоучка), cout обрабатывает элементы своей цепочки вставки от конца к началу, передавая переменную ostream по цепочке для каждого элемента, который должен быть добавлен к потоку.Как только он достигает элемента OUTHACK, вызывается перегруженный оператор, мьютекс блокируется и поток сбрасывается.

Я добавил некоторую информацию об отладке идентификатора времени / потока в вывод для проверки.Пока что мое тестирование показывает, что этот метод работает.У меня есть несколько потоков, набирающих cout с несколькими аргументами, и все идет в правильном порядке.

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

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

Спасибо!

1 Ответ

21 голосов
/ 02 марта 2012

То, что не является потокобезопасным, здесь не cout само по себеЭто вызывает два вызова функции в последовательности.std::cout << a << b примерно эквивалентно вызову operator<<(std::cout, a), за которым следует operator<<(std::cout, b).Последовательный вызов двух функций не гарантирует, что они будут выполняться атомарным образом.

Таким образом, мьютекс защищает только вывод времени и идентификатора потока.Вполне возможно, что между вставками OUTHACK и val1 будет пробираться другая нить, потому что блокировка больше не удерживается после вставки OUTHACK.

Вы можете иметь operator<< для вашего OutputStreamHack вернуть по значению объект, который разблокируется в деструкторе.Поскольку временные значения сохраняются до конца каждого полного выражения, код будет удерживать блокировку «до точки с запятой».Однако, поскольку могут быть задействованы копии, это может быть проблематично без конструктора перемещения (или пользовательского конструктора копирования в C ++ 03, аналогично auto_ptr gasp ).

Другим вариантом является использование существующей поточной безопасности cout (гарантируется языком в C ++ 11, но многие реализации ранее были поточнобезопасными).Создайте объект, который перетекет все в элемент std::stringstream, а затем запишите все сразу, когда он будет уничтожен.

class FullExpressionAccumulator {
public:
    explicit FullExpressionAccumulator(std::ostream& os) : os(os) {}
    ~FullExpressionAccumulator() {
        os << ss.rdbuf() << std::flush; // write the whole shebang in one go
    }

    template <typename T>
    FullExpressionAccumulator& operator<<(T const& t) {
        ss << t; // accumulate into a non-shared stringstream, no threading issues
        return *this;
    }

private:
    std::ostream& os;
    std::stringstream ss;

    // stringstream is not copyable, so copies are already forbidden
};

// using a temporary instead of returning one from a function avoids any issues with copies
FullExpressionAccumulator(std::cout) << val1 << val2 << val3;
...