2012-08-08 12 views
6

Tengo una aplicación multiproceso que crea 48 subprocesos que necesitan acceder a un atributo común (stl :: map). El mapa solo se escribirá cuando comiencen los hilos, y el resto del tiempo se leerá el mapa. Este parece ser el caso de uso perfecto para un pthread_rw_lock, y todo parece estar funcionando bien.¿Cuántos lectores simultáneos puede tener un pthread_rwlock?

Me encontré con un seg-fault completamente no relacionado y comencé a analizar el núcleo. Usando gdb, ejecuté el comando info threads y me sorprendieron los resultados. Observé que varios hilos estaban realmente leyendo del mapa como se esperaba, pero la parte extraña es que varios hilos fueron bloqueados en pthread_rwlock_rdlock() esperando en el rw_lock.

Aquí es el seguimiento de la pila para un hilo que está esperando en la cerradura:

#0 0xffffe430 in __kernel_vsyscall() 
#1 0xf76fe159 in __lll_lock_wait() from /lib/libpthread.so.0 
#2 0xf76fab5d in pthread_rwlock_rdlock() from /lib/libpthread.so.0 
#3 0x0804a81a in DiameterServiceSingleton::getDiameterService(void*)() 

Con tantos hilos, es difícil decir cuántos estaban leyendo y cuántos fueron bloqueados, pero yo no entiendo por qué cualquiera hilos estarían bloqueados esperando a leer, teniendo en cuenta que otros hilos ya están leyendo.

Así que aquí está mi pregunta: ¿Por qué algunos hilos están bloqueados esperando leer un rw_lock, cuando otros hilos ya están leyendo? Parece como si hubiera un límite en la cantidad de hilos que se pueden leer simultáneamente.

Miré las funciones pthread_rwlock_attr_t y no vi nada relacionado.

El sistema operativo es Linux, SUSE 11.

Aquí está el código relacionado:

{ 
    pthread_rwlock_init(&serviceMapRwLock_, NULL); 
} 

// This method is called for each request processed by the threads 
Service *ServiceSingleton::getService(void *serviceId) 
{ 
    pthread_rwlock_rdlock(&serviceMapRwLock_); 
    ServiceMapType::const_iterator iter = serviceMap_.find(serviceId); 
    bool notFound(iter == serviceMap_.end()); 
    pthread_rwlock_unlock(&serviceMapRwLock_); 

    if(notFound) 
    { 
    return NULL; 
    } 

    return iter->second; 
} 

// This method is only called when the app is starting 
void ServiceSingleton::addService(void *serviceId, Service *service) 
{ 
    pthread_rwlock_wrlock(&serviceMapRwLock_); 
    serviceMap_[serviceId] = service; 
    pthread_rwlock_unlock(&serviceMapRwLock_); 
} 

Actualización:

Como se mencionó en los comentarios de MarkB, si hubiera establecido pthread_rwlockattr_getkind_np() para dar prioridad a los escritores, y hay un escritor bloqueado esperando, entonces el comportamiento observado tendría sentido. Pero, estoy usando el valor predeterminado que creo que es dar prioridad a los lectores. Acabo de verificar que hay no hilos bloqueados en espera de escritura. También actualizo el código según lo sugerido por @Shahbaz en los comentarios y obtengo los mismos resultados.

+3

¿Está * seguro * de que no había un bloqueo de bloqueo de escritorios también? –

+0

@MarkB ¡Esa es una excelente pregunta! Pero eso no depende de pthread_rwlockattr_getkind_np() que no he llamado? No estoy seguro de si hay hilos esperando para escribir, pero no deberían ser, ya que eso SOLO debe suceder al principio. Aunque tendré que comprobarlo. – Brady

+0

@MarkB, ¿qué efecto tendría eso si un escritor está esperando y yo no configuré pthread_rwlockattr_getkind_np()? Según tengo entendido, el escritor puede morir de hambre si hay lectores continuos, ¿verdad? – Brady

Respuesta

6

Simplemente observó los problemas de rendimiento inherentes a la adquisición de bloqueos. Lleva algo de tiempo, y acabas de atrapar esos hilos en el medio. Esto es particularmente cierto cuando la operación protegida por la cerradura tiene una duración muy corta.

Editar: Lectura de la fuente, glibc utiliza lll_lock para proteger secciones críticas dentro de sus propias estructuras de datos de la biblioteca pthread. El pthread_rwlock_rdlock comprueba varios indicadores e incrementa los contadores, por lo que hace esas cosas mientras mantiene un bloqueo. Una vez hecho esto, se libera el bloqueo con lll_unlock.

Para demostrar, implementé una breve rutina que duerme después de adquirir el rwlock. El hilo principal espera a que terminen. Pero antes de esperar, imprime la concurrencia lograda por los hilos.

enum { CONC = 50 }; 

pthread_rwlock_t rwlock; 
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 
unsigned count; 

void *routine(void *arg) 
{ 
    int *fds = static_cast<int *>(arg); 
    pthread_rwlock_rdlock(&rwlock); 
    pthread_mutex_lock(&mutex); 
    ++count; 
    if (count == CONC) pthread_cond_signal(&cond); 
    pthread_mutex_unlock(&mutex); 
    sleep(5); 
    pthread_rwlock_unlock(&rwlock); 
    pthread_t self = pthread_self(); 
    write(fds[1], &self, sizeof(self)); 
    return 0; 
} 

y el principal hilo espera para el contador para llegar a 50:

int main() 
{ 
    int fds[2]; 
    pipe(fds); 
    pthread_rwlock_init(&rwlock, 0); 
    pthread_mutex_lock(&mutex); 
    for (int i = 0; i < CONC; i++) { 
     pthread_t tid; 
     pthread_create(&tid, NULL, routine, fds); 
    } 
    while (count < CONC) pthread_cond_wait(&cond, &mutex); 
    pthread_mutex_unlock(&mutex); 
    std::cout << "count: " << count << std::endl; 
    for (int i = 0; i < CONC; i++) { 
     pthread_t tid; 
     read(fds[0], &tid, sizeof(tid)); 
     pthread_join(tid, 0); 
    } 
    pthread_rwlock_destroy(&rwlock); 
    pthread_exit(0); 
} 

Editar: simplificado el ejemplo usando C++ 11 soporte de hilos:

enum { CONC = 1000 }; 
std::vector<std::thread> threads; 

pthread_rwlock_t rwlock; 
std::mutex mutex; 
std::condition_variable cond; 
unsigned count; 

void *routine(int self) 
{ 
    pthread_rwlock_rdlock(&rwlock); 
    { std::unique_lock<std::mutex> lk(mutex); 
     if (++count == CONC) cond.notify_one(); } 
    sleep(5); 
    pthread_rwlock_unlock(&rwlock); 
    return 0; 
} 

int main() 
{ 
    pthread_rwlock_init(&rwlock, 0); 
    { std::unique_lock<std::mutex> lk(mutex); 
     for (int i = 0; i < CONC; i++) { 
      threads.push_back(std::thread(routine, i)); 
     } 
     cond.wait(lk, [](){return count == CONC;}); } 
    std::cout << "count: " << count << std::endl; 
    for (int i = 0; i < CONC; i++) { 
     threads[i].join(); 
    } 
    pthread_rwlock_destroy(&rwlock); 
    pthread_exit(0); 
} 
+0

Actualizo mi pregunta con el seguimiento de la pila de un hilo esperando en un bloqueo de lectura.¿Quiere decir que a pesar de que el hilo está en '__lll_lock_wait()' que no está realmente en espera/bloqueado, pero está en la función para obtener un bloqueo de lectura? Si es así, ese es un nombre de función desafortunado, aunque no sería el primero :) – Brady

+0

@ Sí. No tengo las fuentes en frente de mí para confirmar, pero sospecho que esto corresponde al punto de entrada de llamada del sistema para 'glibc' para llegar al kernel. – jxh

+0

Esto tendría sentido. No es que no te crea :) pero estoy buscando el código fuente para verificarlo. Te lo haré saber cuando lo encuentre, ¡gracias! – Brady

3

Como nota al margen, el código publicado arriba está roto. No puede acceder a iter-> segundo fuera de la sección rw_lock'd, porque tan pronto como desbloquea el rw_lock, un escritor puede eliminar cualquier elemento en el mapa, invalidando así cualquier iterador en él.

Sé que no está haciendo esto en su caso ya que no escribe nada más allá del comienzo de la ejecución del programa, pero aún así, vale la pena mencionarlo.

También, como nota al margen, ya que el comportamiento de su descripción parece serializado (los escritores escriben en el mapa al principio, entonces los lectores leen un mapa de "solo lectura" a partir de ahora), probablemente debería escribirlo de esta manera:

int writerDoneWithMap = 0; 
// pthread_cond & mutex init here 

// The writer write to the map here 

// Then they signal the reader that they are done with it 
while (!__sync_bool_compare_and_swap(&writerDoneWithMap, 1, writerDoneWithMap)); 
pthread_cond_broadcast here 


// The readers will then simply do this: 
while (!writerDoneWithMap) 
{ 
    // pthread_cond_wait here 
} 
// Read the data without locks. 

el código anterior evitar cualquier bloqueo en los lectores si el escritor hubiera terminado de llenar el mapa, y en caso de que no tiene, entonces usted recurre a técnicas típicas pthread_cond/exclusión mutua. El código anterior es correcto si y solo si tiene lectores THEN THEN (pero eso es lo que ha dicho), de lo contrario, fallará.

Cuestiones relacionadas