Compartir a través de


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

Operaciones simultáneas

Cuatro maneras de usar el tiempo de ejecución de simultaneidad en proyectos de C++

Rick Molloy

Descargar el código de ejemplo

A menudo me pregunté cómo integrar la nueva paralelo informática bibliotecas en la versión beta 2010 de Visual Studio en proyectos de C++ existentes. En esta columna, explicaré algunas de las formas de que utilizar las API y las clases que forman parte de la biblioteca paralela patrón (PPL), biblioteca de agentes asincrónico y tiempo de ejecución de simultaneidad en los proyectos existentes. Le guiaré a través de cuatro escenarios comunes de desarrollo de aplicaciones multiproceso se enfrentan los desarrolladores y describen cómo puede ser productivo inmediatamente, utilizando la PPL y la biblioteca de agentes para que el programa multiproceso más eficaz y más escalable.

Uno: Mover el trabajo de un subproceso de interfaz de usuario a una tarea de fondo

Una de las primera cosas que está dijo para evitar como desarrollador de Windows bloquea el subproceso de interfaz de usuario. Experiencia del usuario final de una ventana no responde es increíblemente desagradable independientemente de si los desarrolladores proporcionar a sus clientes un puntero de espera o de Windows los proporciona con una interfaz de usuario bloqueada en el formulario de una ventana de cristal frosted. Las instrucciones que se está dado suele ser bastante breve: No realizar ninguna llamada de bloqueo en el subproceso de interfaz de usuario, pero en su lugar mover estas llamadas a un subproceso en segundo plano. Mi experiencia es que esta guía no es suficiente y que el trabajo de codificación implicado tediosa, propensa y bastante desagradable.

En este escenario, proporcionaré un ejemplo de serie sencillo que muestra cómo mover trabajos utilizando la API de subprocesamiento existente. A continuación, proporcionaré dos enfoques para mover el trabajo a un subproceso de fondo utilizando la biblioteca de agentes y PPL. Se podrá ajustar esta situación con el uniendo los ejemplos a los detalles de un subproceso de interfaz de usuario.

Mover una operación serie de larga ejecución a un subproceso de fondo

Por lo tanto, ¿qué significa mover el trabajo a un subproceso en segundo plano? Si hay alguna función donde se ejecute un largo o potencialmente bloqueo de operación y deseo mover esa función a un subproceso en segundo plano, una buena cantidad de repetitivo código implicado en la mecánica de mover realmente ese trabajo, incluso para algo tan simple como una sola función llamar al como el que se muestra aquí:

void SomeFunction(int x, int y){
        LongRunningOperation(x, y);
}

En primer lugar, deberá empaquetar cualquier estado que se va a utilizarse. Aquí estoy simplemente empaquetar un par de enteros, lo que podría utilizar un contenedor integrado como un std:: vector, std::pair, o un std::tuple, pero normalmente más lo que he visto pendientes de la gente tiene paquete los valores en su propia estructura o clase, así:

struct LongRunningOperationParams{
        LongRunningOperationParams(int x_, int y_):x(x_),y(y_){}
        int x;
        int y;
}

Deberá crear una función global o estática que coincide con el grupo de subprocesos o firma de CreateThread, unpackages ese estado (normalmente por eliminar un void * puntero), ejecuta la función y, a continuación, elimina los datos si es necesario. Aquí se muestra un ejemplo:

DWORD WINAPI LongRunningOperationThreadFunc(void* data){
                LongRunningOperationParams* pData =
           (LongRunningOperationParams*) data;
                LongRunningOperation(pData->x,pData->y);
                //delete the data if appropriate
         delete pData;
}

Ahora puede obtener finalmente a la programación realmente el subproceso con los datos, lo que tiene este aspecto:

void SomeFunction(int x, int y){
        //package up our thread state
        //and store it on the heap
        LongRunningOperationParams* threadData =
        new  LongRunningOperationParams(x,y);
        //now schedule the thread with the data
        CreateThread(NULL,NULL,&LongRunningOperationThreadFunc,
          (void*) pData,NULL);
}

Esto puede no parecer como código mucho más. Técnicamente, he agregado sólo dos líneas de código para SomeFunction y cuatro líneas para nuestra clase de tres líneas para la función de subproceso. Pero eso es realmente cuatro veces mucho código. Fuimos de tres líneas de código a 12 líneas de código sólo para programar una llamada de función único con dos parámetros. La última vez que tuve que hacer algo parecido a esto, Creo que tenía aproximadamente ocho variables de captura y capturar y establecer todo este estado se convierte en bastante tediosa y propensa a errores. Si recuerdo correctamente, encontró y solucionó al menos dos errores sólo en el proceso de capturar el estado y generar el constructor.

También no toca en lo que tarda en esperar el subproceso complete, que normalmente implica crear un evento y una llamada a WaitForSingleObject para ese controlador de seguimiento y, por supuesto, limpiar el identificador cuando haya terminado con él. Que es al menos tres más líneas de código, y que aún se deja fuera de control de excepciones y códigos de retorno.

Alternativa a CreateThread: La clase task_group

El primer enfoque voy a describir es utilizar la clase task_group desde el PPL. Si no está familiarizado con la clase task_group, proporciona métodos para generar tareas asincrónicamente a través de task_group::run y esperando sus tareas completar a través de task_group::wait. También se proporciona la cancelación de las tareas que aún no ha iniciado y se incluye facilidades para empaquetar una excepción con std::exception_ptr y reinicios.

Verá que mucho menos código está implicado aquí que con el enfoque de CreateThread y desde una perspectiva de legibilidad, el código es muy parecido del ejemplo de la serie. El primer paso es crear un objeto task_group. Este objeto debe estar almacenado en algún lugar donde se puede administrar su duración, por ejemplo, en el montón o como una variable miembro en una clase. A continuación, usar task_group::run para programar una tarea (no un subproceso) para realizar el trabajo. Task_group::Run se toma un functor como un parámetro y se administra la duración de esa functor para usted. Utilizando una C ++ 0 x lambda para empaquetar el estado, éste es efectivamente un cambio de dos líneas al programa. Éste es el aspecto del código:

//a task_group member variable
task_group backgroundTasks;
void SomeFunction(int x, int y){
        backgroundTasks.run([x,y](){LongRunningOperation(x, y);});
}

Realizar trabajo asincrónico con la biblioteca de agentes

Otra alternativa es utilizar la biblioteca de agentes, que implica un enfoque basado en transferencia de mensajes. La cantidad de cambio de código es aproximadamente el mismo, pero hay una diferencia clave semántica merece la pena señalar con un enfoque basado en el agente. En lugar de programar una tarea, crear una canalización de transferencia de mensajes y asincrónicamente enviar un mensaje que contiene sólo los datos, confiar en la canalización para procesar el mensaje. En el caso anterior, se debería enviar un mensaje que contiene x e y. El trabajo todavía ocurre en otro subproceso, pero están en cola las llamadas posteriores a la misma canalización y se procesan los mensajes en orden (en contraste con task_group, que no ofrecen garantías de orden).

En primer lugar, tiene una estructura para contener el mensaje. De hecho, podría, utilice la misma estructura que el anterior, pero cambiará el nombre, tal como se muestra aquí:

struct LongRunningOperationMsg{
        LongRunningOperationMsg (int x, int y):m_x(x),m_y(y){}
        int m_x;
        int m_y;
}

El siguiente paso es declarar un lugar para enviar el mensaje. En la biblioteca de agentes, se puede enviar un mensaje a cualquier interfaz de mensaje "destino",pero en este caso particular más adecuado es llamada < T >. Una llamada < T >toma un mensaje y se construye con un functor que toma el mensaje como un parámetro. La declaración y construcción de la llamada a este aspecto (mediante lambdas):

call<LongRunningOperationMsg>* LongRunningOperationCall = new
   call<LongRunningOperationMsg>([]( LongRunningOperationMsg msg)
{
LongRunningOperation(msg.x, msg.y);
})

La modificación SomeFunction es ahora ligera. El objetivo es construir un mensaje y enviarlo al objeto de llamada asincrónica. Se invocará la llamada en un subproceso independiente cuando se recibe el mensaje:

void SomeFunction(int x, int y){
        asend(LongRunningOperationCall, LongRunningOperationMsg(x,y));
}

Obteniendo trabajo atrás hasta el subproceso de interfaz de usuario

Obteniendo trabajo desactivar el subproceso de interfaz de usuario es sólo la mitad el problema. Presumiblemente al final de LongRunningOperation vamos a obtener algunos resultado significativo y el siguiente paso es obtener a menudo trabajo nuevo en el subproceso de interfaz de usuario. El enfoque para tomar varía según la aplicación pero la forma más sencilla de lograr esto en las bibliotecas que ofrece Visual Studio 2010 es utilizar otro par de bloques de API y mensaje de la biblioteca de agentes: try_receive y unbounded_buffer < T >.

Un unbounded_buffer < T >puede utilizarse para almacenar un mensaje que contiene los datos y, potencialmente, el código que necesita para ejecutarse en el subproceso de interfaz de usuario. Try_receive es una llamada no sea de bloqueo de API que puede utilizarse para consultar si hay datos para mostrar.

Por ejemplo, si deseara representar imágenes en el subproceso de interfaz de usuario, podría utilizar código similar al siguiente para obtener datos volver en el subproceso de interfaz de usuario después de realizar una llamada a función InvalidateRect:

unbounded_buffer<ImageClass>* ImageBuffer;
LONG APIENTRY MainWndProc(HWND hwnd, UINT uMsg,
  WPARAM wParam, LPARAM lParam)
{
    RECT rcClient;
    int i;
    ...
   ImageClass image;
   //check the buffer for images and if there is one there, display it.
   if (try_receive(ImageBuffer,image))
       DisplayImage(image);
   ...
}

Aquí se han omitido algunos detalles, como la implementación del bucle de mensaje, pero espero que fue instructiva para demostrar la técnica de esta sección. Le animo a comprobar el código de ejemplo para el artículo, que tiene un completo ejemplo de trabajo de cada uno de estos enfoques.

Figura 1 A no-Thread-Safe clase

samclass
Widget{
size_t m_width;
size_t m_height;
public:
Widget(size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
return m_width;
}
size_t GetHeight(){
return m_height;
}
void SetWidth(size_t width){
m_width = width;
}
void SetHeight(size_t height){
m_height = height;
}
};

Dos: Administración de estado compartidas con bloques de mensajes y agentes

Otra situación común en desarrollo de aplicaciones multiproceso es administrar el estado compartido. Se convierte en concreto, tan pronto como intentar comunicarse o compartir datos entre subprocesos, administración del estado compartido rápidamente en un problema que tiene que tratar con. El enfoque a menudo he visto es simplemente agregar una sección crítica a un objeto para proteger a sus miembros de datos y las interfaces públicas, pero pronto se convierte en un problema de mantenimiento y a veces puede ser también un problema de rendimiento. En este escenario, le guiaré a través de un ejemplo de serie y ingenua mediante bloqueos y, a continuación, mostraré alternativa mediante los bloques de mensaje desde la biblioteca de agentes.

Bloqueo de una clase Widget Simple

de la figura 1 muestra una clase de Widget de no seguro para subprocesos con miembros de datos de alto, ancho y métodos sencillos que altera su estado.

El enfoque ingenua para hacer seguro el subproceso de la clase Widget es proteger a sus métodos con una sección crítica o bloqueo de lector y escritor. El PPL contiene un reader_writer_lock y de figura 2 ofrece un primer vistazo a la solución obvia al enfoque ingenua: utilizando el reader_writer_lock en el PPL.

Figura 2 mediante reader_writer_lock desde la biblioteca de Parallel patrón

class LockedWidget{
size_t m_width;
size_t m_height;
reader_writer_lock lock;
public:
LockedWidget (size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_width;
}
size_t GetHeight(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_height;
}
void SetWidth(size_t width){
auto lockGuard = reader_writer::scoped_lock(lock);
m_width = width;
}
void SetHeight(size_t height){
auto lockGuard = reader_writer::scoped_lock(lock)
m_height = height;
}
};

Lo he hecho aquí es agregar un read_writer_lock como una variable miembro y, a continuación, decorar todos los métodos adecuados con el lector o la versión de escritor del bloqueo. Estoy también mediante objetos scoped_lock para asegurarse de que el bloqueo no izquierdo mantenidos en el medio de una excepción. Todos los métodos Get ahora adquiera el bloqueo de lector, y los métodos set adquirir el bloqueo de escritura. Técnicamente, este enfoque parece es correcta, pero el diseño es realmente incorrecto y es frágil general porque sus interfaces, cuando se combinan, no son seguros para subprocesos. En concreto, si tengo el siguiente código, estoy probable que tenga estado dañado:

Thread1{
                SharedWidget.GetWidth();
                SharedWidget.GetHeight();
}
Thread2{
                SharedWidget.SetWidth();
                SharedWidget.SetHeight();
}

Porque las llamadas en Thread1 y Thread2 pueden se intercalan, Thread1 puede adquirir el bloqueo de lectura para GetWidth y, a continuación, antes de llamar a GetHeight, SetWidth y SetHeight podrían ambos ejecutar. Por lo tanto, además para proteger los datos, deberá asegurarse de que las interfaces para que los datos también son correctas;Éste es uno de los tipos más insidiosos de condiciones de anticipación, porque el código es correcto y los errores son muy difíciles de localizar. Naïve soluciones que he visto para esta situación suele implican la introducción a un método de bloqueo en el propio objeto, o peor, un bloqueo almacenado en algún lugar que los desarrolladores necesitan Recuerde que debe adquirir al tener acceso a ese widget. A veces se utilizan ambos enfoques.

Un enfoque más sencillo es asegurarse de que pueden se intercalan segura interfaces sin exponer esta capacidad para anular el estado del objeto entre llamadas intercaladas. Puede decidir evolucionar la interfaz, como se muestra en de figura 3 el fin de proporcionar métodos GetDimensions y UpdateDimensions. Esta interfaz ahora es menos probable que causen comportamiento sorprendente porque los métodos no permiten exponer intercalaciones unsafe.

Figura 3 una versión de la interfaz con GetDimensions y métodos de UpdateDimensions

struct WidgetDimensions
{
size_t width;
size_t height;
};
class LockedWidgetEx{
WidgetDimensions m_dimensions;
reader_writer_lock lock;
public:
LockedWidgetEx(size_t w, size_t h):
m_dimensions.width(w),m_dimensions.height(h){};
WidgetDimensions GetDimensions(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_dimensions;
}
void UpdateDimensions(size_t width, size_t height){
auto lockGuard = reader_writer::scoped_lock(lock);
m_dimensions.width = width;
m_dimensions.height = height;
}
};

Administración de estado compartidas con bloques de mensajes

Ahora vamos a tienen un vistazo a cómo la biblioteca de agentes puede facilitar administrar compartido más fácil de estado y el código un poco más robusto. Se llama a las clases de claves de la biblioteca de agentes que son útiles para administrar variables compartidas se overwrite_buffer < T >, que almacena un único valor actualizable y devuelve una copia de la última valor cuando recibe;se le llama single_assignment < T >, que almacena y devuelve una copia de un solo valor cuando reciben pero, como una constante, se puede asignar sólo una vez;y unbounded_buffer < T >, que almacena un número ilimitado de elementos (que permite la memoria) y, como una cola FIFO, dequeues el elemento más antiguo cuando se recibe se denomina.

Comenzaré con una overwrite_buffer < T >. En la clase Widget, va a reemplazar primero la variable de miembro m_dimensions con overwrite_buffer < WidgetDimensions > y, a continuación, podrá quitar los bloqueos explícitos de los métodos y reemplazarlos con el envío correspondiente y recibir llamadas. Aún necesito preocuparse acerca de nuestra interfaz está seguro, pero ya no es necesario acordarse de bloquear los datos. Aquí es este aspecto en el código. Es realmente un poco menos líneas de código de la versión bloqueada y el mismo número de líneas que la versión de serie:

class AgentsWidget{
    overwrite_buffer<WidgetDimensions> m_dimensionBuf;
public:
    AgentsWidget(size_t w, size_t h){
        send(&m_dimensionBuf,WidgetDimensions(w,h));
    };
    WidgetDimensions GetDimensions(){
        return receive(&m_dimensionBuf);
    }
    void UpdateDimensions(size_t width, size_t height){
        send(&m_dimensionBuf,WidgetDimensions(w,h));
    }
};

Es una sutil diferencia semántica aquí desde la implementación de bloqueo reader_writer. El overwrite_buffer permite una llamada a UpdateDimensions para que se producen durante una llamada a las dimensiones. Esto no permite prácticamente ningún bloqueo durante estas llamadas, pero una llamada a GetDimensions puede ser ligeramente actualizada. Merece la pena señalar que el problema existía en la versión bloqueada así, porque tan pronto como obtener las dimensiones, tienen el potencial de estar actualizada. Todo lo he hecho aquí es quitar la llamada de bloqueo.

Un unbounded_buffer también puede ser útil para la clase Widget. Imagine que la sutil diferencia semántica que acabo de describir era muy importante. Por ejemplo, si tiene una instancia de un objeto que desea asegurarse de acceso a sólo un subproceso a la vez, puede utilizar unbounded_buffer como un contenedor de objeto que administra el acceso a ese objeto. Para aplicar este a la clase Widget, puede quitar m_dimensions y reemplazarlo por unbounded_buffer < WidgetDimension >y utilizar este búfer a través de las llamadas a GetDimensions y UpdateDimensions. El desafío aquí es asegurarse de que nadie puede obtener un valor de nuestro widget mientras se actualiza. Esto se consigue al vaciar el unbounded_buffer para que las llamadas a GetDimension se bloquearán esperando que se produzca la actualización. Esto se puede observar en la figura 4. Bloque de GetDimensions y UpdateDimensions, esperando acceso exclusivo a la variable de dimensiones.

Figura 4 vaciar el Unbounded_Buffer

class AgentsWidget2{
unbounded_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget2(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
//get the current value
WidgetDimensions value = receive(&m_dimensionBuf);
//and put a copy of it right back in the unbounded_buffer
send(&m_dimensionBuf,value);
//return a copy of the value
return WidgetDimensions(value);
}
void UpdateDimensions (size_t width, size_t height){
WidgetDimensions oldValue = receive(&m_dimensionBuf);
send(&m_dimensionBuf,WidgetDimensions(width,height));
}
};

Realmente es acerca de la coordinación de acceso a los datos

Deseo resaltar algo más acerca de nuestra clase Widget: garantizar que los métodos y datos que se pueden tener acceso simultáneamente funcionan "segura"juntos es fundamental. A menudo, esto puede lograrse mediante coordinación acceso al estado en vez de los métodos de bloqueo o los objetos. En un "líneas de código" puroperspectiva, podrá ver una gran victoria sobre el ejemplo bloqueado y, en concreto, el segundo ejemplo puede incluso tener un poco más código. Sin embargo, lo que se adquiere, es un diseño más seguro, y con un poco planeada, puede a menudo modificar interfaces serie para que el estado interno del objeto no es "rasgado". En el ejemplo de Widget, hice esto utilizando bloques de mensajes y pude proteger ese estado de manera que resulta más seguro. Agregar métodos o funciones a la clase Widget en el futuro es menos probable destruir la sincronización interna que se ha configurado. Con un bloqueo de miembro, es bastante fácil simplemente olvidarse de bloquear el bloqueo cuando se agrega un método en una clase. Pero mover operaciones a un modelo de transferencia de mensajes y mediante como a menudo puede mantener el búfer de sobrescritura en modo natural de su datos y clases utiliza bloques de mensajes sincronizados.

Tres: Mediante combinable para Accumulations Local de subprocesos y la inicialización

El segundo escenario en el que nos protegido acceso a un objeto con bloqueos o bloques de mensajes funciona muy bien para objetos de peso mayor que se tiene acceso con poca frecuencia. Si al leer en el ejemplo que se cree que puede haber un problema de rendimiento si se han utilizado el widget sincronizado en un bucle estrecha (y paralelo), es probablemente correcto. Eso se debe proteger el estado compartido puede ser problemático y los algoritmos de propósito general completamente y objetos que comparten realmente el estado, hay Desafortunadamente mucha de opciones distinto para coordinar el acceso o introducir un bloqueo. Pero casi siempre puede encontrar una manera de refactorizar código o un algoritmo para reducir la dependencia de estado compartido y una vez realizado esto, unos modelos específicas pero comunes en el que un objeto llama a T combinable en < >en el archivo modelo paralelo puede ayudarle.

&Lt; T > combinablees un contenedor simultáneo que ofrece compatibilidad para tres casos de uso generales: manteniendo una variable local de subprocesos o realizar la inicialización local de subprocesos, operaciones asociativa binario (como suma, min y mezcla) en las variables local de subprocesos y combinarlos y visitar cada copia local de subprocesos con una operación (como splicing listas conjuntamente). En esta sección, le explique cada uno de estos casos y proporcione ejemplos de cómo utilizarlas.

Mantiene una variable local de subprocesos o realizar la inicialización de subproceso local

El primer caso de uso que mencioné para < T > combinableera para mantener una variable local de subprocesos. Es relativamente común para almacenar una copia local de subprocesos de estado global. Por ejemplo, en los rayo colorea trazadoras aplicaciones como en nuestro paquete de ejemplo (code.msdn.microsoft.com/concrtextras) o en los ejemplos de desarrollo paralelo con .NET 4.0 (code.msdn.microsoft.com/ParExtSamples) hay una opción para colorear cada fila por subproceso para visualizar el paralelismo. En la versión nativa de la demostración, esto se realiza mediante un objeto combinable que contiene el color de local de subprocesos.

Puede contener una variable local de subprocesos, por supuesto, mediante el almacenamiento local de subprocesos (TLS), pero hay algunas desventajas, principalmente la administración de vigencia y visibilidad y estos van lado en la mano. Para utilizar TLS, primero deberá asignar un índice con TlsAlloc, asignar el objeto y, a continuación, almacenar un puntero al objeto en el índice con TlsSetValue. A continuación, cuando el subproceso está saliendo, debe asegurarse de que se desasigna el objeto. (TlsFree se llama automáticamente.) No hacerlo una o dos veces por subproceso y garantizar no hay pérdidas de cualquier debido de anticipado sale o excepciones es que un reto, pero si la aplicación necesita decenas o cientos de estos elementos, un enfoque diferente es probable que mejor.

&Lt; T > combinablepuede utilizarse para contener un valor local de subprocesos, así, pero la duración de los objetos individuales está vinculada a la duración de la < T > combinableelemento y gran parte de la inicialización está automatizado. Tener acceso al valor local de subprocesos simplemente llamando al método combinable::local, que devuelve una referencia al objeto local. Mostramos un ejemplo utilizando task_group, pero esto puede hacerse con subprocesos Win32 así:

combinable<int> values;

auto task = [&](){
                values.local() = GetCurrentThreadId();
                printf("hello from thread: %d\n",values.local());
};

task_group tasks;

tasks.run(task);

//run a copy of the task on the main thread
task();

tasks.wait();

He mencionado que la inicialización local de subprocesos también se puede lograr con combinable. Si, por ejemplo, deberá inicializar una llamada de biblioteca en cada subproceso en el que se utiliza, puede crear una clase que realiza la inicialización en su constructor. A continuación, en el primer uso por subproceso, se realizará la llamada de biblioteca, pero se omitirán en los usos posteriores. Aquí se muestra un ejemplo:

class ThreadInitializationClass
{
public:
      ThreadInitializationClass(){
            ThreadInitializationRoutine();
      };
};

...
//a combinable object will initialize these
combinable<ThreadInitializationClass> libraryInitializationToken;
            
...
//initialize the library if it hasn't been already on this thread
ThreadInitializationClass& threadInit = libraryInitalizationToken.local();

Realización de reducción en un bucle paralelo

Otro escenario principal para el objeto combinable es realizar reducciones de local de subprocesos o accumulations local de subprocesos. En concreto, puede evitar que un tipo determinado de condición de anticipación cuando paralelo bucles o en recursiva recorridos paralelos con combinable. Éste es un increíblemente ingenua ejemplo que no se va a mostrar speed-ups. En el código siguiente se muestra un bucle simple que tenga el aspecto puede ser paralelo con parallel_for_each, excepto para tener acceso a la variable de suma:

 

int sum = 0;
for (vector<int>::iterator it = myVec.begin(); it != myVec.end(); ++it) {
    int element = *it;
    SomeFineGrainComputation(element);
    sum += element;
}

Ahora, en lugar de colocar un bloqueo en nuestro parallel_for_each, que destruye alguna posibilidad que tuvimos de speed-ups, podemos utilizar un objeto combinable para calcular las sumas de local de subprocesos:

combinable<int> localSums;
 
parallel_for_each(myVec.begin(),  myVec.end(), [&localSums] (int element) {
   SomeFineGrainComputation(element);
   localSums.local() += element;
});

Ahora correctamente se ha evitado la condición de carrera, pero tenemos una colección de sumas de subprocesos locales almacenados en el objeto localSums y es necesario extraer el valor final. Podemos hacer esto con el método combinar, que toma un binario functor similar al siguiente:

int sum = localSums.combine(std::plus<int>);

El tercer caso de uso para combinable < T >, que implica el uso del método combine_each, es cuando necesita visitar cada uno del copia local de subprocesos y realizar alguna operación en ellos (como limpieza o comprobación de errores). Más interesante, otro ejemplo es cuando su objeto combinable es una combinable < lista < T > > y en los subprocesos que está creando std::lists o std::sets. En el caso de std::lists, pueden fácilmente ser spliced junto con list::splice;con std::sets, pueden insertarse con set::insert.

Cuatro: Convertir un subproceso de fondo existente a un agente o una tarea

Suponga que ya tiene un subproceso de fondo o trabajo en la aplicación. Existen algunas razones muy buenas por qué puede desear convertir ese subproceso de fondo a una tarea desde la PPL o a un agente y es relativamente sencillo. Algunas de las principales ventajas de hacerlo son:

Composability y rendimiento. Si los subprocesos de trabajo se calculan mucho y piensa utilizar subprocesos adicionales en el PPL o la biblioteca de agentes, convertir el subproceso de fondo a una tarea de trabajo permite cooperan con otras tareas en tiempo de ejecución y evitar oversubscription en el sistema.

Cancelación y control de excepciones. Si desea poder cancelar el trabajo en un subproceso o dispone de un mecanismo well-described para controlar excepciones fácilmente, un task_group tiene estas capacidades integradas.

Administración de flujo y estado del control. Si necesita administrar el estado de su subproceso (iniciado o finalizado para ejemplo) o tiene un objeto cuyo estado es inseparable de forma eficaz desde el subproceso de trabajo, puede ser útil implementar a un agente.

Cancelaciones de ofertas de Task_group y excepciones

En el primer escenario, hemos explorado lo necesario para programar el trabajo con un task_group: empaquetar esencialmente su trabajo en un functor (mediante una lambda, una std::bind o un objeto de función personalizada) y la programación con el método task_group::run. Lo que no describo era la cancelación y la semántica de control de excepciones, que es, de hecho, relacionada con.

Figura 5 implementación de MyAgentClass

class MyAgentClass : public agent{
public:
MyAgentClass (){
}
AgentsWidget widget;
void run(){
//run is started asynchronously when agent::start is called
//...
//set status to complete
agent::done();
}
};

En primer lugar, explicaré la semántica sencilla. Si el código realiza una llamada a task_group::cancel o una tarea produce una excepción no detectada, cancelación es en efecto para esa task_group. Cuando cancelación está en efecto, las tareas que aún no ha iniciado en ese task_group no iniciarse, que permite el trabajo programado con rapidez y facilidad debe cancelarse de un task_group. Cancelación no interrumpe tareas que se está ejecutando o bloqueados, por lo que una tarea en ejecución puede consultar el estado de cancelación con el método task_group::is_canceling o por la función auxiliar

is_current_task_group_canceling. Aquí es un breve ejemplo:

task_group tasks;   
tasks.run([](){
    ...
    if(is_current_task_group_canceling())
    {
        //cleanup then return
        ...
        return;
    }
});
tasks.cancel();
tasks.wait();

Control de excepciones afecta a la cancelación porque una excepción no detectada en un task_group desencadena cancelación en ese task_group. Si hay una excepción no detectada, la task_group utilizará realmente std::exception_ptr para empaquetar la excepción en el subproceso que se ha producido. Posteriormente, cuando se llama a task_group::wait, la excepción se vuelve a producir en el subproceso que llama espera.

Implementar a un agente asincrónico

La biblioteca de agentes se ofrece una alternativa al uso un task_group: reemplazar un subproceso con la clase base del agente. Si el subproceso tiene mucha estado específico del subproceso y objetos, un agente podría ser un mejor ajuste para el escenario. La clase abstracta agente es una implementación de la trama actor;el uso previsto es implementar su propia clase derivada de agente y, a continuación, encapsular cualquier estado que puede tener su actor (o subproceso) en ese agente. Si hay campos que pretenden ser accesible públicamente, la orientación es exponer como bloques de mensajes o orígenes y destinos y utilizar pasar a comunicarse con el agente de mensaje.

Implementar a un agente requiere derivar una clase de la clase base del agente y, a continuación, reemplazar la ejecución del método virtual. A continuación, se puede iniciar el agente llamando agent::start, que genera el método de ejecución como una tarea, al igual que un subproceso. La ventaja es que ahora se puede almacenar estado local de subprocesos en la clase. Esto permite la sincronización más fácil de estado entre subprocesos, especialmente si el estado se almacena en un bloque de mensaje. de la figura 5 muestra un ejemplo de una implementación que tiene una variable miembro expuesto públicamente del tipo AgentsWidget.

Tenga en cuenta que he establecido estado del agente hacerlo como el método de ejecución está saliendo. Esto permite al agente no sólo se inicia, pero también se esperó. Además, el estado actual del agente puede consultarse mediante una llamada a agent::status. Iniciar y esperando en nuestra clase de agente es sencillo, como se muestra en el siguiente código:

 

MyAgentClass MyAgent;

//start the agent
MyAgent.start();

//do something else
...

//wait for the agent to finish
MyAgent.wait(&MyAgent);

Elemento de la bonificación: Ordenación en paralelo con parallel_sort

Por último, me gustaría sugerir otro punto potencialmente fácil de paralelización, esta vez no desde el PPL o la biblioteca de agentes, pero desde nuestro paquete de ejemplo disponible en code.msdn.microsoft.com/concrtextras. Quicksort paralelo es uno de los ejemplos se utiliza para explicar cómo establecer paralelismos en algoritmos recursivos divide y vencerás con tareas y el paquete de ejemplo contiene una implementación de quicksort paralelo. Ordenación paralelo puede mostrar speed-ups si está ordenando un gran número de elementos donde la operación de comparación es bastante costosa, como con cadenas. Probablemente no mostrará speed-ups para números pequeños de elementos o al ordenar los tipos integrados como enteros y dobles. Aquí es un ejemplo de cómo puede utilizarse:

//from the sample pack
#include "parallel_algorithms.h"
int main()

using namespace concurrency_extras;
{
                vector<string> strings;
 
                //populate the strings
                ...
                parallel_sort(strings.begin(),strings.end());
}

Conclusión

Espero que esta columna le ayuda a expandir horizontes de cómo las bibliotecas paralelas en Visual Studio 2010 se aplicará a los proyectos más allá simplemente utilizando parallel_for o tareas para acelerar el cálculo intensivo bucles. En nuestra documentación en MSDN encontrará muchos otros ejemplos instructivos (msdn.microsoft.com/library/dd504870(VS.100).aspx) y en nuestro paquete de ejemplo (code.msdn.microsoft.com/concrtextras) que ayudan a ilustrar las bibliotecas paralelas y cómo pueden utilizarse. Le animo a desprotegerlos.

Rick Molloy es un administrador de programas en el equipo Parallel Computing Platform de Microsoft.