2011-12-21 5 views
8

Escribo un servidor multihebra compatible con POSIX en c/C++ que debe poder aceptar, leer y escribir en una gran cantidad de conexiones asincrónicamente El servidor tiene varios subprocesos de trabajo que realizan tareas y ocasionalmente (e impredeciblemente) datos de cola para escribir en los sockets. Los clientes también escriben ocasionalmente (e impredeciblemente) datos en los sockets, por lo que el servidor también debe leer de forma asincrónica. Una forma obvia de hacerlo es dar a cada conexión un hilo que lea y escriba desde/hasta su socket; sin embargo, esto es feo, ya que cada conexión puede persistir durante mucho tiempo y, por lo tanto, el servidor puede tener que contener cientos o miles de hilos solo para realizar un seguimiento de las conexiones.Esperando una condición (pthread_cond_wait) y un cambio de socket (seleccionar) simultáneamente

Un mejor enfoque sería tener un solo hilo que manejara todas las comunicaciones utilizando las funciones select()/pselect(). Es decir, un único subproceso espera que cualquier socket sea legible, luego genera un trabajo para procesar la entrada que será manejada por un conjunto de otros subprocesos siempre que la entrada esté disponible. Cada vez que los otros subprocesos de trabajo producen salida para una conexión, se pone en cola, y el subproceso de comunicación espera que ese socket sea grabable antes de escribirlo.

El problema con esto es que el hilo de comunicación puede estar esperando en la función select() o pselect() cuando la salida está en cola por los subprocesos de trabajo del servidor. Es posible que, si no llega ninguna entrada durante varios segundos o minutos, un fragmento de salida en cola simplemente esperará a que se complete el hilo de comunicación select() ing. Esto no debería suceder, sin embargo, los datos deben escribirse lo antes posible.

Ahora mismo veo un par de soluciones que son seguras para subprocesos. Una de ellas es tener el hilo de comunicación ocupado: espere la entrada y actualice la lista de tomas que espera para escribir cada décima de segundo aproximadamente. Esto no es óptimo ya que implica esperar ocupado, pero funcionará. Otra opción es usar pselect() y enviar la señal USR1 (o algo equivalente) siempre que se haya puesto en cola la salida nueva, permitiendo que el hilo de comunicación actualice la lista de sockets que está esperando para el estado de escritura inmediatamente. Prefiero este último aquí, pero todavía no me gusta usar una señal para algo que debería ser una condición (pthread_cond_t). Otra opción más sería incluir, en la lista de descriptores de archivos en los que está esperando select(), un archivo ficticio en el que escribamos un solo byte cada vez que se necesite agregar un socket al fd_set escribible para select(); esto despertaba el servidor de comunicaciones porque ese archivo ficticio particular sería entonces legible, lo que permite que el hilo de comunicaciones actualice inmediatamente su escritura fd_set.

Siento intuitivamente que el segundo enfoque (con la señal) es la forma "más correcta" de programar el servidor, pero tengo curiosidad si alguien sabe cuál de los anteriores es el más eficiente, en términos generales, si alguno de los anteriores causará condiciones de carrera que no conozco, o si alguien sabe de una solución más general a este problema. Lo que realmente quiero es una función pthread_cond_wait_and_select() que permita que el hilo de comunicación espere un cambio en los sockets o una señal de una condición.

Gracias de antemano.

Respuesta

6

Este es un problema bastante común.

Una solución que se utiliza con frecuencia es tener tuberías como mecanismo de comunicación desde los hilos de trabajo hasta el hilo de E/S. Una vez completada su tarea, un hilo de trabajo escribe el puntero al resultado en la tubería. El subproceso de E/S espera en el extremo de lectura del conducto junto con otros sockets y descriptores de archivo y, una vez que el conducto está listo para leerse, se activa, recupera el puntero al resultado y procede a insertar el resultado en la conexión del cliente -modo de bloqueo.

Tenga en cuenta que, dado que las lecturas y escrituras de tuberías de menos o igual a PIPE_BUF son atómicas, los punteros se escriben y leen de una vez. Uno puede incluso tener múltiples hilos de trabajo escribiendo punteros en el mismo conducto debido a la garantía de atomicidad.

3

Su segundo enfoque es la manera más limpia de hacerlo. Es totalmente normal que las cosas como select o epoll incluyan eventos personalizados en su lista. Esto es lo que hacemos en mi proyecto actual para manejar dichos eventos. También utilizamos temporizadores (en Linux timerfd_create) para eventos periódicos.

En Linux, el eventfd le permite crear dichos eventos de usuario arbitrarios para este fin; por lo tanto, diría que es una práctica bastante aceptada. Para las funciones solo de POSIX, bueno, hmm, tal vez uno de los comandos de tubería o socketpair que también he visto.

Las encuestas ocupadas no son una buena opción.Primero escaneará la memoria que será utilizada por otros hilos, lo que provocará la contención de la memoria de la CPU. En segundo lugar, siempre tendrá que volver a su llamada select que creará una gran cantidad de llamadas al sistema e interruptores de contexto que dañarán el rendimiento general del sistema.

3

Desafortunadamente, la mejor manera de hacerlo es diferente para cada plataforma. La forma canónica y portátil de hacerlo es tener su bloque de hilos de E/S en poll. Si necesita que el subproceso de E/S se salga de poll, envía un byte único en un pipe que el hilo está sondeando. Eso hará que el hilo salga de poll inmediatamente.

En Linux, epoll es la mejor manera. En los sistemas operativos derivados de BSD (incluido OSX, creo), kqueue. En Solaris, solía ser /dev/poll y ahora hay algo más cuyo nombre olvido.

Es posible que desee considerar el uso de una biblioteca como libevent o Boost.Asio. Le brindan el mejor modelo de E/S en cada plataforma compatible.

Cuestiones relacionadas