Безопасность потока iostream, должна быть заблокирована отдельно и должна быть заблокирована отдельно?

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

Изменить пояснение: я понимаю, что cout и cerr являются "Thread Safe" в С++ 11. Мой вопрос заключается в том, может ли запись в cout и запись в cerr разными потоками одновременно вмешиваться друг в друга (что приводит к перемежаемому вводу и тому подобное) в том виде, в котором две записи могут выполняться cout.

Ответ 1

Если вы выполните эту функцию:

void f() {
    std::cout << "Hello, " << "world!\n";
}

из нескольких потоков вы получите более или менее случайное чередование двух строк "Hello, " и "world\n". Это потому, что есть два вызова функций, как если бы вы написали код следующим образом:

void f() {
    std::cout << "Hello, ";
    std::cout << "world!\n";
}

Чтобы предотвратить чередование, вы должны добавить блокировку:

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

Таким образом, проблема с чередованием < не имеет cout. Это о коде, который его использует: есть два отдельных вызова функций, вставляющих текст, поэтому, если вы не предотвратите одновременное выполнение нескольких потоков одним и тем же кодом, существует возможность переключения потоков между вызовами функций, что и дает вам чередование.

Обратите внимание, что мьютекс не поддерживает предотвращение потоков. В предыдущем фрагменте кода он предотвращает выполнение содержимого f() одновременно из двух потоков; один из потоков должен ждать, пока другой не закончит.

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

std::mutex mtx;
void f() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cout << "Hello, " << "world!\n";
}

void g() {
    std::lock_guard<std::mutex> lock(mtx);
    std::cerr << "Hello, " << "world!\n";
}

Ответ 2

В С++ 11, в отличие от С++ 03, вставка и извлечение из глобальных объектов потока (cout, cin, cerr и clog) являются потокобезопасными. Нет необходимости предоставлять ручную синхронизацию. Однако возможно, что символы, вставленные разными потоками, будут чередоваться непредсказуемо во время вывода; аналогично, когда несколько потоков считываются со стандартного ввода, непредсказуемо, какой поток будет читать этот токен.

Потоковая безопасность объектов глобального потока активна по умолчанию, но ее можно отключить, вызвав функцию-член sync_with_stdio объекта потока и передав false в качестве аргумента. В этом случае вам придется вручную обрабатывать синхронизацию.

Ответ 3

Это может быть небезопасным для одновременной записи в cout и cerr! Это зависит от того, какой cout привязан к cerr или нет. См. std:: ios:: tie.

"Связанный поток - это объект потока вывода, который очищается до каждая операция ввода/вывода в этом объекте потока."

Это означает, что cout.flush() может быть вызван непреднамеренно потоком, который пишет cerr. Я потратил некоторое время на то, чтобы понять, что это было причиной случайного отсутствия окончаний строк в выходе cout в одном из моих проектов: (

С С++ 98 cout не следует привязывать к cerr. Но, несмотря на стандарт, он привязан при использовании MSVC 2008 (мой опыт). При использовании следующего кода все работает хорошо.

std::ostream *cerr_tied_to = cerr.tie();
if (cerr_tied_to) {
    if (cerr_tied_to == &cout) {
        cerr << "DBG: cerr is tied to cout ! -- untying ..." << endl;
        cerr.tie(0);
    }
}

Смотрите также: почему cerr очищает буфер cout

Ответ 4

Здесь уже есть несколько ответов. Я обобщу и рассмотрю взаимодействия между ними.

Как правило,

std::cout и std::cerr часто будут передаваться в один поток текста, поэтому их совместное использование приводит к наиболее полезной программе.

Если вы игнорируете проблему, cout и cerr по умолчанию имеют псевдоним своих stdio аналогов, которые являются потокобезопасными как в POSIX, до стандартные функции ввода-вывода (С++ 14 §27.4.1/4, более сильная гарантия, чем C). Если вы придерживаетесь этого выбора функций, вы получаете мусорный ввод-вывод, но не undefined поведение (это то, что юрист языка может ассоциировать с "безопасностью потоков", независимо от полезности).

Обратите внимание, что хотя стандартные форматированные функции ввода-вывода (такие как номера чтения и записи) являются потокобезопасными, манипуляторы могут изменять формат (например, std::hex для шестнадцатеричного или std::setw для ограничения входной строки размер). Таким образом, нельзя вообще предположить, что опускание замков безопасно вообще.

Если вы решите заблокировать их отдельно, все будет сложнее.

Отдельная блокировка

Для производительности конфликт блокировок может быть уменьшен путем блокировки cout и cerr отдельно. Они отдельно буферизованы (или небуферированы), и они могут скрываться для разделения файлов.

По умолчанию cerr выполняет сброс cout перед каждой операцией, потому что они "привязаны". Это победит как разделение, так и блокировку, поэтому не забудьте позвонить cerr.tie( nullptr ), прежде чем что-либо делать с ним. (То же самое относится к cin, но не к clog.)

Развязка от stdio

В стандарте говорится, что операции с cout и cerr не содержат рас, но это не может быть именно то, что это означает. Объекты потока не являются особыми; их базовые буферы streambuf.

Кроме того, вызов std::ios_base::sync_with_stdio предназначен для удаления специальных аспектов стандартных потоков - для обеспечения их буферизации, как и другие потоки. Хотя стандарт не упоминает о влиянии sync_with_stdio на расы данных, быстрый поиск в классах libstdС++ и libС++ (GCC и Clang) std::basic_streambuf показывает, что они не используют атомные переменные, поэтому они могут создавать условия гонки, когда используется для буферизации. (С другой стороны, libС++ sync_with_stdio эффективно ничего не делает, поэтому не имеет значения, если вы его назовете.)

Если вам нужна дополнительная производительность независимо от блокировки, sync_with_stdio(false) - хорошая идея. Однако после этого необходимо блокирование наряду с cerr.tie( nullptr ), если блокировки являются отдельными.

Ответ 5

Это может быть полезно;)

inline static void log(std::string const &format, ...) {
    static std::mutex locker;

    std::lock_guard<std::mutex>(locker);

    va_list list;
    va_start(list, format);
    vfprintf(stderr, format.c_str(), list);
    va_end(list);
}

Ответ 6

Я использую что-то вроде этого:

// Wrap a mutex around cerr so multiple threads don't overlap output
// USAGE:
//     LockedLog() << a << b << c;
// 
class LockedLog {
public:
    LockedLog() { m_mutex.lock(); }
    ~LockedLog() { *m_ostr << std::endl; m_mutex.unlock(); }

    template <class T>
    LockedLog &operator << (const T &msg)
    {
        *m_ostr << msg;
        return *this;
    }

private:
    static std::ostream *m_ostr;
    static std::mutex m_mutex;
};

std::mutex LockedLog::m_mutex;
std::ostream* LockedLog::m_ostr = &std::cerr;