Procedimientos recomendados generales en 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 construcciones de sincronización cooperativa siempre que sea posible
Uso de funciones simultáneas de administración de memoria siempre que sea posible
Usar RAII para administrar la duración de los objetos de simultaneidad
No usar objetos de simultaneidad en segmentos de datos compartidos
Usar construcciones de sincronización cooperativa siempre que 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 anexión y de acceso a elementos seguras para simultaneidad. Aquí, seguro para la simultaneidad significa que los punteros e iteradores siempre son válidos. No es una garantía de inicialización de elementos ni de un orden transversal determinado. Sin embargo, para los casos en que se requiere acceso exclusivo a un recurso, el entorno de ejecución 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 más información sobre estos tipos de sincronización y un ejemplo de código, consulte Estructuras de datos de sincronización y Comparación de estructuras de datos de sincronización con la API de Windows.
[Arriba]
Evitar tareas largas que no produzcan
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 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 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 produce resultados para un trabajo que está programado para ejecutarse en un objeto concurrency::task_group o concurrency::structured_task_group, pero que 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 más información sobre las tareas ligeras y la suscripción excesiva, consulte Programador de tareas.
[Arriba]
Uso de oversubscription para las operaciones de desplazamiento que bloquean o tienen una latencia alta
El Runtime de simultaneidad proporciona tipos 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 ver la versión completa de este ejemplo, consulte Cómo: Usar la suscripción excesiva para compensar la latencia.
[Arriba]
Uso de funciones simultáneas de administración de memoria siempre que sea posible
Use las funciones de administración de memoria, concurrency::Alloc y concurrency::Free, cuando tenga tareas específicas que asignen a menudo objetos pequeños con 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 más información sobre estas funciones de administración de memoria, consulte Programador de tareas. Para ver un ejemplo que usa estas funciones, consulte Cómo: Usar Alloc y Free para mejorar el rendimiento de la memoria.
[Arriba]
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 patrón Resource Acquisition Is Initialization (RAII) 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 entorno de ejecución define varias clases que usan el patrón RAII, por ejemplo, concurrency::critical_section::scoped_lock y concurrency::reader_writer_lock::scoped_lock. Estas clases auxiliares se conocen como 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 ver ejemplos adicionales que usan el patrón RAII para administrar la duración de los objetos de simultaneidad, consulte 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.
[Arriba]
No crear objetos de simultaneidad en el ámbito global
Cuando se crea un objeto de simultaneidad en el ámbito global, pueden surgir problemas en la aplicación, como infracciones de acceso a la memoria o interbloqueo.
Por ejemplo, cuando se crea un objeto de runtime de simultaneidad, el runtime crea un programador predeterminado para el usuario si aún no se había creado. Un objeto en tiempo de ejecución creado durante la construcción de objetos globales provocará que el runtime cree este programador predeterminado. Sin embargo, este proceso utiliza un bloqueo interno, lo que puede interferir con la inicialización de otros objetos que admiten la infraestructura del runtime de simultaneidad. Otro objeto de la infraestructura que aún no se haya inicializado podría requerir este bloqueo interno y provocar, por tanto, un interbloqueo en la aplicación.
En el ejemplo siguiente se muestra la creación de un objeto concurrency::Scheduler global. Este patrón no se aplica solo a la clase Scheduler
, sino también al resto de tipos que proporciona el runtime de simultaneidad. Es recomendable que no siga 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 ver ejemplos de la forma correcta de crear objetos Scheduler
, consulte Programador de tareas.
[Arriba]
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.
[Arriba]
Consulte también
Procedimientos recomendados del Runtime de simultaneidad
Biblioteca de modelos de procesamiento paralelo (PPL)
Biblioteca de agentes asincrónicos
Programador de tareas
Estructuras de datos de sincronización
Comparación de estructuras de datos de sincronización con la API de Windows
Procedimiento para usar Alloc y Free para mejorar el rendimiento de la memoria
Procedimiento para usar la suscripción excesiva para compensar la latencia
Procedimiento para usar la clase Context para implementar un semáforo cooperativo
Tutorial: Quitar trabajo de un subproceso de la interfaz de usuario
Procedimientos recomendados en la biblioteca de modelos paralelos
Procedimientos recomendados en la biblioteca de agentes asincrónicos