Compartir a través de


Procedimientos recomendados generales con el Runtime de simultaneidad

En este documento se describen los procedimientos recomendados que se aplican a varias áreas del runtime de simultaneidad.

Secciones

Este documento contiene las siguientes secciones:

  • Usar las construcciones cooperativas de sincronización cuando sea posible

  • Evitar las tareas largas que no producen resultados

  • Usar la sobresuscripción para desplazar las operaciones que se bloquean o tienen alta latencia

  • Usar las funciones de administración de memoria simultáneas cuando sea posible

  • Usar RAII para administrar la duración de los objetos de simultaneidad

  • No crear objetos de simultaneidad en el ámbito global

  • No usar objetos de simultaneidad en segmentos de datos compartidos

Usar las construcciones cooperativas de sincronización cuando sea posible

El runtime de simultaneidad proporciona muchas construcciones seguras para simultaneidad que no requieren un objeto de sincronización externo. Por ejemplo, la clase Concurrency::concurrent_vector proporciona operaciones de anexación y de acceso del elemento seguras para simultaneidad. Sin embargo, para los casos donde se requiere acceso exclusivo a un recurso, el runtime proporciona las clases Concurrency::critical_section, Concurrency::reader_writer_lock y Concurrency::event. Estos tipos se comportan de forma cooperativa; por consiguiente, el programador de tareas puede reasignar los recursos de procesamiento a otro contexto mientras la primera tarea espera los datos. Cuando sea posible, use estos tipos de sincronización en lugar de otros mecanismos de sincronización, como los proporcionados por la API de Windows, que no se comportan de manera cooperativa. Para obtener más información sobre estos tipos de sincronización y un ejemplo de código, vea Estructuras de datos de sincronización y Comparar estructuras de datos de sincronización con la API de Windows.

[Ir al principio]

Evitar las tareas largas que no producen resultados

Dado que el programador de tareas se comporta de forma cooperativa, no es ecuánime entre las tareas. Por consiguiente, una tarea puede evitar que se inicien otras tareas. Aunque esto es aceptable en algunos casos, en otros puede producir un interbloqueo o un colapso.

En el siguiente ejemplo se realizan más tareas que el número de recursos de procesamiento asignados. La primera tarea no produce resultados en el programador de tareas y, por consiguiente, la segunda tarea no se inicia hasta que finaliza la primera tarea.

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

Este ejemplo produce el resultado siguiente:

1: 250000000
1: 500000000
1: 750000000
1: 1000000000
2: 250000000
2: 500000000
2: 750000000
2: 1000000000

Hay varias maneras de habilitar la cooperación entre las dos tareas. Una consiste en producir ocasionalmente resultados de una tarea de ejecución prolongada en el programador de tareas. En el ejemplo siguiente se modifica la función task para llamar al método Concurrency::Context::Yield, que realiza una ejecución en el programador de tareas, a fin de que se pueda ejecutar otra tarea.

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

Este ejemplo produce la salida siguiente:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

El método Context::Yield produce solo otro subproceso activo en el programador al que pertenece el subproceso actual, una tarea ligera u otro subproceso del sistema operativo. Este método no produce resultados para un trabajo que está programado para ejecutarse en un objeto Concurrency::task_group o Concurrency::structured_task_group, pero no se ha iniciado todavía.

Hay otras maneras de habilitar la cooperación entre las tareas de ejecución prolongada. Puede dividir una tarea larga en otras más pequeñas. También puede habilitar la sobresuscripción durante una tarea larga. La sobresuscripción le permite crear más subprocesos que el número de subprocesos de hardware disponibles. La sobresuscripción es especialmente útil cuando una tarea larga contiene mucha latencia, por ejemplo, al leer datos del disco o de una conexión de red. Para obtener más información sobre las tareas ligeras y la sobresuscripción, vea Programador de tareas (Runtime de simultaneidad).

[Ir al principio]

Usar la sobresuscripción para desplazar las operaciones que se bloquean o tienen alta latencia

El runtime de simultaneidad proporciona primitivos de sincronización, como Concurrency::critical_section, que permiten que las tareas se bloqueen de forma cooperativa y produzcan resultados entre sí. Cuando una tarea se bloquea de forma cooperativa o produce resultados, el programador de tareas puede reasignar los recursos de procesamiento a otro contexto mientras la primera tarea espera los datos.

Hay casos en los que no se puede usar el mecanismo de bloqueo cooperativo que el runtime de simultaneidad proporciona. Por ejemplo, una biblioteca externa que usa podría emplear un mecanismos de sincronización diferente. Otro ejemplo es el caso en el que realiza una operación que podría tener mucha latencia, por ejemplo, cuando se usa la función de la API de Windows ReadFile para leer datos de una conexión de red. En estos casos, la sobresuscripción puede permitir que otras tareas se ejecuten cuando otra tarea está inactiva. La sobresuscripción le permite crear más subprocesos que el número de subprocesos de hardware disponibles.

Considere la función siguiente, download, que descarga el archivo en la dirección URL dada. En este ejemplo se usa el método Concurrency::Context::Oversubscribe para aumentar temporalmente el número de subprocesos activos.

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());

   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

Dado que la función GetHttpFile realiza una operación potencialmente latente, la sobresuscripción puede permitir que otras tareas se ejecuten mientras la tarea actual espera los datos. Para obtener la versión completa de este ejemplo, vea Cómo: Usar la suscripción excesiva para compensar la latencia.

[Ir al principio]

Usar las funciones de administración de memoria simultáneas cuando sea posible

Use las funciones de administración de memoria, Concurrency::Alloc y Concurrency::Free, cuando tenga tareas específicas que con frecuencia asignan objetos pequeños que tienen una duración relativamente corta. El runtime de simultaneidad contiene una memoria caché independiente para cada subproceso en ejecución. Las funciones Alloc y Free asignan y liberan memoria de estas memorias caché sin el uso de bloqueos ni barreras de memoria.

Para obtener más información sobre estas funciones de administración de memoria, vea Programador de tareas (Runtime de simultaneidad). Para obtener un ejemplo en el que se usan estas características, vea Cómo: Usar Alloc y Free para mejorar el rendimiento de la memoria.

[Ir al principio]

Usar RAII para administrar la duración de los objetos de simultaneidad

El runtime de simultaneidad usa el control de excepciones para implementar características como la cancelación. Por consiguiente, escriba el código seguro para excepciones cuando se llama al runtime o a otra biblioteca que llama al runtime.

El modelo Resource Acquisition Is Initialization (RAII) –que viene a significar que la adquisición de un recurso es su inicialización– es una forma de administrar con seguridad la duración de un objeto de simultaneidad en un ámbito determinado. Bajo el modelo RAII, se asigna una estructura de datos en la pila. Esa estructura de datos se inicializa o adquiere un recurso cuando se crea, y destruye o libera ese recurso cuando se destruye la estructura de datos. El modelo RAII garantiza que se llama al destructor antes de que el ámbito de inclusión salga. Este modelo resulta útil cuando una función contiene varias instrucciones return. Este modelo también le ayuda a escribir código seguro para excepciones. Cuando una instrucción throw hace que la pila se desenrede, se llama al destructor del objeto RAII; por consiguiente, el recurso siempre se elimina o se libera correctamente.

El runtime define varias clases que usan el modelo RAII, por ejemplo, Concurrency::critical_section::scoped_lock y Concurrency::reader_writer_lock::scoped_lock. Estas clases auxiliares se denominan bloqueos con ámbito. Estas clases proporcionan varias ventajas al trabajar con objetos Concurrency::critical_section o Concurrency::reader_writer_lock. El constructor de estas clases adquiere el acceso al objeto critical_section o reader_writer_lock proporcionado; el destructor libera el acceso a ese objeto. Dado que un bloqueo con ámbito libera automáticamente el acceso a su objeto de exclusión mutua cuando se destruye; por consiguiente, no se desbloquea manualmente el objeto subyacente.

Considere la siguiente clase, account, que se define mediante una biblioteca externa y, por consiguiente, no se puede modificar.

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

En el siguiente ejemplo se realizan varias transacciones en un objeto account en paralelo. En el ejemplo se usa un objeto critical_section para sincronizar el acceso al objeto account porque la clase account no es segura para simultaneidad. Cada operación paralela usa un objeto critical_section::scoped_lock para garantizar que el objeto critical_section se desbloquea cuando la operación se realiza correctamente o tiene errores. Cuando el saldo de cuenta es negativo, la operación withdraw produce un error e inicia una excepción.

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

Este ejemplo genera la siguiente salida de ejemplo:

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
        negative balance: -76

Para obtener ejemplos adicionales en los que se usa el modelo RAII para administrar la duración de los objetos de simultaneidad, vea Tutorial: Quitar trabajo de un subproceso de la interfaz de usuario, Cómo: Usar la clase Context para implementar un semáforo cooperativo y Cómo: Usar la suscripción excesiva para compensar la latencia.

[Ir al principio]

No crear objetos de simultaneidad en el ámbito global

Si crea un objeto de simultaneidad en el ámbito global, puede aparecer un interbloqueo en la aplicación.

Cuando se crea un objeto del runtime de simultaneidad, el runtime crea un programador predeterminado si aún no se ha creado. Esto también se produce para un objeto del runtime que se crea durante la construcción del objeto global. Sin embargo, este proceso toma un bloqueo interno, que puede interferir con la inicialización de otros objetos que admiten la infraestructura del runtime de simultaneidad. Dado que otro objeto de la infraestructura que aún no se ha inicializado podría requerir este bloqueo interno, puede producirse un interbloqueo en la aplicación.

En el ejemplo siguiente se muestra la creación de un objeto global Concurrency::Scheduler. Este modelo no se aplica solo a la clase Scheduler, sino también a todos los demás tipos proporcionados por el runtime de simultaneidad. Se recomienda que no siga este modelo porque puede hacer que se produzca un interbloqueo en la aplicación.

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace Concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Para obtener ejemplos de la forma correcta de crear los objetos Scheduler, vea Programador de tareas (Runtime de simultaneidad).

[Ir al principio]

No usar objetos de simultaneidad en segmentos de datos compartidos

El runtime de simultaneidad no admite el uso de objetos de simultaneidad en una sección de datos compartidos, por ejemplo, una sección de datos que se crea mediante la directiva data_seg #pragma. Un objeto de simultaneidad que se comparte entre los límites del proceso puede colocar el runtime en un estado incoherente o no válido.

[Ir al principio]

Vea también

Tareas

Cómo: Usar Alloc y Free para mejorar el rendimiento de la memoria

Cómo: Usar la suscripción excesiva para compensar la latencia

Tutorial: Quitar trabajo de un subproceso de la interfaz de usuario

Conceptos

Procedimientos recomendados del Runtime de simultaneidad

Parallel Patterns Library (PPL)

Biblioteca de agentes asincrónicos

Programador de tareas (Runtime de simultaneidad)

Estructuras de datos de sincronización

Comparar estructuras de datos de sincronización con la API de Windows

Otros recursos

Cómo: Usar la clase Context para implementar un semáforo cooperativo

Procedimientos recomendados en la biblioteca de modelos paralelos

Procedimientos recomendados en la biblioteca de agentes asincrónicos

Historial de cambios

Fecha

Historial

Motivo

Marzo de 2011

Información adicional sobre la posibilidad de un interbloqueo en el ámbito global.

Comentarios de los clientes.