Cout синхронизирован / потокобезопасен?

112

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

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

Зависит ли этот поставщик? Что делает gcc?


Важно : дайте ссылку на свой ответ, если вы скажете «да», поскольку мне нужно какое-то доказательство этого.

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

edA-qa mort-ora-y
источник
2
Это зависит от поставщика. C ++ (до C ++ 0x) не имеет понятия о нескольких потоках.
Sven
2
А как насчет c ++ 0x? Он определяет модель памяти и то, что такое поток, так что, возможно, эти вещи просочились в вывод?
rubenvb
2
Есть ли какие-нибудь поставщики, которые делают его поточно-ориентированным?
edA-qa mort-ora-y
У кого-нибудь есть ссылка на самый последний предлагаемый стандарт C ++ 2011?
edA-qa mort-ora-y
4
В каком-то смысле это то место, где printfсияет, поскольку весь вывод записывается stdoutодним выстрелом; при использовании std::coutкаждого звена цепочки выражений будет выводиться отдельно в stdout; между ними может быть какой-то другой поток записи, stdoutиз-за которого порядок конечного вывода нарушается.
legends2k

Ответы:

106

Стандарт C ++ 03 об этом ничего не говорит. Если у вас нет гарантий относительно потоковой безопасности чего-либо, вы должны относиться к этому как к небезопасному для потоков.

Особый интерес здесь представляет то, что coutбуферизуется. Даже если вызовы write(или что-то еще, что обеспечивает этот эффект в этой конкретной реализации) гарантированно являются взаимоисключающими, буфер может совместно использоваться разными потоками. Это быстро приведет к повреждению внутреннего состояния потока.

И даже если доступ к буферу гарантированно будет потокобезопасным, что, по вашему мнению, произойдет в этом коде?

// in one thread
cout << "The operation took " << result << " seconds.";

// in another thread
cout << "Hello world! Hello " << name << "!";

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

В C ++ 11 есть некоторые гарантии. FDIS говорит следующее в §27.4.1 [iostream.objects.overview]:

Одновременный доступ к функциям ввода (§27.7.2.1) и вывода (§27.7.3.1) синхронизированного (§27.5.3.4) стандартного объекта iostream, функций ввода и вывода (§27.7.3.1) или стандартного потока C несколькими потоками не должен приводить к гонке данных (§ 1.10). [Примечание: пользователи должны по-прежнему синхронизировать одновременное использование этих объектов и потоков несколькими потоками, если они хотят избежать чередования символов. - конец примечания]

Таким образом, вы не получите поврежденные потоки, но вам все равно придется синхронизировать их вручную, если вы не хотите, чтобы вывод был мусором.

Р. Мартиньо Фернандес
источник
2
Технически верно для C ++ 98 / C ++ 03, но я думаю, что все это знают. Но это не дает ответа на два интересных вопроса: как насчет C ++ 0x? Что на самом деле делают типичные реализации ?
Nemo
1
@ edA-qa mort-ora-y: Нет, ты ошибаешься. C ++ 11 четко определяет, что стандартные объекты потока могут быть синхронизированы и сохранять четко определенное поведение, а не то, что они есть по умолчанию.
ildjarn
12
@ildjarn - Нет, @ edA-qa mort-ora-y правильно. Пока cout.sync_with_stdio()это правда, использование coutдля вывода символов из нескольких потоков без дополнительной синхронизации четко определено, но только на уровне отдельных байтов. Таким образом, cout << "ab";и cout << "cd"выполняемые в разных потоках могут выводить acdb, например, но не могут вызывать Undefined Behavior.
JohannesD
4
@JohannesD: Мы согласны с этим - он синхронизирован с базовым C API. Я хочу сказать, что он не "синхронизируется" полезным способом, т.е. все равно требуется ручная синхронизация, если им не нужны данные мусора.
ildjarn
2
@ildjarn, я в порядке с данными мусора, я это понимаю. Меня просто интересует состояние гонки данных, которое теперь кажется ясным.
edA-qa mort-ora-y
16

Это большой вопрос.

Во-первых, C ++ 98 / C ++ 03 не имеет понятия «поток». Так что в том мире вопрос бессмысленный.

А как насчет C ++ 0x? См . Ответ Мартиньо (который, признаюсь, меня удивил).

Как насчет конкретных реализаций до C ++ 0x? Ну, например, вот исходный код для basic_streambuf<...>:sputcGCC 4.5.2 (заголовок "streambuf"):

 int_type
 sputc(char_type __c)
 {
   int_type __ret;
   if (__builtin_expect(this->pptr() < this->epptr(), true)) {
       *this->pptr() = __c;
        this->pbump(1);
        __ret = traits_type::to_int_type(__c);
      }
    else
        __ret = this->overflow(traits_type::to_int_type(__c));
    return __ret;
 }

Ясно, что это не приводит к блокировке. И ни то, ни другое xsputn. И это определенно тот тип streambuf, который использует cout.

Насколько я могу судить, libstdc ++ не блокирует ни одну из потоковых операций. И я бы не ожидал, что это будет медленно.

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

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

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

Резюме: «Я бы не стал». Создайте класс ведения журнала, который выполняет правильную блокировку, или перейдите на C ++ 0x.

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

Nemo
источник
1
Хороший ответ, но посмотрите на ответ Мартиньо, который показывает, что C ++ 11 действительно определяет синхронизацию для cout.
edA-qa mort-ora-y
7

Стандарт C ++ не определяет, является ли запись в потоки потокобезопасной, но обычно это не так.

www.techrepublic.com/article/use-stl-streams-for-easy-c-plus-plus-thread-safe-logging

а также: Являются ли стандартные потоки вывода в C ++ потокобезопасными (cout, cerr, clog)?

ОБНОВИТЬ

Пожалуйста, посмотрите ответ @Martinho Fernandes, чтобы узнать, что говорит об этом новый стандарт C ++ 11.

фоксис
источник
3
Я думаю, поскольку C ++ 11 теперь является стандартом, этот ответ сейчас на самом деле неверен.
edA-qa mort-ora-y
6

Как упоминается в других ответах, это определенно зависит от поставщика, поскольку стандарт C ++ не упоминает о потоковой передаче (это изменяется в C ++ 0x).

GCC не дает много обещаний относительно безопасности потоков и ввода-вывода. Но документация по обещаниям находится здесь:

Ключевым моментом, вероятно, является:

Тип __basic_file - это просто набор небольших оберток вокруг слоя C stdio (снова см. Ссылку в разделе «Структура»). Мы не блокируемся, а просто переходим к вызовам fopen, fwrite и так далее.

Итак, для версии 3.0 на вопрос «безопасна ли многопоточность для ввода-вывода» нужно ответить: «безопасна ли библиотека C вашей платформы для операций ввода-вывода?» Некоторые из них по умолчанию, некоторые нет; многие предлагают несколько реализаций библиотеки C с различными компромиссами между безопасностью потоков и эффективностью. Вы, программист, всегда должны заботиться о нескольких потоках.

(Например, стандарт POSIX требует, чтобы операции C stdio FILE * были атомарными. POSIX-совместимые библиотеки C (например, в Solaris и GNU / Linux) имеют внутренний мьютекс для сериализации операций с FILE * s. Однако вам все равно понадобится чтобы не делать глупостей вроде вызова fclose (fs) в одном потоке с последующим доступом к fs в другом.)

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

Не знаю, изменилось ли что-нибудь в упомянутом таймфрейме 3.0.

Документацию по безопасности потоков MSVC для iostreamsможно найти здесь: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Один объект является потокобезопасным для чтения из нескольких потоков. Например, для объекта A безопасно читать A из потока 1 и из потока 2 одновременно.

Если один поток записывает в один объект, то все операции чтения и записи в этот объект в том же или других потоках должны быть защищены. Например, для объекта A, если поток 1 записывает в A, то потоку 2 необходимо запретить чтение или запись в A.

Безопасно читать и писать в один экземпляр типа, даже если другой поток читает или записывает в другой экземпляр того же типа. Например, для объектов A и B одного типа безопасно, если A записывается в потоке 1, а B читается в потоке 2.

...

Классы iostream

Классы iostream следуют тем же правилам, что и другие классы, за одним исключением. Запись в объект из нескольких потоков безопасна. Например, поток 1 может писать в cout одновременно с потоком 2. Однако это может привести к смешиванию выходных данных двух потоков.

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

Обратите внимание, что эта информация относится к самой последней версии MSVC (в настоящее время для VS 2010 / MSVC 10 / cl.exe16.x). Вы можете выбрать информацию для более старых версий MSVC, используя раскрывающийся список на странице (а для более старых версий информация отличается).

Майкл Берр
источник
1
«Я не знаю, изменилось ли что-нибудь в упомянутом таймфрейме 3.0». Это определенно сработало. В течение последних нескольких лет реализация потоков g ++ выполняла собственную буферизацию.
Nemo