Excelente Lectura tomada del MSDN Magazine de Microsoft, redactado por Kenny Kerr quien es un experto en desarrollo nativo en Windows.

Cuando comienza un nuevo proyecto, ¿se pregunta si su programa será dependiente de cálculos o de E/S? Debería hacerlo. He descubierto que en la mayoría de los casos es lo uno o lo otro. Podría estar trabajando en una biblioteca de análisis a la que se entregan muchos datos y que mantiene un grupo de procesadores ocupados mientras lo reduce todo a un conjunto de agregados. Por el contrario, su código podría dedicar la mayor parte del tiempo a esperar que algo suceda, que los datos lleguen desde la red, que un usuario haga clic en algo o realice un movimiento complejo con los dedos. En este caso, los subprocesos en su programa no están haciendo mucho. Ciertamente, hay casos en que los programas están muy dedicados a la E/S y la computación. Pienso en el motor de base de datos SQL Server, pero es menos común en la programación informática de la actualidad. Por lo general, su programa tiene la tarea de coordinar el trabajo de los demás. Podría ser un servidor web o un cliente que se comunica con una base de datos SQL, ingresando algunos cálculos a la GPU o presentando algún contenido para que el usuario interactúe con él. Considerando todas estas diferentes situaciones, ¿cómo decide cuáles capacidades de subprocesos requiere su programa y cuáles bloques de creación de simultaneidad son necesarios o útiles? Bueno, esa es una pregunta difícil de responder en general y algo que deberá analizar cuando aborde un nuevo proyecto. Sin embargo, es útil comprender la evolución de los subprocesos en Windows y C++ para que pueda tomar una decisión informada basada en las opciones prácticas que están disponibles.

Naturalmente, los subprocesos no ofrecen un valor directo al usuario. Su programa no será más asombroso si usa el doble de subprocesos que otro programa. Lo que cuenta es lo que hace con esos subprocesos. Para ilustrar estas ideas y la forma en que los subprocesos han evolucionado con el tiempo, tomemos el ejemplo de leer algunos datos desde un archivo. Voy a omitir las bibliotecas de C y C++ porque su compatibilidad con E/S está dirigida mayormente a E/S sincrónica o de bloqueo y generalmente no es relevante, a menos que vaya a crear un programa de consola sencillo. Desde luego, no hay nada malo en eso. Algunos de mis programas favoritos son programas de consola que hacen una sola cosa y la hacen muy bien. De todas maneras, eso no es muy interesante así que lo dejaré atrás.

Un subproceso

Como punto de partida, comenzaré con la API de Windows y la bien conocida, y acertadamente llamada, función ReadFile. Antes de comenzar a leer el contenido de un archivo, necesito un identificador para el archivo, que ofrece la inmensamente eficaz función CreateFile:

  1. auto fn = L”C:\\data\\greeting.txt”;
  2. auto f = CreateFile(fn, GENERIC_READ,
  3.   FILE_SHARE_READ, nullptr,
  4.   OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
  5. ASSERT(f);

Para mantener la brevedad de los ejemplos, usaré las macros ASSERT y VERIFY como marcadores de posición para indicar dónde deberá agregar algún control de errores para administrar los errores informados por las diversas funciones de API. En este fragmento de código, la función CreateFile se usa para abrir el archivo más que para crearlo. La misma función se usa para ambas operaciones. Lo de crear que indica el nombre consiste más que nada en que se crea un objeto de archivo de kernel, no tanto en si se crea o no un archivo en el sistema de archivos. Los parámetros son bastante autoexplicativos y no muy pertinentes para este análisis, con la excepción del penúltimo, que le permite especificar un conjunto de marcadores y atributos que indican el tipo de comportamiento de E/S que usted necesita del kernel. En este caso usé la constante FILE_ATTRIBUTE_NORMAL, que simplemente indica que el archivo se debe abrir para una E/S sincrónica normal. Recuerde llamar a la función CloseHandle para liberar el bloqueo del kernel en el archivo cuando haya terminado con él. Una clase encapsuladora de identificadores, como la que describí en el artículo de julio de 2011, “C++ y la API de Windows” (msdn.microsoft.com/magazine/hh288076), resolverá el problema.

Ahora puedo continuar y llamar a la función ReadFile para que lea el contenido del archivo en la memoria:

  1. char b[64];
  2. DWORD c;
  3. VERIFY(ReadFile(f, b, sizeof(b), &c, nullptr));
  4. printf(“> %.*s\n”, c, b);

Como podría esperarse, el primer parámetro especifica el identificador para el archivo. Los siguientes dos describen la memoria en la que el contenido del archivo se debe leer. ReadFile también devolverá el número real de bytes copiados si hubieran menos bytes disponibles que los solicitados. El parámetro final solo se usa para E/S asincrónica, tema que retomaré en un momento. Es este ejemplo simplista, después sencillamente imprimo los caracteres que realmente se leyeron del archivo. Naturalmente, podría necesitar llamar a ReadFile varias veces si es necesario.

Dos subprocesos

Este modelo de E/S es fácil de aprender y de hecho bastante útil para muchos programas pequeños, especialmente para programas basados en consola. Pero no ajusta su escala muy bien. Si necesita leer dos archivos independientes al mismo tiempo, quizás para admitir a varios usuarios, necesitará dos subprocesos. No hay problema, para eso existe la función CreateThread. A continuación se muestra un sencillo ejemplo:

  1. auto t = CreateThread(nullptr, 0,
  2.   [] (void *) -> DWORD
  3. {
  4.   CreateFile/ReadFile/CloseHandle
  5.   return 0;
  6. },
  7. nullptr, 0, nullptr);
  8. ASSERT(t);
  9. WaitForSingleObject(t, INFINITE);

Aquí estoy usando una lambda sin estado en lugar de una función de devolución de llamada para representar el procedimiento del subproceso. El compilador de Visual C++ 2012 cumple con la especificación del lenguaje C++11 en que las lambdas sin estado deben ser implícitamente convertibles en punteros de función. Lo anterior es conveniente y el compilador de Visual C++ lo hace mejor al producir automáticamente la convención de llamada apropiada en la arquitectura x86, que presenta diversas convenciones de llamada.

La función CreateThread devuelve un identificador que representa un subproceso que a continuación espero para usar la función WaitForSingleObject. El subproceso mismo se bloquea mientras se lee el archivo. De esta forma puedo hacer que varios subprocesos realicen diferentes operaciones de E/S en conjunto. Después podría llamar a WaitForMultipleObjects para esperar hasta que hayan finalizado todos los subprocesos. Recuerde también llamar a la función CloseHandle para liberar los recursos relacionados con el subproceso en el kernel.

Sin embargo, esta técnica no ajusta la escala a más de un puñado de usuarios o archivos o el que fuera el vector de escalabilidad de su programa. Para ser claros, no es que varias operaciones de lectura pendientes no ajusten la escala. Es todo lo contrario. Es la sobrecarga de subprocesos y sincronización lo que acabará con la escalabilidad del programa.

De vuelta a un solo subproceso

Una solución a este problema es usar algo denominado E/S que se puede poner en alerta mediante llamadas de procedimiento asincrónicas (APC). En este modelo, su programa se basa en una cola de APC que el kernel asocia con cada subproceso. Las APC vienen con variedades para modo de kernel y modo de usuario. Es decir, el procedimiento, o la función, que está en cola podría corresponder a un programa en el modo de usuario o incluso a algún controlador en modo de kernel. La segunda es una forma sencilla para que el kernel permita al controlador ejecutar código en el contexto del espacio de direcciones del modo de usuario de un subproceso para que tenga acceso a su memoria virtual. Pero esta técnica también está disponible para programadores de modo de usuario. Debido a que la E/S de todos modos es fundamentalmente asincrónica en el hardware (y por consiguiente en el kernel), tiene sentido comenzar a leer el contenido del archivo y hacer que el kernel ponga en cola a una APC cuando finalmente se complete.

Para comenzar, los marcadores y atributos traspasados a la función CreateFile se deben actualizar para permitir que el archivo proporcione E/S superpuesta para que las operaciones en el archivo no sean serializadas por el kernel. Los términos asincrónico y superpuesto se usan de forma intercambiable en la API de Windows y tienen el mismo significado. De todos modos, se debe usar la constante FILE_FLAG_OVERLAPPED cuando se cree el identificador de archivo:

  1. auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,
  2.   OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);

Nuevamente, la única diferencia en este fragmento de código es que reemplacé la constante FILE_ATTRIBUTE_NORMAL con la constante FILE_FLAG_OVERLAPPED, pero la diferencia en tiempo de ejecución es enorme. Para realmente proporciona una APC que el kernel pueda poner en cola en el momento de completar la E/S, necesito usar la función alternativa ReadFileEx. Aunque ReadFile se puede usar para iniciar la E/S asincrónica, solo ReadFileEx le permite proporcionar una APC para llamar cuando se completa. A continuación el subproceso puede proseguir y hacer otro trabajo útil, tal vez iniciar operaciones asincrónicas, mientras la E/S se completa en el segundo plano.

Nuevamente, gracias a C++11 y Visual C++, se puede usar una lambda para representar la APC. El truco está en que la APC probablemente buscará acceder al búfer recientemente rellenado, pero este no es uno de los parámetros para la APC y debido a que solo se permiten lambdas sin estado, no puede usar la lambda para capturar la variable del búfer. La solución es colgar el búfer de la estructura OVERLAPPED (superpuesta), por así decirlo. Debido a que hay un puntero a la estructura OVERLAPPED a disposición de la APC, puede simplemente convertir el resultado en una estructura de su elección. La figura 1 ofrece un ejemplo sencillo.

Figura 1: E/S que se puede poner en alerta con APC

  1. struct overlapped_buffer
  2. {
  3.   OVERLAPPED o;
  4.   char b[64];
  5. };
  6. overlapped_buffer ob = {};
  7. VERIFY(ReadFileEx(f, ob.b, sizeof(ob.b),
  8.   &ob.o, [] (DWORD e, DWORD c,
  9.   OVERLAPPED * o)
  10. {
  11.   ASSERT(ERROR_SUCCESS == e);
  12.   auto ob = reinterpret_cast<overlapped_buffer *>(o);
  13.   printf(“> %.*s\n”, c, ob->b);
  14. }));
  15. SleepEx(INFINITE, true);

Además del puntero OVERLAPPED, también se proporciona un código de error a la APC como su primer parámetro y el número de bytes copias como el segundo. En algún punto, la E/S se completa, pero para que se ejecute la APC, se debe poner al mismo subproceso en un estado que se puede poner en alerta. La forma más sencilla de hacerlo es con la función SleepEx, que reactiva el subproceso en cuanto una APC se pone en cola y ejecuta cualquier APC antes de devolver el control. Desde luego que el subproceso no se puede suspender en absoluto si ya hay APC en la cola. También puede comprobar el valor devuelto de SleepEx para averiguar qué provocó que se reanudara. Incluso puede usar un valor de cero en lugar de INFINITE para vaciar la cola de APC antes de proceder sin demora.

Sin embargo, no es tan útil usar SleepEx y fácilmente puede llevar a programadores inescrupulosos a sondear en busca de APC, lo que nunca es una buena idea. Existe la posibilidad de que si usa una E/S asincrónica desde un solo subproceso, este también sea el bucle de mensaje de su programa. De cualquier forma, también puede usar la función MsgWaitForMultipleObjectsEx para esperar algo más que solo las APC y crear un tiempo de ejecución de un sol subproceso más atractivo para su programa. El posible inconveniente con las APC es que pueden incorporar algunos errores difíciles de entrada reiterada, así que téngalo presente.

Un subproceso por procesador

A medida que encuentre más cosas para que su programa realice, podría darse cuenta de que el procesador en el que se ejecuta el subproceso de su programa está cada vez más ocupado mientras que los restantes procesadores en el equipo están sin hacer nada esperando que algo suceda. Aunque las APC son prácticamente la forma más eficiente de realizar una E/S asincrónica, tienen la desventaja evidente de solo completarse en el mismo subproceso que inició la operación. Por lo tanto, el desafío es desarrollar una solución que pueda ajustar la escala a todos los procesadores disponibles. Podría concebir un diseño de su propia factura, tal vez coordinar el trabajo entre una serie de subprocesos con bucles de mensajes que se pueden poner en alerta, pero nada de lo que podría implementar se acerca al simple rendimiento y escalabilidad del puerto de terminación de E/S, en gran medida debido a su integración profunda con diferentes partes del kernel.

Aunque una APC permite que se completen operaciones de E/S asincrónica en un solo subproceso, un puerto de terminación permite que cualquier subproceso comience una operación de E/S y que los resultados sean procesados por un subproceso arbitrario. Un puerto de terminación es un objeto de kernel que usted crea antes de asociarlo con cualquier número de objetos de archivos, sockets, canalizaciones y más. El puerto de terminación expone una interfaz de cola en la que el kernel puede enviar un paquete de terminación en la cola cuando se complete la E/S y su programa pueda sacar el paquete de la cola en cualquier subproceso y proceso disponible según sea necesario. Incluso puede poner en cola sus propios paquetes de terminación si es necesario. La principal dificultad es evitar la API confusa. La figura 2 proporciona una clase de contenedor sencilla para el puerto de terminación, lo que aclara cómo se usan las funciones y cómo se relacionan.

Figura 2: el contenedor del puerto de terminación

  1. class completion_port
  2. {
  3.   HANDLE h;
  4.   completion_port(completion_port const &);
  5.   completion_port & operator=(completion_port const &);
  6. public:
  7.   explicit completion_port(DWORD tc = 0) :
  8.     h(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, tc))
  9.   {
  10.     ASSERT(h);
  11.   }
  12.   ~completion_port()
  13.   {
  14.     VERIFY(CloseHandle(h));
  15.   }
  16.   void add_file(HANDLE f, ULONG_PTR k = 0)
  17.   {
  18.     VERIFY(CreateIoCompletionPort(f, h, k, 0));
  19.   }
  20.   void queue(DWORD c, ULONG_PTR k, OVERLAPPED * o)
  21.   {
  22.     VERIFY(PostQueuedCompletionStatus(h, c, k, o));
  23.   }
  24.   void dequeue(DWORD & c, ULONG_PTR & k, OVERLAPPED *& o)
  25.   {
  26.     VERIFY(GetQueuedCompletionStatus(h, &c, &k, &o, INFINITE));
  27.   }
  28. };

La principal confusión está en torno a la doble labor que la función Create­IoCompletionPort realiza, que de hecho primero crea un objeto de puerto de terminación y después lo asocia con un objeto de archivo superpuesto. El puerto de terminación se crea una sola vez y después se asocia con cualquier número de archivos. Técnicamente, usted puede realizar ambos pasos en una sola llamada, pero esto es útil solo si usa el puerto de terminación con un solo archivo, y eso ¿qué gracia tiene?

Cuando cree el puerto de terminación, la única consideración es el último parámetro que indica el recuento de subprocesos. Este es el número máximo de subprocesos que se permitirán para quitar de la cola los paquetes de terminación simultáneamente. Configurar esto en cero significa que el kernel permitirá un subproceso por procesador.

Agregar un archivo se denomina técnicamente una asociación; el principal aspecto que se debe tener presente es el parámetro que indica la clave que se asociará con el archivo. Debido a que no se puede colgar información adicional del final del identificador como es el caso con una estructura OVERLAPPED, la clave ofrece una forma para que asocie información específica del programa con el archivo. Cada vez que el kernel pone en cola un paquete de terminación relacionado con este archivo, también se incluirá la clave. Lo anterior tiene especial importancia porque el identificador del archivo ni siquiera está incluido en el paquete de terminación.

Como ya señalé, puede poner en cola sus propios paquetes de terminación. En este caso, los valores que proporcione son una decisión completamente suya. Al kernel no le importan y no intentará interpretarlos de modo alguno. Por consiguiente, puede proporcionar un puntero OVERLAPPED falso y se almacenará exactamente la misma dirección en el paquete de terminación.

Sin embargo, en la mayoría de los casos, deberá esperar que el kernel ponga en cola el paquete de terminación una vez que se complete la operación de E/S asincrónica. Normalmente un programa crea uno o más subprocesos por procesador y llama a GetQueuedCompletionStatus o a mi función de contenedor para sacar de cola, en un bucle sin fin. Podría poner en cola un paquete de terminación de control especial, uno por subproceso, cuando su programa necesite llegar a un final y usted desee que estos subprocesos terminen. Al igual que con las APC, puede colgar más información de la estructura OVERLAPPED para asociar información adicional con cada operación de E/S:

  1. completion_port p;
  2. p.add_file(f);
  3. overlapped_buffer ob = {};
  4. ReadFile(f, ob.b, sizeof(ob.b), nullptr, &ob.o);

Aquí nuevamente uso la función ReadFile function original, pero en este caso proporciono un puntero a la estructura OVERLAPPED como su último parámetro. Un subproceso de espera podría quitar de la cola al paquete de terminación de la siguiente manera:

  1. DWORD c;
  2. ULONG_PTR k;
  3. OVERLAPPED * o;
  4. p.dequeue(c, k, o);
  5. auto ob = reinterpret_cast<overlapped_buffer *>(o);

Un grupo de subprocesos

Si ha estado siguiendo mi columna durante algún tiempo, recordará que el año pasado dediqué cinco meses a tratar el grupo de subprocesos de Windows en detalle. Tampoco le sorprenderá que esta misma API del grupo de subprocesos se implemente usando puertos de terminación de E/S, que proporcionan este mismo modelo de trabajo en cola, pero sin la necesidad de que usted administre los subprocesos. También ofrece muchas características y ventajas que la convierten en una atractiva alternativa al uso de un objeto de puerto de terminación directamente. Si todavía no lo ha hecho, le recomiendo que lea esas columnas para ponerse al día con la API del grupo de subprocesos de Windows. Una lista de mis columnas en línea se encuentra disponible en bit.ly/StHJtH.

Como mínimo, puede usar la función TrySubmitThreadpoolCallback para lograr que el grupo de subprocesos cree uno de sus objetos de trabajo de manera interna y hacer que la devolución de llamada se envíe a ejecución de inmediato. No podría ser más fácil que esto:

  1. TrySubmitThreadpoolCallback([](PTP_CALLBACK_INSTANCE, void *)
  2. {
  3.   // Work goes here!
  4. },
  5. nullptr, nullptr);

Si necesita un poco más de control, ciertamente puede crear un objeto de trabajo directamente y asociarlo con un entorno de grupo de subprocesos y grupo de limpieza. Esto también le ofrecerá el mejor rendimiento posible.

Desde luego, este análisis trata sobre E/S superpuesta y el grupo de subprocesos proporciona objetos de E/S justamente para eso. No me voy a detener mucho en esto, porque ya lo traté en detalle en mi columna de diciembre de 2011, “Temporizadores y E/S en el grupo de subprocesos” (msdn.microsoft.com/magazine/hh580731), pero la figura 3 ofrece un nuevo ejemplo.

Figura 3: E/S en el grupo de subprocesos

  1. OVERLAPPED o = {};
  2. char b[64];
  3. auto io = CreateThreadpoolIo(f, [] (PTP_CALLBACK_INSTANCE,
  4.   void * b,   void *, ULONG e, ULONG_PTR c, PTP_IO)
  5. {
  6.   ASSERT(ERROR_SUCCESS == e);
  7.   printf(“> %.*s\n”, c, static_cast<char *>(b));
  8. },
  9. b, nullptr);
  10. ASSERT(io);
  11. StartThreadpoolIo(io);
  12. auto r = ReadFile(f, b, sizeof(b), nullptr, &o);
  13. if (!r && ERROR_IO_PENDING != GetLastError())
  14. {
  15.   CancelThreadpoolIo(io);
  16. }
  17. WaitForThreadpoolIoCallbacks(io, false);
  18. CloseThreadpoolIo(io);

Dado que CreateThreadpoolIo me permite traspasar un parámetro de contexto adicional a la devolución de llamada en cola, no necesito colgar el búfer de la estructura OVERLAPPED, aunque desde luego podría hacerlo si fuera necesario. Los aspectos principales para tener en cuenta aquí son que StartThreadpoolIo se debe llamar antes de comenzar la operación de E/S asincrónica y CancelThreadpoolIo se debe llamar si la operación de E/S no se puede realizar o se completa en línea, por así decirlo.

Subprocesos rápidos y fluidos

Llevando el concepto de un grupo de subprocesos a nuevas alturas, la nueva API de Windows para aplicaciones de la Tienda Windows también ofrece una abstracción del grupo de subprocesos, aunque una mucho más simple con bastantes menos características. Afortunadamente, nada le impide usar un grupo de subprocesos alternativo apropiado para su compilador y sus bibliotecas. Ahora que si los encargados de la Tienda Windows lo aprobarán, es otra historia. De todas maneras, vale la pena mencionar el grupo de subprocesos para aplicaciones de la Tienda Windows e integra el patrón asincrónico personificado por la API de Windows para aplicaciones de la Tienda Windows.

Mediante el uso de las sofisticadas extensiones C++/CX proporciona una API relativamente simple para ejecutar código de forma asincrónica:

  1. ThreadPool::RunAsync(ref new WorkItemHandler([] (IAsyncAction ^)
  2. {
  3.   // Work goes here!
  4. }));

Sintácticamente esto es bastante sencillo. Incluso podemos esperar que se vuelva más sencillo en una versión futura de Visual C++ si el compilador puede generar automáticamente un delegado C++/CX de una lambda, por lo menos conceptualmente, al igual que lo hace en la actualidad para los punteros de función.

Aun así, esta sintaxis relativamente sencilla va en contra de una gran complejidad. A un alto nivel, ThreadPool es una clase estática, para tomar prestado un término del lenguaje C# y, por ende, no se puede crear. Ofrece algunas sobrecargas del método estático RunAsync y eso es todo. Cada una adopta por lo menos un delegado como primer parámetro. Aquí estoy construyendo el delegado con una lambda. Los métodos RunAsync también devuelven una interfaz IAsyncAction, que ofrece acceso a la operación asincrónica.

Como comodidad, esto funciona bastante bien y se integra armoniosamente en el modelo de programación asincrónica que dominante en la API de Windows para aplicaciones de la Tienda Windows. Por ejemplo, puede encapsular la interfaz IAsyncAction devuelta por el método RunAsync en una tarea de Biblioteca de modelos de procesamiento paralelo (PPL) y alcanzar un nivel de capacidad de composición similar a lo que describí en mis columnas de septiembre y octubre, “La búsqueda de sistemas asincrónicos eficientes y que admitan composición” (msdn.microsoft.com/magazine/jj618294) y “Volver al futuro con las funciones reanudables” (msdn.microsoft.com/magazine/jj658968).

Sin embargo, es útil y en cierta medida da que pensar darse cuenta de lo que este código aparentemente inofensivo realmente representa. En el centro de las extensiones C++/CX está un tiempo de ejecución basado en COM y su interfaz IUnknown. No es posible que tal modelo de objeto basado en interfaz pueda proporcionar métodos estáticos. Debe haber un objeto para que haya una interfaz, además de algún tipo de generador de clases para crear ese objeto, y de hecho lo hay.

Windows en tiempo de ejecución define algo denominado clase de tiempo de ejecución que es muy similar a la clase COM tradicional. Si usted es de la escuela antigua, podría definir la clase en un archivo IDL y ejecutarlo a través de un nueva versión del compilador MIDL específicamente apropiada para la tarea y generará archivos de metadatos.winmd y los correspondientes encabezados.

Una clase de tiempo de ejecución puede tener métodos de instancia y métodos estáticos, que se definen con interfaces independientes. La interfaz que contiene los métodos de instancia se convierte en la interfaz predeterminada de la clase y la interfaz que contiene los métodos estáticos se atribuye a la clase de tiempo de ejecución en los metadatos generados. En este caso la clase de tiempo de ejecución ThreadPool carece del atributo activatable y no tiene interfaz predeterminada, pero una vez creada, se puede realizar una consulta a la interfaz estática y a continuación, se puede llamar a esos métodos no tan estáticos. La figura 4 ofrece un ejemplo de lo que podría implicar. Tenga en mente que la mayoría de esto sería generado por el compilador, pero debería darle una buena idea de lo que realmente cuesta hacer que esa sencilla llamada al método estático ejecute un delegado de forma asincrónica.

Figura 4: el grupo de subprocesos de WinRT

  1. class WorkItemHandler :
  2.   public RuntimeClass<RuntimeClassFlags<ClassicCom>,
  3.   IWorkItemHandler>
  4. {
  5.   virtual HRESULT __stdcall Invoke(IAsyncAction *)
  6.   {
  7.     // Work goes here!
  8.     return S_OK;
  9.   }
  10. };
  11. auto handler = Make<WorkItemHandler>();
  12. HSTRING_HEADER header;
  13. HSTRING clsid;
  14. auto hr = WindowsCreateStringReference(
  15.   RuntimeClass_Windows_System_Threading_ThreadPool,
  16.   _countof(RuntimeClass_Windows_System_Threading_ThreadPool)
  17.   – 1, &header, &clsid);
  18. ASSERT(S_OK == hr);
  19. ComPtr<IThreadPoolStatics> tp;
  20. hr = RoGetActivationFactory(
  21.   clsid, __uuidof(IThreadPoolStatics),
  22.   reinterpret_cast<void **>(tp.GetAddressOf()));
  23. ASSERT(S_OK == hr);
  24. ComPtr<IAsyncAction> a;
  25. hr = tp->RunAsync(handler.Get(), a.GetAddressOf());
  26. ASSERT(S_OK == hr);

Sin duda esto está muy lejos de la relativa sencillez y eficiencia de llamar a la función TrySubmitThreadpoolCallback. Es útil comprender el costo de las abstracciones que use, aunque termine decidiendo que el costo se justifica considerando alguna medida de productividad. Permítanme verlo por partes brevemente.

El delegado WorkItemHandler realmente es una interfaz IWorkItemHandler basada en IUnknown con un solo método Invoke. La implementación de esta interfaz no es proporcionada por la API, sino más bien por el compilador. Esto tiene sentido porque proporciona un contenedor conveniente para cualquier variable capturada por la lambda y el cuerpo de la lambda residiría naturalmente dentro del método Invoke generado por el compilador. En este ejemplo, simplemente me baso en la clase de plantillas RuntimeClass de la Biblioteca de Windows en tiempo de ejecución (WRL) para que implemente a IUnknown. A continuación puede usar la útil función de plantillas Make para crear una instancia de mi WorkItemHandler. En el caso de lambdas sin estado y punteros de función, esperaría también que el compilador produjera una implementación estática con una implementación que no tiene ningún efecto de IUnknown para evitar la sobrecarga de asignación dinámica.

Para crear una instancia de la clase de tiempo de ejecución para llamar a la función RoGet­ActivationFactory. Sin embargo, necesita un Id. de clase. Observe que no es el CLSID de COM tradicional, sino más bien el nombre del tipo completamente calificado, en este caso, Windows.System.Threading.ThreadPool. Aquí uso un matriz de constantes generada por el compilador MIDL para evitar tener que contar la cadena en el tiempo de ejecución. Como si eso no fuera suficiente, también necesito crear una versión HSTRING de este Id. de clase. Aquí uso la función WindowsCreateStringReference, que, a diferencia de la función WindowsCreateString normal, no crea una copia de la cadena de origen. Por motivos de comodidad, WRL también proporciona la clase HStringReference que encapsula esta funcionalidad. Ahora puedo llamar a la función RoGetActivationFactory, que solicita la interfaz IThreadPoolStatics directamente y almacena el puntero resultante en un puntero inteligente proporcionado por WRL.

Ahora finalmente puedo llamar al método RunAsync en esta interfaz, proporcionándole mi implementación IWorkItemHandler, así como también la dirección de un puntero inteligente IAsyncAction que representa el objeto de acción resultante.

Por lo tanto, tal vez no sorprende que esta API del grupo de subprocesos proporcione mucho menos cantidad de funcionalidad y flexibilidad que las que ofrece la API del grupo de subprocesos principal de Windows o el tiempo de ejecución de simultaneidad. Sin embargo, el beneficio de C++/CX y las clases de tiempo de ejecución se obtiene en algún punto entre el programa y el tiempo de ejecución. Como programador de C++, puede agradecer que Windows 8 no sea una plataforma completamente nueva y que la API de Windows tradicional esté a su disposición, si y cuando la necesite.

Fuente: http://msdn.microsoft.com/es-co/magazine/jj883951.aspx