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 concurrency::concurrent_vector clase proporciona seguridad de simultaneidad anexar y acceder a las operaciones de elemento.Sin embargo, en los casos que requieren acceso exclusivo a un recurso, el tiempo de ejecución proporciona el concurrency::critical_section, concurrency::reader_writer_lock, y concurrency::event las clases.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.

Top

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 siguiente resultado.

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 task función para llamar a la concurrency::Context::Yield método ceder la ejecución para el programador de tareas para que 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 el siguiente resultado.

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 puede informar al trabajo que está programado para ejecutarse un concurrency::task_group o concurrency::structured_task_group objeto pero aún no ha comenzado.

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).

Top

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

El tiempo de ejecución de simultaneidad proporciona las primitivas de sincronización, tales como concurrency::critical_section, que permiten a las tareas bloquear de manera cooperativa y dar prioridad a cada uno de los otros.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.Este ejemplo se utiliza la concurrency::Context::Oversubscribe método 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.

Top

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

Utilice las funciones de administración de memoria, concurrency::Alloc y concurrency::Free, cuando las tareas de personalización avanzada que frecuentemente 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.

Top

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 tiempo de ejecución 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 cuando se trabaja con concurrency::critical_section o concurrency::reader_writer_lock objetos.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.

Top

No crear objetos de simultaneidad en el ámbito global

Cuando se crea un objeto de simultaneidad en el ámbito global puede causar problemas tales como memoria o interbloqueo infracciones de acceso que se produzca en la aplicación.

Por ejemplo, cuando se crea un objeto en tiempo de ejecución de simultaneidad, el tiempo de ejecución crea a un programador predeterminado para si uno no se ha creado.Un objeto en tiempo de ejecución que se crea durante la construcción de objetos globales en consecuencia hará que el tiempo de ejecución crear a este planificador predeterminado.Sin embargo, este proceso toma un bloqueo interno, que puede interferir con la inicialización de otros objetos que admiten la infraestructura en tiempo de ejecución de simultaneidad.Este bloqueo interno podría requerirse por parte de otro objeto de infraestructura que aún no se ha inicializado y, por tanto, puede producir interbloqueo se producen en la aplicación.

En el ejemplo siguiente se muestra la creación de un global concurrency::Scheduler objeto.Este patrón se aplica no sólo a la Scheduler clase pero todos los demás tipos proporcionados por el tiempo de ejecución de simultaneidad.Se recomienda no sigue este patrón, ya que puede provocar un comportamiento inesperado 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).

Top

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.

Top

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

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

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

Conceptos

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

Procedimientos recomendados en la biblioteca de modelos paralelos

Procedimientos recomendados en la biblioteca de agentes asincrónicos

Otros recursos

Procedimientos recomendados del Runtime de simultaneidad