asynchronous epoll io-completion-ports

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



io-completion-ports (1)

¿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?

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 100,000s de hilos perfectamente.
    • La implementación ingenua muestra un problema de " manada estruendosa ".
    • 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).
    • Los epoll realmente pueden usar epoll , aunque la forma obvia (todos los subprocesos bloquean en epoll_wait ) no sirve, porque epoll activará cada subproceso que lo espere, por lo que seguirá teniendo 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 tu amigo aquí, en combinación con, por ejemplo, una cola de avance rápido por hilo. Aunque está mal documentado y es difícil de manejar, futex ofrece exactamente lo que se necesita. epoll puede devolver varios eventos a la vez, y futex permite de manera eficiente y de manera controlada controlar los hilos bloqueados N a la vez (N es min(num_cpu, num_events) idealmente) y, en el mejor de los casos, no implica un extra syscall / context switch en absoluto.
      • No es trivial de implementar, toma algo de cuidado.
  • fork (también conocido como threadpool 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 caros (¡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 los sistemas modernos, fork no es "libre", aunque la sobrecarga está mayormente fusionada por el mecanismo de copiado por escritura. En grandes conjuntos de datos que también se modifican , un número considerable de fallas de página después de la fork pueden 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 núcleo cuando se agregan descriptores ( epoll_ctl )
        • Esto generalmente es algo que sucede raramente .
      • No necesita copiar datos en el espacio del kernel al esperar eventos ( epoll_wait )
        • Esto suele ser 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 a cómo funciona la poll
        • 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") suceden en pocas 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 no son ciertas, epoll puede ser ineficiente, y la poll puede tener el mismo rendimiento o mejor.
      • epoll no puede hacer "magia", es decir, todavía es necesariamente O (N) con respecto a la cantidad de eventos que ocurren .
      • Sin embargo, epoll bien con el nuevo recvmmsg recvmmsg, 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, por ejemplo, 15 notificaciones de EPOLLIN con un syscall en un servidor ocupado, y leer los 15 mensajes correspondientes con un segundo syscall (¡una reducción del 93% en syscalls!). Desafortunadamente, todas las operaciones en una recvmmsg recvmmsg se refieren al mismo socket, por lo que es más útil para servicios basados ​​en UDP (para TCP, debería haber un tipo de recvmmsmsg recvmmsmsg que también toma un descriptor de socket por artículo).
      • Los descriptores siempre deben configurarse como no bloqueantes y uno debe verificar EAGAIN incluso cuando se usa epoll porque existen situaciones excepcionales donde la epoll informes de epoll y una posterior lectura (o escritura) seguirán epoll . Este es también el caso de poll / select en algunos núcleos (aunque presumiblemente se ha corregido).
      • Con una implementación ingenua , la inanición de remitentes lentos es posible. Cuando se lee ciegamente hasta que EAGAIN se devuelve al recibir una notificación, es posible leer de manera indefinida nuevos datos entrantes de un remitente rápido mientras muere de hambre por completo a un remitente lento (mientras los datos sigan llegando lo suficientemente rápido, es posible que no vea EAGAIN durante bastante ¡mientras!). 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. Además, 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 había una llamada anterior).
        El comportamiento real y observable en el modo disparado por flancos está mucho más cerca de "activar el primer hilo que ha llamado epoll_wait , lo que indica que la actividad IO ha sucedido desde que alguien llamó epoll_wait o una función de lectura / escritura en el descriptor, y luego solo informa preparación para el próximo epoll_wait subprocesos o ya bloqueado en epoll_wait , para cualquier operación que ocurra después de que alguien haya llamado una función de lectura (o escritura) en el descriptor ". Tiene 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 lo que es waitable o alertable 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

libevent : 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. Los docs Linux y MSDN explican extensamente 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 lo sugiere, terminación , no 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, comienza la operación real, que con suerte no bloqueará (aparte de lo que cabría esperar, no existe 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 alegría, el estándar permite dos valores de error diferentes]).