¿Solución temporal en el límite de hilos en Grand Central Dispatch?

Con Grand Central Dispatch , uno puede realizar fácilmente tareas que consumen mucho tiempo en un hilo que no sea principal, evitar bloquear el thead principal y mantener la IU receptiva. Simplemente usando dispatch_async y realice la tarea en una cola simultánea global.

 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // code }); 

Sin embargo, algo suena demasiado bueno para ser cierto como este, por lo general tiene su lado negativo. Después de que usamos esto mucho en nuestro proyecto de aplicación iOS, recientemente descubrimos que hay un límite de 64 hilos. Una vez que lleguemos al límite, la aplicación se congelará o se bloqueará. Al pausar la aplicación con Xcode, podemos ver que el hilo principal está en semaphore_wait_trap .

Buscar en Google en la web confirma que otros también enfrentan este problema, pero hasta ahora no se ha encontrado una solución para esto.

Despacho de envío Límite fuerte alcanzado: 64 (demasiados hilos de despacho bloqueados en operaciones síncronas)

Otra pregunta de stackoverflow confirma que este problema ocurre al usar dispatch_sync y dispatch_barrier_async también.

Pregunta:
Como Grand Central Dispatch tiene un límite de 64 hilos, ¿hay alguna solución para esto?

¡Gracias por adelantado!

Bueno, si estás limitado y determinado, puedes liberarte de los grilletes de GCD, y salir y atacar el límite de subprocesos de OS por proceso usando pthreads, pero la conclusión es la siguiente: si estás golpeando el límite de ancho de cola en GCD, es posible que desee considerar reevaluar su enfoque de simultaneidad.

En los extremos, hay dos formas de llegar al límite:

  1. Puede tener 64 hilos bloqueados en alguna primitiva del sistema operativo a través de un syscall de locking. (E / S encuadernado)
  2. Puede legítimamente tener 64 tareas ejecutables listas para rockear al mismo tiempo. (CPU enlazado)

Si se encuentra en la situación n. ° 1, el enfoque recomendado es utilizar E / S sin locking. De hecho, GCD tiene un montón de llamadas, introducidas en 10.7 / Lion IIRC, que facilitan la progtwigción asincrónica de E / S y mejoran la reutilización de hilos. Si usa el mecanismo de E / S de GCD, esos hilos no estarán atados esperando E / S, GCD simplemente pondrá en cola sus bloques (o funciones) cuando los datos estén disponibles en su descriptor de archivo (o puerto mach). Consulte la documentación para dispatch_io_create y friends .

En caso de que ayude, aquí hay un pequeño ejemplo (presentado sin garantía) de un servidor de eco TCP implementado usando el mecanismo de E / S de GCD:

 in_port_t port = 10000; void DieWithError(char *errorMessage); // Returns a block you can call later to shut down the server -- caller owns block. dispatch_block_t CreateCleanupBlockForLaunchedServer() { // Create the socket int servSock = -1; if ((servSock = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0) { DieWithError("socket() failed"); } // Bind the socket - if the port we want is in use, increment until we find one that isn't struct sockaddr_in echoServAddr; memset(&echoServAddr, 0, sizeof(echoServAddr)); echoServAddr.sin_family = AF_INET; echoServAddr.sin_addr.s_addr = htonl(INADDR_ANY); do { printf("server attempting to bind to port %d\n", (int)port); echoServAddr.sin_port = htons(port); } while (bind(servSock, (struct sockaddr *) &echoServAddr, sizeof(echoServAddr)) < 0 && ++port); // Make the socket non-blocking if (fcntl(servSock, F_SETFL, O_NONBLOCK) < 0) { shutdown(servSock, SHUT_RDWR); close(servSock); DieWithError("fcntl() failed"); } // Set up the dispatch source that will alert us to new incoming connections dispatch_queue_t q = dispatch_queue_create("server_queue", DISPATCH_QUEUE_CONCURRENT); dispatch_source_t acceptSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, servSock, 0, q); dispatch_source_set_event_handler(acceptSource, ^{ const unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); for (unsigned long i = 0; i < numPendingConnections; i++) { int clntSock = -1; struct sockaddr_in echoClntAddr; unsigned int clntLen = sizeof(echoClntAddr); // Wait for a client to connect if ((clntSock = accept(servSock, (struct sockaddr *) &echoClntAddr, &clntLen)) >= 0) { printf("server sock: %d accepted\n", clntSock); dispatch_io_t channel = dispatch_io_create(DISPATCH_IO_STREAM, clntSock, q, ^(int error) { if (error) { fprintf(stderr, "Error: %s", strerror(error)); } printf("server sock: %d closing\n", clntSock); close(clntSock); }); // Configure the channel... dispatch_io_set_low_water(channel, 1); dispatch_io_set_high_water(channel, SIZE_MAX); // Setup read handler dispatch_io_read(channel, 0, SIZE_MAX, q, ^(bool done, dispatch_data_t data, int error) { BOOL close = NO; if (error) { fprintf(stderr, "Error: %s", strerror(error)); close = YES; } const size_t rxd = data ? dispatch_data_get_size(data) : 0; if (rxd) { // echo... printf("server sock: %d received: %ld bytes\n", clntSock, (long)rxd); // write it back out; echo! dispatch_io_write(channel, 0, data, q, ^(bool done, dispatch_data_t data, int error) {}); } else { close = YES; } if (close) { dispatch_io_close(channel, DISPATCH_IO_STOP); dispatch_release(channel); } }); } else { printf("accept() failed;\n"); } } }); // Resume the source so we're ready to accept once we listen() dispatch_resume(acceptSource); // Listen() on the socket if (listen(servSock, SOMAXCONN) < 0) { shutdown(servSock, SHUT_RDWR); close(servSock); DieWithError("listen() failed"); } // Make cleanup block for the server queue dispatch_block_t cleanupBlock = ^{ dispatch_async(q, ^{ shutdown(servSock, SHUT_RDWR); close(servSock); dispatch_release(acceptSource); dispatch_release(q); }); }; return Block_copy(cleanupBlock); } 

De todos modos ... volviendo al tema en cuestión:

Si estás en la situación n. ° 2, deberías preguntarte: "¿Realmente estoy ganando algo con este enfoque?" Digamos que tienes la MacPro más estudiada: 12 núcleos, 24 núcleos virtuales / hipertrofiados. Con 64 hilos, tienes un aprox. Relación de 3: 1 de hilos a núcleo virtual. Los cambios de contexto y las fallas de caché no son gratuitas. Recuerde, supusimos que no estaba vinculado a E / S para este escenario, por lo que todo lo que hace al tener más tareas que núcleos es perder tiempo de CPU con interruptores de contexto y cache thrash.

En realidad, si su aplicación se cuelga porque ha alcanzado el límite de ancho de la cola, entonces el escenario más probable es que haya dejado de hacer cola. Probablemente haya creado una dependencia que se reduce a un punto muerto. El caso que he visto más a menudo es cuando múltiples hilos entrelazados intentan dispatch_sync en la misma cola, cuando no quedan subprocesos. Esto siempre es un fracaso.

Este es el motivo: el ancho de la cola es un detalle de implementación. El límite de ancho de 64 hilos de GCD no está documentado porque una architecture de concurrencia bien diseñada no debe depender del ancho de la cola. Siempre debe diseñar su architecture de simultaneidad de modo que una cola de 2 hilos de ancho finalmente termine el trabajo con el mismo resultado (si es más lento) que una cola de 1000 hilos de ancho. Si no lo haces, siempre habrá una posibilidad de que tu cola se muera de hambre. La división de su carga de trabajo en unidades paralelizables debe abrirse a la posibilidad de optimización, no un requisito para el funcionamiento básico. Una forma de aplicar esta disciplina durante el desarrollo es intentar trabajar con una cola en serie en los lugares donde utiliza colas concurrentes, pero espera un comportamiento sin enclavamientos. Realizar comprobaciones como esta te ayudará a detectar algunos (pero no todos) estos errores antes.

Además, al punto preciso de su pregunta original: IIUC, el límite de 64 hilos es de 64 hilos por cola simultánea de nivel superior , por lo que si realmente siente la necesidad, puede usar las tres colas simultáneas de nivel superior (Predeterminado, Alto y Bajo prioridad) para lograr más de 64 hilos en total. Por favor, no hagas esto sin embargo. Arregle su diseño de modo que no se muera de hambre en su lugar. Estarás más feliz. Y de todos modos, como indiqué arriba, si estás muriendo de hambre en una fila de 64 hilos, probablemente llegues a las tres colas de nivel superior y / o corras al límite de subproceso por proceso y te mueras de hambre de esa manera también.