Este artículo proviene de un motor de traducción automática.

Windows con C++

Temporizadores y E/S en el grupo de subprocesos

Kenny Kerr

Kenny KerrEn este sentido, mi entrega final en el grupo de subprocesos de Windows 7, voy a cubrir los dos restantes generadoras de devolución de llamada objetos proporcionados por la API. Hay aún más de lo que pude escribir sobre el grupo de subprocesos, pero después de cinco artículos que cubren prácticamente todas sus características, debe ser cómodo usando para alimentar sus aplicaciones con eficacia y eficiencia.

En mi agosto (msdn.microsoft.com/magazine/hh335066) y noviembre (msdn.microsoft.com/magazine/hh547107) columnas, describí trabajar y esperar objetos respectivamente. Un objeto de trabajo le permite presentar trabajos, en forma de una función, directamente al grupo de subprocesos de ejecución. La función se ejecutará en la mayor brevedad. Un objeto de espera indica el grupo de subprocesos a esperar para un objeto de sincronización de núcleo en su nombre y la cola de una función cuando se es señalado. Se trata de una alternativa escalable para las primitivas de sincronización tradicional y una alternativa eficiente para el sondeo. Sin embargo, hay muchos casos donde se requieren para ejecutar el código después de un intervalo determinado o en algún período ordinario temporizadores. Esto puede ser debido a una falta de apoyo de "ingreso" en algún protocolo Web o tal vez porque están implementando un protocolo de comunicaciones UDP-estilo y que necesita para administrar las retransmisiones. Afortunadamente, el grupo de subprocesos API proporciona un objeto timer para manejar todas estas situaciones de una manera eficiente y ahora familiar.

Objetos de temporizador

La función CreateThreadpoolTimer crea un objeto timer. Si la función se realiza correctamente, devuelve un puntero opaco que representa el objeto timer. Si se produce un error, devuelve un valor de puntero nulo y proporciona más información a través de la función GetLastError. Dado un objeto timer, la función CloseThreadpoolTimer informa que el grupo de subprocesos que puede liberarse el objeto. Si has estado siguiendo a lo largo en la serie, esto debe todo suena bastante familiar. Aquí es una clase de rasgos que puede utilizarse con la plantilla de clase handy unique_handle introduje en mi columna de julio de 2011 (msdn.microsoft.com/magazine/hh288076):

struct timer_traits
{
  static PTP_TIMER invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_TIMER value) throw()
  {
    CloseThreadpoolTimer(value);
  }
};
typedef unique_handle<PTP_TIMER, timer_traits> timer;

Ahora puedo usar el typedef y crear un objeto timer como sigue:

void * context = ...
timer t(CreateThreadpoolTimer(its_time, context, nullptr));
check_bool(t);

Como es habitual, el parámetro final opcionalmente acepta un puntero a un entorno que puede asociar el objeto timer con un entorno, como describí en mi columna de septiembre de 2011 (respecto­revista/hh416747). El primer parámetro es la función de devolución de llamada que se pondrán en cola para el grupo de subprocesos cada vez que el temporizador caduca. La devolución de llamada de temporizador se declara lo siguiente:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void * context, PTP_TIMER);

Para controlar cuándo y con qué frecuencia el temporizador caduca, utilice la función SetThreadpoolTimer. Naturalmente, su primer parámetro proporciona el objeto timer, pero el segundo parámetro indica el tiempo debido a que el temporizador debe caducar. Utiliza una estructura FILETIME para describir el tiempo absoluto o relativo. Si no está seguro de cómo funciona esto, os animo a leer la columna del mes pasado, cuando describí la semántica alrededor de la estructura FILETIME en detalle. Aquí es un ejemplo sencillo donde configurar el temporizador expira en cinco segundos:

union FILETIME64
{
  INT64 quad;
  FILETIME ft;
};
FILETIME relative_time(DWORD milliseconds)
{
  FILETIME64 ft = { -static_cast<INT64>(milliseconds) * 10000 };
  return ft.ft;
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Una vez más, si no está seguro acerca de cómo funciona la función relative_time, por favor, lea mi columna de noviembre de 2011. En este ejemplo, el temporizador caduca después de cinco segundos, momento en el que el grupo de subprocesos cola una instancia de la función de devolución de llamada its_time. Si no se toman medidas, no más devoluciones de llamada se pondrán en cola.

También puede utilizar SetThreadpoolTimer para crear un temporizador periódico que se cola una devolución de llamada en un intervalo regular. Aquí está un ejemplo:

auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 500, 0);

En este ejemplo, devolución de llamada del temporizador es cola primero después de cinco segundos y, a continuación, cada segundo semestre después de hasta que el objeto timer es restablecer o cerrado. A diferencia de lo debido, el período es simplemente especificado en milisegundos. Tenga en cuenta que un periódico temporizador cola una devolución de llamada después de transcurrido el plazo determinado, independientemente de cuánto tarda la devolución de llamada para ejecutar. Esto significa que es posible que varias devoluciones de llamada ejecutar simultáneamente, o superposición, si el intervalo es lo suficientemente pequeño o las devoluciones de llamada toman un tiempo suficientemente largo para ejecutar.

Si es necesario asegurar las devoluciones de llamada no se superponen, y el tiempo de inicio precisa para cada período no que importante, entonces un enfoque diferente para crear un temporizador periódico podría ser apropiado. En lugar de especificar un período en la llamada a SetThreadpoolTimer, simplemente restablece el temporizador en la devolución de llamada. De esta forma, puede asegurarse de que nunca se superpondrán las devoluciones de llamada. Si nada más, esto simplifica la depuración. Imagínese recorriendo una devolución de llamada de temporizador en el depurador sólo para encontrar que el grupo de subprocesos ya ha en cola unos pocos casos más mientras estaban analizando el código (o rellenando su café). Con este enfoque, no ocurrirá nunca. Aquí es lo que parece:

void CALLBACK its_time(PTP_CALLBACK_INSTANCE, void *, PTP_TIMER timer)
{
  // Your code goes here
  auto due_time = relative_time(500);
  SetThreadpoolTimer(timer, &due_time, 0, 0);
}
auto due_time = relative_time(5 * 1000);
SetThreadpoolTimer(t.get(), &due_time, 0, 0);

Como puede ver, el vencimiento inicial es cinco segundos y luego reinicio el tiempo debido a 500 ms al final de la devolución de llamada. He tomado ventaja del hecho de que la firma de devolución de llamada proporciona un puntero al objeto temporizador originarios, haciendo la tarea de restablecer el temporizador muy simple. También puede utilizar RAII para garantizar que la llamada a SetThreadpoolTimer se llama confiable antes de la devolución de llamada devuelve.

Puede llamar a SetThreadpoolTimer con un valor de puntero nulo para el debido tiempo detener cualquier vencimientos futuros temporizador que pueden resultar en más devoluciones de llamada. También necesitará llamar a WaitForThreadpool­TimerCallbacks para evitar cualquier raza condiciones. Por supuesto, objetos temporizador funcionan igualmente bien con grupos de limpieza, como se describe en mi columna de octubre de 2011.

Parámetro final del SetThreadpoolTimer puede ser un poco confuso porque la documentación se refiere a una "longitud de la ventana", así como un retraso. ¿Qué es eso todo? Esto es realmente una característica que afecta a la eficiencia energética y ayuda a reduce la capacidad general del consumo. Se basa en una técnica llamada temporizador coalescencia. Obviamente, la mejor solución es evitar totalmente temporizadores y utilizar en su lugar. Esto permite a los procesadores del sistema la mayor cantidad de tiempo de inactividad, lo que les anima a entrar en sus Estados de inactividad bajo consumo tanto como sea posibles. Todavía, si temporizadores son necesarios, temporizador coalescencia puede reducir el total consumo de energía al reducir el número de interrupciones de temporizador que se requieren. Temporizador coalescencia se basa en la idea de un "retraso tolerable" para la caducidad del temporizador. Dado cierto retraso tolerable, el núcleo de Windows puede ajustar el tiempo de caducidad real para coincidir con las alarmas existentes. Una buena regla general es definir el retraso a una décima parte del período de uso. Por ejemplo, si el temporizador debe expirar en 10 segundos, utilice un segundo de retraso, dependiendo de lo que es adecuado para su aplicación. Cuanto mayor sea el retardo, interrumpe la oportunidad más que el kernel tiene que optimizar su temporizador. Por otro lado, nada menos que 50 ms no será de mucha ayuda porque comienza a inmiscuirse en el intervalo de reloj del núcleo predeterminado.

Objetos de finalización de E/s

Ahora es el momento para mí introducir la joya de la rosca API de pool: el objeto de finalización de entrada/salida (E/s), o simplemente el objeto de I/O. Cuando introdujo por primera vez el grupo de subprocesos API, mencioné que el grupo de subprocesos está construido encima de la API de puerto de finalización de I/O. Tradicionalmente, implementar la I/O más escalable en Windows era posible sólo mediante la API de puerto de finalización de I/O. He escrito acerca de esta API en el pasado. Aunque no es particularmente difícil de usar, no era siempre que fácil de integrar con una aplicación otros subprocesos necesita. Gracias a la API de piscina de hilo, sin embargo, tiene lo mejor de ambos mundos con una única API para el trabajo, sincronización, temporizadores y ahora I/O, demasiado. El otro beneficio es que es realmente más intuitivo que utiliza la API de puerto de finalización de I/O, especialmente cuando se trata de manejar varios identificadores de archivo y varias operaciones superpuestas al mismo tiempo realizando superpuesto finalización de I/O con el grupo de subprocesos.

Como ya habrá adivinado, la función CreateThreadpoolIo crea un objeto de I/O y la función de CloseThreadpoolIo informa que el grupo de subprocesos que puede liberarse el objeto. Aquí es una clase de rasgos para la plantilla de la clase unique_handle:

struct io_traits
{
  static PTP_IO invalid() throw()
  {
    return nullptr;
  }
  static void close(PTP_IO value) throw()
  {
    CloseThreadpoolIo(value);
  }
};
typedef unique_handle<PTP_IO, io_traits> io;

La función CreateThreadpoolIo acepta un identificador de archivo, lo que implica que un objeto de I/O es capaz de controlar la I/O para un único objeto. Naturalmente, que objeto necesita soporte E/s superpuesta, pero esto incluye tipos de recursos populares tales como archivos de sistema de archivo, canalizaciones con nombre, zócalos y así sucesivamente. Permítanme demostrar con un ejemplo simple de espera para recibir un paquete UDP utilizando un socket. Para administrar el socket, usaré unique_handle con la siguiente clase de rasgos:

struct socket_traits
{
  static SOCKET invalid() throw()
  {
    return INVALID_SOCKET;
  }
  static void close(SOCKET value) throw()
  {
    closesocket(value);
  }
};
typedef unique_handle<SOCKET, socket_traits> socket;

A diferencia de las clases de rasgos que he mostrado hasta ahora, en este caso la función no válida no devuelve un valor de puntero nulo. Esto es debido a la función WSASocket, como la función CreateFile, utiliza un valor inusual para indicar un identificador no válido. Dada esta clase de rasgos y typedef, puedo crear un socket y objeto de I/O sencillamente:

socket s(WSASocket( ...
, WSA_FLAG_OVERLAPPED));
check_bool(s);
void * context = ...
io i(CreateThreadpoolIo(reinterpret_cast<HANDLE>(s.get()), io_completion, context, nullptr));
check_bool(i);

La función de devolución de llamada que indica la realización de cualquier operación de E/s se declara lo siguiente:

void CALLBACK io_completion(PTP_CALLBACK_INSTANCE, void * context, void * overlapped,
  ULONG result, ULONG_PTR bytes_copied, PTP_IO)

Los únicos parámetros para esta devolución de llamada deben estar familiarizados si has usado E/s superpuesta antes. Porque E/s superpuesta es por naturaleza asincrónica y permite la superposición de las operaciones de E/s, de ahí el nombre superpuesto i/o — tiene que haber una manera de identificar la operación de E/s particular que se ha completado. Este es el propósito del parámetro superpuesto. Este parámetro proporciona un puntero a la estructura OVERLAPPED o WSAOVERLAPPED que fue especificado cuando se inició una operación de E/s particular. El enfoque tradicional de una estructura OVERLAPPED de embalaje en una estructura más grande para colgar más datos de este parámetro puede seguir utilizándose. El parámetro superpuesto proporciona una manera de identificar la operación de E/s particular que se ha completado, mientras que el parámetro de contexto — como siempre — proporciona un contexto para el extremo de I/O, independientemente de cualquier operación en particular. Teniendo en cuenta estos dos parámetros, debe tener ningún problema en la coordinación del flujo de datos a través de la aplicación. El parámetro resultado indica si la operación superpuesta sucedió con el habitual ERROR_SUCCESS, o cero, que indica el éxito. Finalmente, el parámetro de bytes_copied obviamente indica cuántos bytes realmente fueron leídos o escritos. Un error común es asumir que realmente se copió el número de bytes solicitados. No cometer ese error: es la razón para la existencia de este parámetro.

La única parte del soporte para del grupo de subprocesos E/s que es un poco complicado es el manejo de la propia solicitud de E/s. Se encarga a este código correctamente. Antes de llamar a una función de iniciar alguna operación de E/s asincrónica, como ReadFile o WSARecvFrom, debe llamar a la función StartThreadpoolIo para que el grupo de subprocesos sepan que una operación de E/s está a punto de comenzar. El truco es que si la operación de E/s ocurre completar de forma sincrónica, entonces, deberá notificar al grupo de subprocesos de ello llamando a la función CancelThreadpoolIo. Tenga en cuenta que la realización de I/O no necesariamente equivale a éxito. Una operación de E/s puede tener éxito o fallar tanto de forma sincrónica o asincrónica. De cualquier manera, si la operación de E/s no notificará el puerto de finalización de su finalización, necesita saber el grupo de subprocesos. Aquí es lo que este aspecto en el contexto de la recepción de un paquete UDP:

StartThreadpoolIo(i.get());
auto result = WSARecvFrom(s.get(), ...
if (!result)
{
  result = WSA_IO_PENDING;
}
else
{
  result = WSAGetLastError();
}
if (WSA_IO_PENDING != result)
{
  CancelThreadpoolIo(i.get());
}

Como puede ver, comenzar el proceso llamando al StartThreadpoolIo para indicar el grupo de subprocesos que una operación de E/s está a punto de comenzar. Entonces pido WSARecvFrom para conseguir las cosas. Interpretar el resultado es la parte crucial. La WSARecvFrom función devuelve cero si la operación se completó correctamente, pero el puerto de finalización será aún notificado, para cambiar el resultado a WSA_IO_PENDING. Cualquier otro resultado de WSARecvFrom indica un fallo, con la excepción, por supuesto, de WSA_IO_PENDING, que significa simplemente que se ha iniciado con éxito la operación pero se completará más tarde. Ahora, simplemente llamar CancelThreadpoolIo si el resultado no está pendiente para mantener el grupo de subprocesos para acelerar. Diferentes extremos de I/O pueden proporcionar semánticas diferentes. Por ejemplo, E/s de archivos puede configurarse para evitar notificando el puerto de finalización de finalización sincrónico. Luego debe llamar a CancelThreadpoolIo según corresponda.

Como los otros generadores de devolución de llamada objetos en el grupo de subprocesos API, pendiente de las devoluciones de llamada para E/s objetos pueden cancelar mediante la función WaitForThreadpoolIoCallbacks. Sólo tenga en cuenta que esto será cancelar cualquier pendientes devoluciones de llamada, pero no cancelarlas pendientes de las operaciones de I/O. Aún necesitará utilizar la función apropiada para cancelar la operación para evitar las condiciones de carrera. Esto le permite segura libre de cualquier estructura OVERLAPPED y así sucesivamente.

Y eso es todo por el grupo de subprocesos API. Como he dicho, hay más podría escribir acerca de este potente API, pero teniendo en cuenta el recorrido detallado he proporcionado hasta el momento, estoy seguro de que estás bien en tu camino a usarlo para alimentar su próxima aplicación. Conmigo el próximo mes como sigo explorar Windows con C++.

Kenny Kerr es un artesano de software con una pasión por el desarrollo de Windows nativo. Llegar a él en kennykerr.ca.