O cout é sincronizado / thread-safe?

112

Em geral, presumo que os fluxos não estão sincronizados, cabe ao usuário fazer o bloqueio apropriado. No entanto, coisas como coutreceber tratamento especial na biblioteca padrão?

Ou seja, se vários threads estiverem gravando, couteles podem corromper o coutobjeto? Eu entendo que, mesmo se sincronizado, você ainda terá uma saída intercalada aleatoriamente, mas essa intercalação é garantida. Ou seja, é seguro usar a coutpartir de vários threads?

Este fornecedor é dependente? O que o gcc faz?


Importante : forneça algum tipo de referência para sua resposta se você disser "sim", pois preciso de algum tipo de prova disso.

Minha preocupação também não é com as chamadas de sistema subjacentes, elas estão bem, mas os fluxos adicionam uma camada de buffer no topo.

edA-qa mort-ora-y
fonte
2
Isso depende do fornecedor. C ++ (antes de C ++ 0x) não tem noção de vários threads.
Sven
2
E sobre c ++ 0x? Ele define um modelo de memória e o que é um thread, então talvez essas coisas tenham vazado na saída?
rubenvb
2
Há algum fornecedor que o torne thread-safe?
edA-qa mort-ora-y
Alguém tem um link para o padrão proposto C ++ 2011 mais recente?
edA-qa mort-ora-y
4
Em certo sentido, é aqui que printfbrilha, pois a saída completa é gravada de stdoutuma só vez; ao usar std::coutcada elo da cadeia de expressão seria enviado separadamente para stdout; entre eles pode haver algum outro thread de gravação stdoutdevido ao qual a ordem da saída final fica confusa.
legends2k

Respostas:

106

O padrão C ++ 03 não diz nada sobre isso. Quando você não tem garantias sobre a segurança de thread de algo, deve tratá-la como não segura de thread.

De particular interesse aqui é o fato de que couté armazenado em buffer. Mesmo se as chamadas para write(ou seja lá o que for que realize esse efeito naquela implementação particular) tenham garantia de serem mutuamente exclusivas, o buffer pode ser compartilhado por diferentes threads. Isso levará rapidamente à corrupção do estado interno do fluxo.

E mesmo que o acesso ao buffer seja seguro para thread, o que você acha que acontecerá neste código?

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

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

Você provavelmente deseja que cada linha aqui atue em exclusão mútua. Mas como uma implementação pode garantir isso?

No C ++ 11, temos algumas garantias. O FDIS diz o seguinte em §27.4.1 [iostream.objects.overview]:

O acesso simultâneo a funções de entrada formatada e não formatada (§27.7.2.1) e saída (§27.7.3.1) de um objeto iostream padrão sincronizado (§27.5.3.4) ou um fluxo C padrão por vários threads não deve resultar em uma corrida de dados (§ 1,10). [Nota: Os usuários ainda devem sincronizar o uso simultâneo desses objetos e fluxos por vários threads se desejam evitar caracteres intercalados. - nota final]

Portanto, você não obterá fluxos corrompidos, mas ainda precisará sincronizá-los manualmente se não quiser que a saída seja lixo.

R. Martinho Fernandes
fonte
2
Tecnicamente verdadeiro para C ++ 98 / C ++ 03, mas acho que todo mundo sabe disso. Mas isso não responde a duas perguntas interessantes: e o C ++ 0x? O que as implementações típicas realmente fazem ?
Nemo
1
@ edA-qa mort-ora-y: Não, você entendeu errado. C ++ 11 define claramente que os objetos de fluxo padrão podem ser sincronizados e reter um comportamento bem definido, não que sejam por padrão.
ildjarn de
12
@ildjarn - Não, @ edA-qa mort-ora-y está correto. Contanto que cout.sync_with_stdio()seja verdade, usar coutpara gerar caracteres de vários threads sem sincronização adicional está bem definido, mas apenas no nível de bytes individuais. Assim, cout << "ab";e cout << "cd"executado em threads diferentes acdbpode gerar saída , por exemplo, mas não pode causar comportamento indefinido.
JohannesD
4
@JohannesD: Estamos de acordo - é sincronizado com a API C subjacente. Meu ponto é que não é "sincronizado" de uma maneira útil, ou seja, ainda é necessário sincronização manual se não quiserem dados de lixo.
ildjarn de
2
@ildjarn, estou bem com os dados de lixo, isso eu entendo. Estou apenas interessado na condição de corrida de dados, que parece estar clara agora.
edA-qa mort-ora-y
16

Esta é uma grande pergunta.

Primeiro, C ++ 98 / C ++ 03 não tem o conceito de "thread". Portanto, naquele mundo, a pergunta não tem sentido.

E sobre C ++ 0x? Veja a resposta do Martinho (que admito que me surpreendeu).

Que tal implementações específicas pré-C ++ 0x? Bem, por exemplo, aqui está o código-fonte basic_streambuf<...>:sputcdo GCC 4.5.2 (cabeçalho "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;
 }

Claramente, isso não executa nenhum bloqueio. E nem mesmo xsputn. E esse é definitivamente o tipo de streambuf que cout usa.

Até onde eu posso dizer, libstdc ++ não executa nenhum bloqueio em qualquer uma das operações de fluxo. E eu não esperava nenhum, pois seria lento.

Portanto, com essa implementação, obviamente, é possível que a saída de duas threads se corrompa ( não apenas intercale).

Este código pode corromper a própria estrutura de dados? A resposta depende das possíveis interações dessas funções; por exemplo, o que acontece se uma thread tenta liberar o buffer enquanto outra tenta chamar xsputnou algo assim. Pode depender de como o compilador e a CPU decidem reordenar as cargas e armazenamentos de memória; seria necessária uma análise cuidadosa para ter certeza. Também depende do que sua CPU faz se dois threads tentam modificar o mesmo local simultaneamente.

Em outras palavras, mesmo que funcione bem em seu ambiente atual, ele pode falhar quando você atualiza seu tempo de execução, compilador ou CPU.

Resumo executivo: "Eu não faria". Crie uma classe de registro que faça o bloqueio adequado ou mude para C ++ 0x.

Como uma alternativa fraca, você pode definir cout como sem buffer. É provável (embora não garantido) que ignoraria toda a lógica relacionada ao buffer e chamaria writediretamente. Embora isso possa ser proibitivamente lento.

Nemo
fonte
1
Boa resposta, mas veja a resposta de Martinho que mostra que C ++ 11 define a sincronização para cout.
edA-qa mort-ora-y
7

O padrão C ++ não especifica se a gravação em fluxos é segura para thread, mas geralmente não é.

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

e também: Os fluxos de saída padrão em C ++ thread-safe (cout, cerr, clog)?

ATUALIZAR

Dê uma olhada na resposta de @Martinho Fernandes para saber o que o novo padrão C ++ 11 fala sobre isso.

foxis
fonte
3
Acho que, como C ++ 11 agora é o padrão, essa resposta está realmente errada agora.
edA-qa mort-ora-y
6

Como outras respostas mencionam, isso é definitivamente específico do fornecedor, já que o padrão C ++ não faz menção a threading (isso muda em C ++ 0x).

O GCC não faz muitas promessas sobre segurança de thread e E / S. Mas a documentação para o que ele promete está aqui:

o principal é provavelmente:

O tipo __basic_file é simplesmente uma coleção de pequenos invólucros em torno da camada C stdio (novamente, consulte o link em Estrutura). Não fazemos nenhum bloqueio, mas simplesmente passamos para chamadas para fopen, fwrite e assim por diante.

Portanto, para 3.0, a pergunta "é multithreading seguro para E / S" deve ser respondida com "a biblioteca C da sua plataforma é segura para E / S?" Alguns são por padrão, outros não; muitos oferecem várias implementações da biblioteca C com vários compromissos de segurança e eficiência de thread. Você, o programador, sempre é obrigado a tomar cuidado com vários threads.

(Por exemplo, o padrão POSIX requer que as operações C stdio FILE * sejam atômicas. Bibliotecas C em conformidade com POSIX (por exemplo, em Solaris e GNU / Linux) têm um mutex interno para serializar operações em FILE * s. No entanto, você ainda precisa para não fazer coisas estúpidas como chamar fclose (fs) em um segmento seguido por um acesso de fs em outro.)

Portanto, se a biblioteca C da sua plataforma for threadsafe, suas operações de E / S fstream serão threadsafe no nível mais baixo. Para operações de nível superior, como manipular os dados contidos nas classes de formatação de stream (por exemplo, configurar callbacks dentro de um std :: ofstream), você precisa proteger esses acessos como qualquer outro recurso compartilhado crítico.

Não sei se algo mudou desde o cronograma 3.0 mencionado.

A documentação de segurança de thread do MSVC para iostreamspode ser encontrada aqui: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Um único objeto é thread-safe para leitura de vários threads. Por exemplo, dado um objeto A, é seguro ler A do thread 1 e do thread 2 simultaneamente.

Se um único objeto está sendo gravado por um encadeamento, todas as leituras e gravações nesse objeto no mesmo ou em outros encadeamentos devem ser protegidas. Por exemplo, dado um objeto A, se o thread 1 estiver gravando em A, então o thread 2 deve ser impedido de ler ou gravar em A.

É seguro ler e gravar em uma instância de um tipo, mesmo se outro thread estiver lendo ou gravando em uma instância diferente do mesmo tipo. Por exemplo, dados os objetos A e B do mesmo tipo, é seguro se A estiver sendo escrito no encadeamento 1 e B estiver sendo lido no encadeamento 2.

...

Classes iostream

As classes iostream seguem as mesmas regras que as outras classes, com uma exceção. É seguro gravar em um objeto a partir de vários threads. Por exemplo, o thread 1 pode gravar em cout ao mesmo tempo que o thread 2. No entanto, isso pode resultar na saída dos dois threads sendo misturados.

Nota: A leitura de um buffer de fluxo não é considerada uma operação de leitura. Deve ser considerada como uma operação de gravação, porque altera o estado da classe.

Observe que essas informações são para a versão mais recente do MSVC (atualmente para o VS 2010 / MSVC 10 / cl.exe16.x). Você pode selecionar as informações para versões mais antigas do MSVC usando um controle suspenso na página (e as informações são diferentes para versões mais antigas).

Michael Burr
fonte
1
"Não sei se alguma coisa mudou desde o cronograma 3.0 mencionado." Definitivamente sim. Nos últimos anos, a implementação de streams g ++ executou seu próprio armazenamento em buffer.
Nemo