Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Ez a dokumentum az egyidejűségi futtatókörnyezet több területére vonatkozó ajánlott eljárásokat ismerteti.
Szakaszok
Ez a dokumentum a következő szakaszokat tartalmazza:
Kooperatív szinkronizálási szerkezetek használata, ha lehetséges
Kerülje a hosszadalmas feladatokat, amelyek nem hoznak eredményt
Egyidejű memóriakezelési függvények használata, ha lehetséges
Az egyidejűségi objektumok élettartamának kezelése az RAII használatával
Ne hozzon létre egyidejűségi objektumokat a globális hatókörben
Ne használjon párhuzamossági objektumokat megosztott adatszegmensekben
Kooperatív szinkronizálási szerkezetek használata, ha lehetséges
Az egyidejűségi futtatókörnyezet számos olyan egyidejűségbiztos szerkezetet biztosít, amelyekhez nincs szükség külső szinkronizálási objektumra. Az egyidejűség::concurrent_vector osztály például egyidejűségbiztos hozzáfűzési és elemhozzáférési műveleteket biztosít. Itt az egyidejűség-biztonságos érték azt jelenti, hogy a mutató vagy az iterátor mindig érvényes. Ez nem garantálja az elemek inicializálását vagy egy adott bejárási sorrendet. Azokban az esetekben azonban, amikor kizárólagos hozzáférést igényel egy erőforráshoz, a futtatókörnyezet biztosítja a concurrency::critical_section, concurrency::reader_writer_lock, és concurrency::event osztályokat. Ezek a típusok együttműködően viselkednek; Ezért a feladatütemező át tudja helyezni a feldolgozási erőforrásokat egy másik környezetbe, amikor az első tevékenység az adatokra vár. Ha lehetséges, használja ezeket a szinkronizálási típusokat más szinkronizálási mechanizmusok helyett, például a Windows API által biztosítottak helyett, amelyek nem működnek együttműködően. További információ ezekről a szinkronizálási típusokról és egy példakódról: Szinkronizálási adatstruktúrák és szinkronizálási adatstruktúrák összehasonlítása a Windows API-val.
[Felső]
Kerülje a hosszadalmas feladatokat, amelyek nem hoznak eredményt
Mivel a feladatütemező együttműködően viselkedik, nem biztosít méltányosságot a tevékenységek között. Ezért egy tevékenység megakadályozhatja, hogy más tevékenységek elindulnak. Bár ez bizonyos esetekben elfogadható, más esetekben ez holtpontot vagy éhezést okozhat.
Az alábbi példa több feladatot hajt végre, mint a lefoglalt feldolgozási erőforrások száma. Az első feladat nem ad át időt a feladatütemezőnek, ezért a második feladat csak az első feladat befejezéséig kezdődik.
// 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();
}
Ez a példa a következő kimenetet hozza létre:
1: 250000000 1: 500000000 1: 750000000 1: 1000000000 2: 250000000 2: 500000000 2: 750000000 2: 1000000000
A két feladat közötti együttműködés többféleképpen is lehetővé válik. A hosszú ideig futó feladat során az egyik módja az, hogy időnként átengedjük azt a feladatütemezőnek. Az alábbi példa módosítja a task függvényt úgy, hogy meghívja a concurrency::Context::Yield metódust azzal a céllal, hogy átadja a végrehajtást a feladatütemezőnek, így egy másik feladat futhat.
// 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();
}
Ez a példa a következő kimenetet hozza létre:
1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000
A Context::Yield metódus csak egy másik aktív szálat hoz létre azon az ütemezőn, amelyhez az aktuális szál tartozik, egy egyszerűsített feladatot vagy egy másik operációsrendszer-szálat. Ez a módszer nem engedi az olyan munkát, amely úgy van ütemezve, hogy a concurrency::task_group vagy a concurrency::structured_task_group objektumban fusson, de még nem indult el.
Más módokon is lehetővé teheti a hosszú ideig futó feladatok közötti együttműködést. A nagy feladatokat kisebb altevékenységekre bonthatja. A túljelentkezést is engedélyezheti egy hosszadalmas feladat során. A túljelentkezés lehetővé teszi, hogy több szálat hozzon létre, mint a rendelkezésre álló hardverszálak száma. A túljelentkezés különösen akkor hasznos, ha egy hosszú feladat nagy mennyiségű késést tartalmaz, például adatokat olvas lemezről vagy hálózati kapcsolatról. Az egyszerűsített tevékenységekről és a túljelentkezésről további információt a Feladatütemezőben talál.
[Felső]
Túlfoglalás használata olyan műveletek ellensúlyozására, amelyek blokkolnak vagy nagy késést okoznak
A Konkurens Futtatókörnyezet olyan szinkronizálási primitíveket biztosít, mint például konkurens::critical_section, amelyek lehetővé teszik, hogy a feladatok együttműködve blokkolják és átadják a vezérlést egymásnak. Amikor egy feladat együttműködően blokkol vagy enged át, a feladatütemező áthelyezheti a feldolgozási erőforrásokat egy másik kontextusba, amíg az első feladat az adatokra vár.
Vannak olyan esetek, amikor nem használhatja az egyidejűségi futtatókörnyezet által biztosított együttműködési blokkoló mechanizmust. Előfordulhat például, hogy egy külső kódtár más szinkronizálási mechanizmust használ. Egy másik példa, amikor olyan műveletet hajt végre, amely nagy késéssel járhat, például amikor a Windows API ReadFile függvénnyel adatokat olvas be egy hálózati kapcsolatból. Ezekben az esetekben a túljelentkezés lehetővé teszi más tevékenységek futtatását, ha egy másik tevékenység tétlen. A túljelentkezés lehetővé teszi, hogy több szálat hozzon létre, mint a rendelkezésre álló hardverszálak száma.
Vegye figyelembe a következő függvényt, downloadamely letölti a fájlt a megadott URL-címre. Ez a példa a párhuzamosítás::Context::Oversubscribe metódust használja az aktív szálak számának ideiglenes növeléséhez.
// 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;
}
Mivel a GetHttpFile függvény potenciálisan látens műveletet hajt végre, a túljelentkezés lehetővé teszi más tevékenységek futtatását, miközben az aktuális tevékenység az adatokra vár. A példa teljes verziójáért lásd a következőt: Hogyan: Túlterhelés alkalmazása a késés ellensúlyozására.
[Felső]
Egyidejű memóriakezelési függvények használata, ha lehetséges
Használja a memóriakezelési függvényeket, concurrency::Alloc és concurrency::Free, ha aprólékos feladatokkal rendelkezik, amelyek gyakran kis tárgyakat allokálnak, amelyek viszonylag rövid élettartamúak. A párhuzamos futtatókörnyezet minden futó szálhoz külön memória gyorsítótárat tart fenn. A Alloc és Free függvények zárolások és memóriakorlátok nélkül foglalnak le és szabadítanak fel memóriát ezekből a gyorsítótárakból.
Ezekről a memóriakezelési függvényekről további információt a Feladatütemezőben talál. Az alábbi függvényeket használó példa : Útmutató: Az Alloc és a Free használata a memória teljesítményének javításához.
[Felső]
Az egyidejűségi objektumok élettartamának kezelése az RAII használatával
Az egyidejűségi futtatókörnyezet kivételkezeléssel valósít meg olyan funkciókat, mint a lemondás. Ezért írjon kivételbiztos kódot, amikor meghívja a futtatókörnyezetet, vagy egy másik könyvtárat, amely meghívja a futtatókörnyezetet.
Az erőforrás-beszerzés inicializálási (RAII) mintája az egyik módja annak, hogy egy adott hatókörben lévő konkurens objektum élettartamát biztonságosan kezeljük. A RAII-minta alatt egy adatstruktúra van lefoglalva a veremen. Ez az adatstruktúra inicializál vagy szerez be egy erőforrást, amikor létrejön, és megsemmisíti vagy felszabadítja az erőforrást az adatstruktúra megsemmisítésekor. A RAII-minta garantálja, hogy a destruktort a rendszer meghívja, mielőtt a beágyazási hatókör kilép. Ez a minta akkor hasznos, ha egy függvény több return utasítást tartalmaz. Ez a minta segít a kivételmentes kód írásában is. Amikor egy throw utasítás a verem kikapcsolását okozza, a rendszer meghívja a RAII-objektum destruktorát, ezért az erőforrás mindig helyesen törlődik vagy felszabadul.
A futtatókörnyezet több osztályt határoz meg, amelyeket az RAII mintázat használ, például konkurencia::critical_section::scoped_lock és konkurencia::reader_writer_lock::scoped_lock. Ezeket a segédosztályokat hatókörzárolásoknak nevezzük. Ezek az osztályok számos előnnyel járnak a párhuzamosság::critical_section vagy párhuzamosság::reader_writer_lock objektumok használatakor. Ezeknek az osztályoknak a konstruktora hozzáférést szerez a megadott critical_section vagy reader_writer_lock objektumhoz; a destruktor hozzáférést ad az objektumhoz. Mivel a hatókörrel rendelkező zárolás akkor szünteti meg automatikusan a kölcsönös kizárási objektumhoz való hozzáférést, amikor megsemmisül, ezért nem szükséges manuálisan feloldani a mögöttes objektum zárolását.
Vegye figyelembe a következő osztályt, amelyet egy külső kódtár határoz meg, accountezért nem módosítható.
// 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;
};
Az alábbi példa több tranzakciót hajt végre egy account objektumon párhuzamosan. A példa egy objektumot critical_section használ az account objektumhoz való hozzáférés szinkronizálására, mivel az account osztály nem egyidejűségbiztos. Minden párhuzamos művelet egy critical_section::scoped_lock objektumot használ annak biztosítására, hogy az critical_section objektum feloldva legyen, ha a művelet sikeres vagy sikertelen. Ha a számlaegyenleg negatív, a withdraw művelet kivétellel meghiúsul.
// 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;
}
}
Ez a példa a következő mintakimenetet hozza létre:
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
További példákért, amelyek a RAII-mintát használják az egyidejűségi objektumok élettartamának kezelésére, tekintse meg az Útmutató: A munka eltávolítása User-Interface szálból, Hogyan: A környezet osztály használata együttműködő szemafor megvalósításához, és Hogyan: Túlzott erőforrások kihasználása a késleltetés kompenzálásához.
[Felső]
Ne hozzon létre egyidejűségi objektumokat a globális hatókörben
Ha globális hatókörben hoz létre egyidejűségi objektumot, problémákat okozhat, például holtpontot vagy memóriahozzáférési szabálysértéseket okozhat az alkalmazásban.
Ha például létrehoz egy egyidejűségi futtatókörnyezet objektumot, a futtatókörnyezet létrehoz egy alapértelmezett ütemezőt, ha még nem lett egy létrehozva. A globális objektumépítés során létrehozott futtatókörnyezeti objektumok ennek megfelelően létrehozzák ezt az alapértelmezett ütemezőt. Ez a folyamat azonban belső zárolást használ, amely megzavarhatja az egyidejűségi futtatókörnyezeti infrastruktúrát támogató egyéb objektumok inicializálását. Ennek a belső zárolásnak szüksége lehet egy másik infrastruktúra-objektumra, amely még nem lett inicializálva, így holtpontot okozhat az alkalmazásban.
Az alábbi példa egy globális concurrency::Scheduler objektum létrehozását mutatja be. Ez a minta nem csak az osztályra, hanem az Scheduler egyidejűségi futtatókörnyezet által biztosított összes többi típusra is vonatkozik. Javasoljuk, hogy ne kövesse ezt a mintát, mert az váratlan viselkedést okozhat az alkalmazásban.
// 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()
{
}
Példák az objektumok létrehozásának Scheduler helyes módjára: Feladatütemező.
[Felső]
Ne használjon párhuzamos objektumokat megosztott adatszegmensekben
Az egyidejűségi futtatókörnyezet nem támogatja az egyidejűségi objektumok használatát egy megosztott adatszakaszban, például a data_seg#pragma irányelv által létrehozott adatszakaszban. A folyamathatárok között megosztott egyidejűségi objektumok inkonzisztens vagy érvénytelen állapotba helyezhetik a futtatókörnyezetet.
[Felső]
Lásd még
A párhuzamos futtatási környezet legjobb gyakorlatai
Párhuzamos minták kódtára (PPL)
Aszinkron ügynökök könyvtára
Feladatütemező
Szinkronizálási adatstruktúrák
Szinkronizálási adatstruktúrák összehasonlítása a Windows API-val
Útmutató: Az Alloc és a Free használata a memória teljesítményének javításához
Útmutató: Túl előfizetés használata a késleltetés csökkentéséhez
Útmutató: A Context osztály használata a kooperatív szemafor implementálásához
Útmutató: Hogyan távolítsuk el a munkát egy User-Interface szálból
Ajánlott eljárások a párhuzamos minták könyvtárában
Legjobb gyakorlatok az aszinkron ügynökök könyvtárában