Si protegemos una escritura con una critical section, seguramente querreis proteger la lectura.

Es normal tener una critical section en proyectos que hacen escrituras concurrentes en variables o en una colección de variables, y si no lo haces ya tienes una pista de porqué se cae tu sistema ;).

Sin embargo, si protegemos una escritura con una critical section, seguramente también querremos proteger la lectura, ya que si no, la lectura también luchará contra la escritura a la hora de acceder a la variable de la discordia.

Vamos a ver un ejemplo que Adam Rosenfiel compartió hace tiempo en un comentario hace unos cuantos años. Voy a reproducir el ejemplo para que no tengáis que hacer clic, pero también para conseguir un post más largo y para que parezca que hago algo (ya que Adam hizo todo este trabajo):

class X {
volatile int mState;
CRITICAL_SECTION mCrit;
HANDLE mEvent;
};

X::Wait() {
while(mState != kDone) {
WaitForSingleObject(mEvent, INFINITE);
}
}

X::~X() {
DestroyCriticalSection(&mCrit);
}

X::SetState(int state) {
EnterCriticalSection(&mCrit);
// do some state logic
mState = state;
SetEvent(mEvent);
LeaveCriticalSection(&mCrit);
}

Thread1()
{
X x;
... spawn off thread 2 ...
x.Wait();
}

Thread2(X* px)
{
...
px->SetState(kDone);
...
}

Hay una condición de carrera aquí:

  • El thread 1 llama a x::wait y espera.
  • El thread 2 llama a x::SetState
  • El thread 2 obtiene preferencia juesto después de llamar a SetEvent.
  • El thread 1 se levanta con la llama WaitForSingleObject, comprueba que mState==kDone, sale del bucle y termina el método x::Wait.
  • El thread 1 destruye el objeto X, que destruye a su vez la critical section.
  • El thread 2 se ejecuta e intenta salir de la critical section que ha sido destruida.

La solución es incluir la lectura de mState en una critical section:

X::Wait() {
while(1) {
EnterCriticalSection(&mCrit);
int state = mState;
LeaveCriticalSection(&mCrit);
if(state == kDone)
break;
WaitForSingleObject(mEvent, INFINITE);
}
}

Olvidarnos de incluir la lectura en una critical section es algo común. Yo lo he hecho más de una vez. Te estarás diciendo: "No necesito una crítical sectio aquí. Sólo estoy leyendo un valor que se puede leer sin problemas." Pero te olvidas que la critical sectio no está ahí sólo para proteger la escritura de la variable; también está para todas las otras acciones que ocurren en la sección crítica.

En realidad sí que he trabajado un poco, Os dejo con este puzle con forma de problema:

class BufferPool {
public:
BufferPool() { ... }
~BufferPool() { ... }

Buffer *GetBuffer()
{
Buffer *pBuffer = FindFreeBuffer();
if (pBuffer) {
pBuffer->mIsFree = false;
}
return pBuffer;
}

void ReturnBuffer(Buffer *pBuffer)
{
pBuffer->mIsFree = true;
}

private:
Buffer *FindFreeBuffer()
{
EnterCriticalSection(&mCrit);
Buffer *pBuffer = NULL;
for (int i = 0; i < 8; i++) {
if (mBuffers[i].mIsFree) {
pBuffer = &mBuffers[i];
break;
}
}
LeaveCriticalSection(&mCrit);
return pBuffer;
}
private:
CRITICAL_SECTION mCrit;
Buffer mBuffers[8];
};

Una pequeña pista: si declaráis la variable como volatile, no ayudará.

Llevo un tiempo siguiendo el blog de The Old New Thing y me parece un blog interesante con muchas "historias para no dormir" y posts interesantes. Este post es una traducción libre del post: If you protect a write with a critical section, you may also want to protect the read.

Me ha parecido digno de traducir y compartir.

Espero que os haya gustado.

Juan María Laó Ramos.