Pregunta ¿Cuál es la diferencia entre epoll, poll, threadpool?


¿Podría alguien explicar cuál es la diferencia entre epoll, poll y threadpool?

  • ¿Cuáles son los pros / contras?
  • ¿Alguna sugerencia para marcos?
  • ¿Alguna sugerencia para tutoriales simples / básicos?
  • Parece que epoll y poll son específicos de Linux ... ¿Existe una alternativa equivalente para Windows?

73
2017-11-04 01:37


origen


Respuestas:


Threadpool no encaja realmente en la misma categoría que poll y epoll, así que supongo que te refieres a threadpool como en "threadpool para manejar muchas conexiones con un hilo por conexión".

Pros y contras

  • subprocesos
    • Razonablemente eficiente para la concurrencia pequeña y mediana, incluso puede superar a otras técnicas.
    • Hace uso de múltiples núcleos.
    • No escala mucho más allá de "varios cientos", aunque algunos sistemas (por ejemplo, Linux) pueden, en principio, programar cientos de hilos perfectamente.
    • Implementación ingenua exhibe "manada atronadora"problema"
    • Además del cambio de contexto y el rebaño atronador, uno debe considerar la memoria. Cada hilo tiene una pila (generalmente al menos un megabyte). Mil hilos, por lo tanto, toman un gigabyte de RAM solo para la pila. Incluso si esa memoria no está comprometida, aún le resta espacio de direcciones considerable en un sistema operativo de 32 bits (realmente no es un problema por debajo de 64 bits).
    • Trapos poder realmente uso epoll, aunque la forma obvia (todos los hilos bloquean epoll_wait) no sirve, porque epoll se despertará cada hilo esperando, por lo que todavía tendrá los mismos problemas.
      • Solución óptima: hilo único escucha en epoll, realiza la multiplexación de entrada y realiza solicitudes completas a un grupo de subprocesos.
      • futex es su amigo aquí, en combinación con, por ejemplo, una cola de avance rápido por hilo. Aunque está mal documentado y difícil de manejar, futex ofrece exactamente lo que se necesita. epoll puede devolver varios eventos a la vez, y futex le permite de manera eficiente y de manera controlada controlar de manera precisa norte hilos bloqueados a la vez (siendo N min(num_cpu, num_events) idealmente), y en el mejor de los casos no implica un cambio adicional de syscall / context.
      • No es trivial de implementar, toma algo de cuidado.
  • fork (a.k. un hilo de moda antiguo)
    • Razonablemente eficiente para concurrencia pequeña y mediana.
    • No escala más allá de "algunos cientos".
    • Los interruptores de contexto son mucho más caro (¡diferentes espacios de direcciones!).
    • Escala significativamente peor en sistemas más antiguos donde la horquilla es mucho más costosa (copia profunda de todas las páginas). Incluso en sistemas modernos fork no es "libre", aunque la sobrecarga se combina principalmente con el mecanismo de copiar y escribir. En grandes conjuntos de datos que son también modificado, un número considerable de fallas de página siguientes fork puede afectar negativamente el rendimiento.
    • Sin embargo, se ha comprobado que funciona de manera confiable por más de 30 años.
    • Ridículamente fácil de implementar y sólido: si alguno de los procesos falla, el mundo no termina. No hay (casi) nada que puedas hacer mal.
    • Muy propenso a la "manada tronante".
  • poll / select
    • Dos sabores (BSD vs. Sistema V) de más o menos lo mismo.
    • Un uso algo viejo y lento, algo incómodo, pero prácticamente no existe una plataforma que no los soporte.
    • Espera hasta que "algo suceda" en un conjunto de descriptores
      • Permite que un hilo / proceso maneje muchas solicitudes a la vez.
      • Sin uso multi-core
    • Necesita copiar la lista de descriptores del usuario al espacio del kernel cada vez que espere. Necesita realizar una búsqueda lineal sobre descriptores. Esto limita su efectividad.
    • No escala bien a "miles" (de hecho, límite duro alrededor de 1024 en la mayoría de los sistemas, o tan bajo como 64 en algunos).
    • Úselo porque es portátil si solo trata con una docena de descriptores de todos modos (no hay problemas de rendimiento allí), o si debe admitir plataformas que no tienen nada mejor. No lo uses de otra manera.
    • Conceptualmente, un servidor se vuelve un poco más complicado que uno bifurcado, ya que ahora necesita mantener muchas conexiones y una máquina de estado para cada conexión, y debe multiplexar entre solicitudes a medida que entran, ensamblar solicitudes parciales, etc. Un simple bifurcado el servidor solo conoce un solo socket (bueno, dos, contando el socket de escucha), lee hasta que tenga lo que quiere o hasta que la conexión esté medio cerrada, y luego escribe lo que quiera. No se preocupa por el bloqueo o la preparación o la inanición, ni por la introducción de datos no relacionados, ese es otro problema de otro proceso.
  • epoll
    • Solo Linux
    • Concepto de modificaciones costosas versus esperas eficientes:
      • Copia información sobre los descriptores al espacio del kernel cuando se agregan descriptores (epoll_ctl)
        • Esto es usualmente algo que sucede raramente.
      • Hace no necesidad de copiar datos al espacio del núcleo cuando se esperan eventos (epoll_wait)
        • Esto es usualmente algo que sucede muy a menudo.
      • Agrega el mesero (o más bien su estructura epoll) a las colas de espera de los descriptores
        • Por lo tanto, Descriptor sabe quién está escuchando y señala directamente a los camareros cuando corresponde, en lugar de a los camareros que buscan una lista de descriptores.
        • Manera opuesta de cómo poll trabajos
        • O (1) con k pequeña (muy rápido) con respecto al número de descriptores, en lugar de O (n)
    • Funciona muy bien con timerfd y eventfd (impresionante resolución y precisión del temporizador, también).
    • Funciona bien con signalfd, eliminando el manejo incómodo de las señales, haciéndolas parte del flujo de control normal de una manera muy elegante.
    • Una instancia epoll puede alojar otras instancias epoll recursivamente
    • Suposiciones hechas por este modelo de programación:
      • La mayoría de los descriptores están inactivos la mayor parte del tiempo, pocas cosas (por ejemplo, "datos recibidos", "conexión cerrada") ocurren realmente en algunas descripciones.
      • La mayoría de las veces, no desea agregar / eliminar descriptores del conjunto.
      • La mayoría de las veces, estás esperando que algo suceda.
    • Algunas trampas menores:
      • Un epoll desencadenado por nivel despierta todos los hilos que le esperan (esto es "funciona según lo previsto"), por lo tanto, la forma ingenua de usar epoll con un threadpool es inútil. Al menos para un servidor TCP, no es un gran problema ya que las solicitudes parciales tendrían que ser ensambladas primero de todos modos, por lo que una implementación naive multiproceso no funcionará de ninguna manera.
      • No funciona como uno esperaría con la lectura / escritura de archivos ("siempre listo").
      • No se pudo usar con AIO hasta hace poco, ahora es posible a través de eventfd, pero requiere una función no documentada (hasta la fecha).
      • Si las suposiciones anteriores son no cierto, epoll puede ser ineficiente, y poll puede realizar igual o mejor.
      • epoll no puede hacer "magia", es decir, todavía es necesariamente O (N) con respecto al número de eventos que ocurren.
      • Sin embargo, epoll juega bien con el nuevo recvmmsg syscall, ya que devuelve varias notificaciones de disponibilidad a la vez (tantas como estén disponibles, hasta lo que especifique como maxevents) Esto hace posible recibir, p. 15 notificaciones EPOLLIN con un syscall en un servidor ocupado, y leer los 15 mensajes correspondientes con un segundo syscall (¡una reducción del 93% en llamadas de sistema!). Desgraciadamente, todas las operaciones en una recvmmsg La invocación hace referencia al mismo socket, por lo que es más útil para los servicios basados ​​en UDP (para TCP, tendría que haber un tipo de recvmmsmsg syscall que también toma un descriptor de socket por artículo!).
      • Los descriptores deberían siempre establecerse en no bloqueo y uno debe verificar EAGAIN incluso cuando se usa epoll porque hay situaciones excepcionales donde epoll la preparación de los informes y una posterior lectura (o escritura) todavía bloquear. Este es también el caso de poll/select en algunos núcleos (aunque presumiblemente se ha corregido).
      • Con un ingenuo implementación, inanición de remitentes lentos es posible. Cuando ciegamente lee hasta EAGAIN se devuelve después de recibir una notificación, es posible leer de forma indefinida nuevos datos entrantes de un remitente rápido mientras que muere de hambre por completo un remitente lento (siempre y cuando los datos sigan llegando lo suficientemente rápido, es posible que no vea EAGAIN ¡durante bastante tiempo!). Se aplica a poll/select de la misma manera.
      • El modo activado por el borde tiene algunas peculiaridades y un comportamiento inesperado en algunas situaciones, ya que la documentación (tanto las páginas man como TLPI) son vagas ("probablemente", "debería", "podría") y a veces confunden su funcionamiento.
        La documentación indica que varios hilos que esperan en un epoll están señalizados. También establece que una notificación le informa si la actividad de IO ha sucedido desde la última llamada a epoll_wait (o desde que se abrió el descriptor, si no hubo una llamada anterior).
        El comportamiento real y observable en el modo disparado por el borde está mucho más cerca de "despierta el primero hilo que ha llamado epoll_wait, señalando que la actividad de IO ha sucedido desde nadie último llamado ya sea  epoll_wait  o una función de lectura / escritura en el descriptor, y luego solo informa de la preparación nuevamente a la siguiente cadena de mensajes o ya bloqueada en  epoll_wait, para cualquier operación que ocurra después nadie llamada función de lectura (o escritura) en el descriptor ". Tiene algún tipo de sentido, también ... simplemente no es exactamente lo que sugiere la documentación.
  • kqueue
    • Análogo de BSD a epoll, uso diferente, efecto similar.
    • También funciona en Mac OS X
    • Se rumorea que es más rápido (nunca lo he usado, por lo que no puedo decir si eso es cierto).
    • Registra eventos y devuelve un conjunto de resultados en un solo syscall.
  • Puertos de finalización IO
    • Epoll para Windows, o más bien epoll sobre esteroides.
    • Funciona a la perfección con todo que es waitable o se puede alertar de alguna manera (sockets, temporizadores waitable, operaciones de archivos, hilos, procesos)
    • Si Microsoft tiene una cosa bien en Windows, son los puertos de terminación:
      • Funciona sin preocupaciones fuera de la caja con cualquier cantidad de hilos
      • No hay rebaños atronadores
      • Despierta los hilos uno por uno en un orden LIFO
      • Mantiene cachés calientes y minimiza los interruptores de contexto
      • Respeta el número de procesadores en la máquina o entrega el número deseado de trabajadores
    • Permite a la aplicación publicar eventos, lo que se presta a una implementación de colas de trabajos paralelos muy fácil, a prueba de fallas y eficiente (programa más de 500,000 tareas por segundo en mi sistema).
    • Desventaja menor: no elimina fácilmente los descriptores de archivos una vez agregados (debe cerrar y volver a abrir).

Frameworks

liberante - La versión 2.0 también admite puertos de finalización en Windows.

ASIO - Si usa Boost en su proyecto, no busque más: ya tiene este disponible como boost-asio.

¿Alguna sugerencia para tutoriales simples / básicos?

Los marcos enumerados anteriormente vienen con una extensa documentación. El Linux documentos y MSDN explica extensivamente los puertos epoll y de finalización.

Mini-tutorial para usar epoll:

int my_epoll = epoll_create(0);  // argument is ignored nowadays

epoll_event e;
e.fd = some_socket_fd; // this can in fact be anything you like

epoll_ctl(my_epoll, EPOLL_CTL_ADD, some_socket_fd, &e);

...
epoll_event evt[10]; // or whatever number
for(...)
    if((num = epoll_wait(my_epoll, evt, 10, -1)) > 0)
        do_something();

Mini-tutorial para puertos de terminación de E / S (nota llamando a CreateIoCompletionPort dos veces con diferentes parámetros):

HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, 0, 0, 0); // equals epoll_create
CreateIoCompletionPort(mySocketHandle, iocp, 0, 0); // equals epoll_ctl(EPOLL_CTL_ADD)

OVERLAPPED o;
for(...)
    if(GetQueuedCompletionStatus(iocp, &number_bytes, &key, &o, INFINITE)) // equals epoll_wait()
        do_something();

(Estos mini-tuts omiten todo tipo de comprobación de errores, y con suerte no cometí errores tipográficos, pero en su mayor parte deberían estar bien para darte una idea).

EDITAR:
Tenga en cuenta que los puertos de terminación (Windows) funcionan conceptualmente al revés como epoll (o kqueue). Señalan, como su nombre sugiere, terminaciónno preparación. Es decir, desactivas una solicitud asíncrona y te olvidas de ella hasta que, un tiempo después, te dicen que se ha completado (con éxito o no tanto, y existe el caso excepcional de "completado inmediatamente" también).
Con epoll, se bloquea hasta que se le notifica que "algunos datos" (posiblemente tan poco como un byte) han llegado y están disponibles o que hay suficiente espacio en el búfer para que pueda realizar una operación de escritura sin bloquear. Solo entonces, cuando comience la operación real, que con suerte no bloqueará (aparte de lo que cabría esperar, no hay una garantía estricta para eso; por lo tanto, es una buena idea establecer descriptores para no bloquear y verificar EAGAIN [EAGAIN] y EWOULDBLOCK para enchufes, porque oh joy, el estándar permite dos valores de error diferentes]).


206
2018-03-27 14:25