¿Cout está sincronizado / es seguro para subprocesos?

112

En general, asumo que las transmisiones no están sincronizadas, depende del usuario hacer el bloqueo apropiado. Sin embargo, ¿cosas como coutrecibir un tratamiento especial en la biblioteca estándar?

Es decir, si se escriben varios subprocesos, ¿ coutpueden dañar el coutobjeto? Entiendo que incluso si estuviera sincronizado, todavía obtendría una salida intercalada aleatoriamente, pero ese intercalado está garantizado. Es decir, ¿es seguro utilizarlo coutdesde varios subprocesos?

¿Depende este proveedor? ¿Qué hace gcc?


Importante : proporcione algún tipo de referencia para su respuesta si dice "sí", ya que necesito algún tipo de prueba de esto.

Mi preocupación tampoco es sobre las llamadas al sistema subyacentes, están bien, pero las secuencias agregan una capa de almacenamiento en búfer en la parte superior.

edA-qa mort-ora-y
fuente
2
Esto depende del proveedor. C ++ (antes de C ++ 0x) no tiene noción de múltiples subprocesos.
Sven
2
¿Qué pasa con c ++ 0x? Define un modelo de memoria y lo que es un hilo, entonces, ¿quizás estas cosas se filtraron en la salida?
rubenvb
2
¿Hay algún proveedor que lo haga seguro para subprocesos?
edA-qa mort-ora-y
¿Alguien tiene un enlace al estándar propuesto de C ++ 2011 más reciente?
edA-qa mort-ora-y
4
En cierto sentido, aquí es donde printfbrilla, ya que la salida completa se escribe de stdoutuna vez; cuando el uso de std::coutcada eslabón de la cadena de expresión se enviaría por separado a stdout; entre ellos puede haber algún otro hilo de escritura stdoutdebido al cual el orden de la salida final se estropea.
legends2k

Respuestas:

106

El estándar C ++ 03 no dice nada al respecto. Cuando no tiene garantías sobre la seguridad de subprocesos de algo, debe tratarlo como no seguro para subprocesos.

De particular interés aquí es el hecho de que coutestá protegido. Incluso si writese garantiza que las llamadas a (o lo que sea que logre ese efecto en esa implementación en particular) sean mutuamente excluyentes, el búfer podría ser compartido por los diferentes subprocesos. Esto conducirá rápidamente a la corrupción del estado interno de la secuencia.

E incluso si se garantiza que el acceso al búfer es seguro para subprocesos, ¿qué crees que sucederá en este código?

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

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

Probablemente desee que cada línea aquí actúe en exclusión mutua. Pero, ¿cómo puede garantizar eso una implementación?

En C ++ 11, tenemos algunas garantías. El FDIS dice lo siguiente en §27.4.1 [iostream.objects.overview]:

El acceso simultáneo a las funciones de entrada y salida (§27.7.2.1) y salida (§27.7.3.1) de un objeto iostream estándar sincronizado (§27.5.3.4) o una secuencia C estándar por varios subprocesos no dará lugar a una carrera de datos (§ 1.10). [Nota: los usuarios aún deben sincronizar el uso concurrente de estos objetos y secuencias por varios subprocesos si desean evitar caracteres intercalados. - nota final]

Por lo tanto, no obtendrá transmisiones dañadas, pero aún debe sincronizarlas manualmente si no desea que la salida sea basura.

R. Martinho Fernandes
fuente
2
Técnicamente cierto para C ++ 98 / C ++ 03, pero creo que todo el mundo lo sabe. Pero esto no responde a las dos preguntas interesantes: ¿Qué pasa con C ++ 0x? ¿Qué implementaciones típicas de hecho lo hacen ?
Nemo
1
@ edA-qa mort-ora-y: No, te equivocas. C ++ 11 define claramente que los objetos de flujo estándar se pueden sincronizar y conservar un comportamiento bien definido, no que lo sean de forma predeterminada.
ildjarn
12
@ildjarn - No, @ edA-qa mort-ora-y es correcto. Siempre que cout.sync_with_stdio()sea ​​cierto, el uso coutpara generar caracteres de varios subprocesos sin sincronización adicional está bien definido, pero solo en el nivel de bytes individuales. Por lo tanto, cout << "ab";y se puede cout << "cd"ejecutar en diferentes subprocesos acdb, por ejemplo, pero no puede causar un comportamiento indefinido.
JohannesD
4
@JohannesD: Estamos de acuerdo en eso: está sincronizado con la API C subyacente. Mi punto es que no está "sincronizado" de una manera útil, es decir, uno todavía necesita sincronización manual si no quiere datos basura.
ildjarn
2
@ildjarn, estoy bien con los datos basura, eso lo entiendo. Solo me interesa la condición de la carrera de datos, que parece estar clara ahora.
edA-qa mort-ora-y
16

Esta es una gran pregunta.

Primero, C ++ 98 / C ++ 03 no tiene el concepto de "hilo". Entonces, en ese mundo, la pregunta no tiene sentido.

¿Qué pasa con C ++ 0x? Vea la respuesta de Martinho (que admito que me sorprendió).

¿Qué hay de las implementaciones específicas anteriores a C ++ 0x? Bueno, por ejemplo, aquí está el código fuente basic_streambuf<...>:sputcde GCC 4.5.2 (encabezado "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, esto no realiza ningún bloqueo. Y tampoco lo hace xsputn. Y este es definitivamente el tipo de streambuf que usa Cout.

Por lo que puedo decir, libstdc ++ no bloquea ninguna de las operaciones de transmisión. Y no esperaría ninguno, ya que sería lento.

Entonces, con esta implementación, obviamente es posible que la salida de dos subprocesos se corrompa entre sí ( no solo se intercalen).

¿Podría este código dañar la estructura de datos en sí? La respuesta depende de las posibles interacciones de estas funciones; por ejemplo, qué sucede si un hilo intenta vaciar el búfer mientras otro intenta llamar xsputno lo que sea. Puede depender de cómo el compilador y la CPU decidan reordenar las cargas y almacenes de memoria; se necesitaría un análisis cuidadoso para estar seguro. También depende de lo que haga su CPU si dos subprocesos intentan modificar la misma ubicación al mismo tiempo.

En otras palabras, incluso si funciona bien en su entorno actual, podría romperse cuando actualice su tiempo de ejecución, compilador o CPU.

Resumen ejecutivo: "No lo haría". Cree una clase de registro que realice un bloqueo adecuado o muévala a C ++ 0x.

Como alternativa débil, puede configurar cout como sin búfer. Es probable (aunque no garantizado) que omita toda la lógica relacionada con el búfer y llame writedirectamente. Aunque eso podría ser prohibitivamente lento.

Nemo
fuente
1
Buena respuesta, pero mire la respuesta de Martinho que muestra que C ++ 11 define la sincronización para cout.
edA-qa mort-ora-y
7

El estándar C ++ no especifica si escribir en secuencias es seguro para subprocesos, pero normalmente no lo es.

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

y también: ¿Son los flujos de salida estándar en C ++ seguros para subprocesos (cout, cerr, clog)?

ACTUALIZAR

Por favor, eche un vistazo a la respuesta de @Martinho Fernandes para saber qué dice el nuevo estándar C ++ 11 sobre esto.

foxis
fuente
3
Supongo que dado que C ++ 11 es ahora el estándar, esta respuesta es realmente incorrecta ahora.
edA-qa mort-ora-y
6

Como mencionan otras respuestas, esto definitivamente es específico del proveedor, ya que el estándar C ++ no menciona el subproceso (esto cambia en C ++ 0x).

GCC no hace muchas promesas sobre seguridad de subprocesos y E / S. Pero la documentación de lo que promete está aquí:

la clave es probablemente:

El tipo __basic_file es simplemente una colección de pequeños envoltorios alrededor de la capa C stdio (nuevamente, vea el enlace debajo de Estructura). No nos encerramos, simplemente pasamos a las llamadas para fopen, fwrite, etc.

Por lo tanto, para 3.0, la pregunta "¿es seguro el multiproceso para E / S" debe responderse con "la biblioteca C de su plataforma es segura para E / S?" Algunos lo son por defecto, otros no; muchos ofrecen múltiples implementaciones de la biblioteca C con diferentes compensaciones de seguridad y eficiencia de subprocesos. Usted, el programador, siempre debe tener cuidado con varios subprocesos.

(Como ejemplo, el estándar POSIX requiere que las operaciones C stdio FILE * sean atómicas. Las bibliotecas C que cumplen con POSIX (por ejemplo, en Solaris y GNU / Linux) tienen un mutex interno para serializar operaciones en FILE * s. Sin embargo, aún necesita para no hacer cosas estúpidas como llamar a fclose (fs) en un hilo seguido de un acceso de fs en otro.)

Por lo tanto, si la biblioteca C de su plataforma es segura para subprocesos, sus operaciones de E / S de fstream serán seguras para subprocesos en el nivel más bajo. Para operaciones de nivel superior, como manipular los datos contenidos en las clases de formato de flujo (por ejemplo, configurar devoluciones de llamada dentro de un std :: ofstream), necesita proteger dichos accesos como cualquier otro recurso compartido crítico.

No sé si algo ha cambiado desde el período de tiempo 3.0 mencionado.

La documentación de seguridad de subprocesos de MSVC para iostreamsse puede encontrar aquí: http://msdn.microsoft.com/en-us/library/c9ceah3b.aspx :

Un solo objeto es seguro para la lectura de varios subprocesos. Por ejemplo, dado un objeto A, es seguro leer A del hilo 1 y del hilo 2 simultáneamente.

Si un solo objeto está siendo escrito por un hilo, entonces todas las lecturas y escrituras en ese objeto en el mismo o en otros hilos deben estar protegidas. Por ejemplo, dado un objeto A, si el hilo 1 está escribiendo en A, entonces se debe evitar que el hilo 2 lea o escriba en A.

Es seguro leer y escribir en una instancia de un tipo incluso si otro hilo está leyendo o escribiendo en una instancia diferente del mismo tipo. Por ejemplo, dados los objetos A y B del mismo tipo, es seguro si A se escribe en el hilo 1 y B se lee en el hilo 2.

...

clases de iostream

Las clases de iostream siguen las mismas reglas que las otras clases, con una excepción. Es seguro escribir en un objeto desde varios subprocesos. Por ejemplo, el hilo 1 puede escribir en cout al mismo tiempo que el hilo 2. Sin embargo, esto puede provocar que la salida de los dos hilos se entremezcle.

Nota: La lectura de un búfer de flujo no se considera una operación de lectura. Debe considerarse como una operación de escritura, porque cambia el estado de la clase.

Tenga en cuenta que esa información es para la versión más reciente de MSVC (actualmente para VS 2010 / MSVC 10 / cl.exe16.x). Puede seleccionar la información para versiones anteriores de MSVC mediante un control desplegable en la página (y la información es diferente para versiones anteriores).

Michael Burr
fuente
1
"No sé si algo ha cambiado desde el plazo de 3.0 mencionado". Definitivamente lo hizo. Durante los últimos años, la implementación de g ++ streams ha realizado su propio almacenamiento en búfer.
Nemo