Megosztás:


Kivételkezelés az egyidejűségi futtatókörnyezetben

Az egyidejűségi futtatókörnyezet C++ kivételkezelést használ számos hiba közléséhez. Ezek a hibák közé tartozik a futtatókörnyezet érvénytelen használata, a futásidejű hibák, például az erőforrás beolvasásának meghiúsulása, valamint a tevékenységeknek és tevékenységcsoportoknak megadott munkahelyi függvényekben előforduló hibák. Amikor egy feladat vagy feladatcsoport kivételt dob, a futásidejű környezet megtartja a kivételt, és azt a környezetbe továbbítja, amely a feladat vagy feladatcsoport befejezésére vár. Az olyan összetevők esetében, mint az egyszerűsített feladatok és az ügynökök, a futtatókörnyezet nem kezeli a kivételeket. Ezekben az esetekben saját kivételkezelési mechanizmust kell implementálnia. Ez a témakör azt ismerteti, hogy a futtatókörnyezet hogyan kezeli a tevékenységek, feladatcsoportok, egyszerűsített feladatok és aszinkron ügynökök által kidobott kivételeket, és hogyan reagálhat az alkalmazások kivételeire.

Kulcsfontosságú pontok

  • Amikor egy feladat vagy feladatcsoport kivételt dob, a futásidejű környezet megtartja a kivételt, és azt a környezetbe továbbítja, amely a feladat vagy feladatcsoport befejezésére vár.

  • Ha lehetséges, vegye körül az egyidejűség minden hívását a concurrency::task::get és concurrency::task::wait függvényekhez egy try/catch blokkal, hogy kezelhesse azokat a hibákat, amelyekből helyre tud állni. A futtatókörnyezet leállítja az alkalmazást, ha egy feladat kivételt dob, és ezt a kivételt a feladat, annak egyik folytatása vagy a fő alkalmazás nem kapja el.

  • A tevékenységalapú folytatások mindig futnak; nem számít, hogy az előzményfeladat sikeresen befejeződött-e, kivételt jelzett vagy megszakított. Az értékalapú folytatás nem fut, ha az előzményfeladat elvet vagy megszakít.

  • Mivel a feladat-alapú folytatások automatikusan lefutnak, fontolja meg, hogy a folytatási lánc végén adjon-e hozzá egy ilyen folytatást. Ezzel garantálható, hogy a kód minden kivételt betart.

  • A futtatókörnyezet egyidejűség::task_canceled kivételt dob, amikor egyidejűség::task::get hívást végrehajt és az a feladat megszakításra kerül.

  • A futtatókörnyezet nem kezeli az egyszerűsített feladatok és ügynökök kivételeit.

Ebben a dokumentumban

Tevékenységek és folytatások

Ez a szakasz azt ismerteti, hogyan kezeli a futtatási környezet a concurrency::task objektumok és azok folytatásai által kiváltott kivételeket. A tevékenység- és folytatási modellről további információt a Tevékenység párhuzamossága című témakörben talál.

Amikor egy objektumnak task átadott munkafüggvény törzsében kivételt dob, a futtatókörnyezet tárolja azt, és átirányítja a kivételt annak a környezetnek, amely a concurrency::task::get vagy concurrency::task::wait függvényt hívja. A feladat-párhuzamosság dokumentum a tevékenységalapú és az értékalapú folytatásokat írja le, de összegzésként az értékalapú folytatás típusparamétert T , a tevékenységalapú folytatás pedig egy típusparamétert task<T>vesz igénybe. Ha egy dobó tevékenység egy vagy több értékalapú folytatással rendelkezik, a rendszer nem ütemezi a folytatások futtatását. Az alábbi példa a következő viselkedést szemlélteti:

// eh-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]
    {
        throw exception();
    });
    
    // Create a continuation that prints its input value.
    auto continuation = t.then([]
    {
        // We do not expect this task to run because
        // the antecedent task threw.
        wcout << L"In continuation task..." << endl;
    });

    // Wait for the continuation to finish and handle any 
    // error that occurs.
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        continuation.wait();

        // Alternatively, call get() to produce the same result.
        //continuation.get();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception.
*/

A tevékenységalapú folytatással kezelheti az előzményfeladat által okozott kivételeket. A tevékenységalapú folytatások mindig futnak; nem számít, hogy a feladat sikeresen befejeződött, kivételt jelzett vagy megszakított. Amikor egy feladat kivételt dob, a feladatalapú folytatások ütemezésre kerülnek a futtatásra. Az alábbi példa egy olyan feladatot mutat be, amely mindig hibát okoz. A tevékenységnek két folytatása van; Az egyik értékalapú, a másik feladatalapú. A tevékenységalapú kivétel mindig fut, ezért képes elkapni az előzményfeladat által okozott kivételt. Amikor a példa mindkét folytatás befejezésére vár, a kivétel ismét kivétel lesz, mert a feladatkivétel mindig kiváltódik, amikor task::get vagy task::wait meghívásra kerül.

// eh-continuations.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{    
    wcout << L"Running a task..." << endl;
    // Create a task that throws.
    auto t = create_task([]() -> int
    {
        throw exception();
        return 42;
    });

    //
    // Attach two continuations to the task. The first continuation is  
    // value-based; the second is task-based.

    // Value-based continuation.
    auto c1 = t.then([](int n)
    {
        // We don't expect to get here because the antecedent 
        // task always throws.
        wcout << L"Received " << n << L'.' << endl;
    });

    // Task-based continuation.
    auto c2 = t.then([](task<int> previousTask)
    {
        // We do expect to get here because task-based continuations
        // are scheduled even when the antecedent task throws.
        try
        {
            wcout << L"Received " << previousTask.get() << L'.' << endl;
        }
        catch (const exception& e)
        {
            wcout << L"Caught exception from previous task." << endl;
        }
    });

    // Wait for the continuations to finish.
    try
    {
        wcout << L"Waiting for tasks to finish..." << endl;
        (c1 && c2).wait();
    }
    catch (const exception& e)
    {
        wcout << L"Caught exception while waiting for all tasks to finish." << endl;
    }
}
/* Output:
    Running a task...
    Waiting for tasks to finish...
    Caught exception from previous task.
    Caught exception while waiting for all tasks to finish.
*/

Javasoljuk, hogy a feladatalapú folytatásokkal észlelje azokat a kivételeket, amelyeket kezelni tud. Mivel a feladat-alapú folytatások automatikusan lefutnak, fontolja meg, hogy a folytatási lánc végén adjon-e hozzá egy ilyen folytatást. Ezzel garantálható, hogy a kód minden kivételt betart. Az alábbi példa egy alapvető értékalapú folytatási láncot mutat be. A lánc harmadik feladata hibaüzenetet ad, ezért az azt követő értékalapú kezelések nem futnak. A végső lépés azonban feladatalapú, ezért mindig végrehajtódik. Ez az utolsó folytatás kezeli a harmadik tevékenység által okozott kivételt.

Javasoljuk, hogy a lehető legspecifikusabb kivételeket kezelje. Kihagyhatja ezt az utolsó feladat-alapú folytatást, ha nincsenek konkrét hibák, amelyeket kezelni kell. Minden kivétel kezeletlen marad, és megszüntetheti az alkalmazást.

// eh-task-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    int n = 1;
    create_task([n]
    {
        wcout << L"In first task. n = ";
        wcout << n << endl;
        
        return n * 2;

    }).then([](int n)
    {
        wcout << L"In second task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](int n)
    {
        wcout << L"In third task. n = ";
        wcout << n << endl;

        // This task throws.
        throw exception();
        // Not reached.
        return n * 2;

    }).then([](int n)
    {
        // This continuation is not run because the previous task throws.
        wcout << L"In fourth task. n = ";
        wcout << n << endl;

        return n * 2;

    }).then([](task<int> previousTask)
    {
        // This continuation is run because it is task-based.
        try
        {
            // The call to task::get rethrows the exception.
            wcout << L"In final task. result = ";
            wcout << previousTask.get() << endl;
        }
        catch (const exception&)
        {
            wcout << L"<exception>" << endl;
        }
    }).wait();
}
/* Output:
    In first task. n = 1
    In second task. n = 2
    In third task. n = 4
    In final task. result = <exception>
*/

Jótanács

Az concurrency::task_completion_event::set_exception metódust használhatja arra, hogy kivételt társítson egy feladatbefejezési eseményhez. A Task Parallelism dokumentum részletesen bemutatja a concurrency::task_completion_event osztályt.

concurrency::task_canceled egy fontos futásidejű kivételtípus, amely a következőhöz kapcsolódik: task. A futtatókörnyezet a híváskor task_canceled indul task::get el, és a feladat megszakad. (Ezzel szemben task::wait a task_status::canceled értéket adja vissza és nem dob.) Ezt a kivételt el is kaphatja és kezelheti egy tevékenységalapú folytatásból vagy híváskor task::get. A feladattörléssel kapcsolatos további információkért lásd : Lemondás a PPL-ben.

Figyelmeztetés

Soha ne használja a "throw" kifejezést a kódban. Hívja inkább a concurrency::cancel_current_task függvényt.

A futtatókörnyezet leállítja az alkalmazást, ha egy feladat kivételt dob, és ezt a kivételt a feladat, annak egyik folytatása vagy a fő alkalmazás nem kapja el. Ha az alkalmazás összeomlik, beállíthatja a Visual Studiót, hogy megszakítsa a futást, amikor C++ kivételeket dobnak. Miután diagnosztizálta a kezeletlen kivétel helyét, a kezeléséhez használjon feladatalapú folytatást.

A jelen dokumentumban Futtatókörnyezet által kiváltott kivételek szakasz részletesebben ismerteti a futtatókörnyezeti kivételekkel való munkát.

[Felső]

Feladatcsoportok és párhuzamos algoritmusok

Ez a szakasz azt ismerteti, hogy a futtatókörnyezet hogyan kezeli a tevékenységcsoportok által kidobott kivételeket. Ez a szakasz olyan párhuzamos algoritmusokra is vonatkozik, mint az concurrency::parallel_for, mivel ezek az algoritmusok feladatcsoportokra épülnek.

Figyelmeztetés

Győződjön meg arról, hogy tisztában van a kivételek függő tevékenységekre gyakorolt hatásával. A kivételkezelés feladatokkal vagy párhuzamos algoritmusokkal való használatával kapcsolatos ajánlott eljárásokért tekintse meg a Párhuzamos minták tára témakör ajánlott eljárásainak a Törlés és kivételkezelés objektummegsemmisítésre gyakorolt hatásának ismertetése című szakaszát.

A tevékenységcsoportokkal kapcsolatos további információkért lásd: Feladat-párhuzamosság. A párhuzamos algoritmusokról további információt a Párhuzamos algoritmusok című témakörben talál.

Amikor egy munkafüggvény törzsében kivételt dob, amelyet concurrency::task_group vagy concurrency::structured_task_group objektumnak ad át, a futtatókörnyezet tárolja ezt a kivételt, és azt az összefüggéshez irányítja, amely a concurrency::task_group::wait, concurrency::structured_task_group::wait, concurrency::task_group::run_and_wait vagy concurrency::structured_task_group::run_and_wait függvényt hívja. A futtatókörnyezet emellett leállítja a tevékenységcsoportban lévő összes aktív tevékenységet (beleértve a gyermektevékenység-csoportokban lévőket is), és elveti a még el nem indított tevékenységeket.

Az alábbi példa egy kivételt okozó munkafüggvény alapstruktúráját mutatja be. A példa egy objektumot task_group használ két point objektum értékeinek párhuzamos nyomtatásához. A munkafüggvény print_point egy point objektum értékeit nyomtatja a konzolra. A munkafüggvény kivételt jelez, ha a bemeneti érték .NULL A futtatókörnyezet tárolja ezt a kivételt, és azt a task_group::wait-t hívó környezetbe juttatja.

// eh-task-group.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Defines a basic point with X and Y coordinates.
struct point
{
   int X;
   int Y;
};

// Prints the provided point object to the console.
void print_point(point* pt)
{
   // Throw an exception if the value is NULL.
   if (pt == NULL)
   {
      throw exception("point is NULL.");
   }

   // Otherwise, print the values of the point.
   wstringstream ss;
   ss << L"X = " << pt->X << L", Y = " << pt->Y << endl;
   wcout << ss.str();
}

int wmain()
{
   // Create a few point objects.
   point pt = {15, 30};
   point* pt1 = &pt;
   point* pt2 = NULL;

   // Use a task group to print the values of the points.
   task_group tasks;

   tasks.run([&] {
      print_point(pt1);
   });

   tasks.run([&] {
      print_point(pt2);
   });

   // Wait for the tasks to finish. If any task throws an exception,
   // the runtime marshals it to the call to wait.
   try
   {
      tasks.wait();
   }
   catch (const exception& e)
   {
      wcerr << L"Caught exception: " << e.what() << endl;
   }
}

Ez a példa a következő kimenetet hozza létre.

X = 15, Y = 30Caught exception: point is NULL.

A feladatcsoportban kivételkezelést használó teljes példáért lásd : A kivételkezelés használata párhuzamos hurokból való megszakításhoz.

[Felső]

A futtatókörnyezet által kidobott kivételek

Kivételt okozhat a futtatókörnyezet hívása. A legtöbb kivételtípus programozási hibára utal, kivéve a concurrency::task_canceled és concurrency::operation_timed_out kivételeket. Ezek a hibák általában helyreállíthatatlanok, ezért nem szabad elkapni vagy kezelni az alkalmazáskóddal. Azt javasoljuk, hogy csak akkor kapja meg vagy kezelje a helyreállíthatatlan hibákat az alkalmazáskódban, ha programozási hibákat kell diagnosztizálnia. A futtatókörnyezet által definiált kivételtípusok megértése azonban segíthet a programozási hibák diagnosztizálásában.

A kivételkezelési mechanizmus ugyanaz, mint a futtatókörnyezet által kidobott kivételek esetében, mint a munkahelyi függvények által kidobott kivételek. Az concurrency::receive függvény például akkor dob operation_timed_out, ha nem kap üzenetet a megadott időszakban. Ha a receive egy feladatcsoportnak átadott munkafüggvényben kivételt jelez, a futtatókörnyezet tárolja a kivételt, és továbbítja azt a task_group::wait, structured_task_group::wait, task_group::run_and_wait, vagy structured_task_group::run_and_wait hívó környezetébe.

Az alábbi példa a concurrency::parallel_invoke algoritmust használja, hogy két feladatot párhuzamosan futtasson. Az első feladat öt másodpercet vár, majd üzenetet küld egy üzenetpuffernek. A második feladat a függvényt használja, receive hogy három másodpercet várjon egy üzenet fogadásához ugyanabból az üzenetpufferből. A receive függvény akkor dob, operation_timed_out ha az adott időszakban nem kapja meg az üzenetet.

// eh-time-out.cpp
// compile with: /EHsc
#include <agents.h>
#include <ppl.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
   single_assignment<int> buffer;
   int result;

   try
   {
      // Run two tasks in parallel.
      parallel_invoke(
         // This task waits 5 seconds and then sends a message to 
         // the message buffer.
         [&] {
            wait(5000); 
            send(buffer, 42);
         },
         // This task waits 3 seconds to receive a message.
         // The receive function throws operation_timed_out if it does 
         // not receive a message in the specified time period.
         [&] {
            result = receive(buffer, 3000);
         }
      );

      // Print the result.
      wcout << L"The result is " << result << endl;
   }
   catch (operation_timed_out&)
   {
      wcout << L"The operation timed out." << endl;
   }
}

Ez a példa a következő kimenetet hozza létre.

The operation timed out.

Az alkalmazás rendellenes leállásának megakadályozása érdekében győződjön meg arról, hogy a kód kezeli a kivételeket a futtatókörnyezetbe való hívások során. A kivételeket szintén kezelje, amikor olyan külső kódot hív meg, amely a Konkurenciakezelő futtatókörnyezetet használja, például egy harmadik féltől származó könyvtárat.

[Felső]

Több kivétel

Ha egy feladat vagy párhuzamos algoritmus több kivételt is kap, a futtatókörnyezet csak az egyik kivételt alkalmazza a hívási környezetből. A futtatókörnyezet nem garantálja, hogy melyik kivételt kezeli.

Az alábbi példa a parallel_for algoritmus használatát mutatja be, amellyel számokat nyomtat a konzolra. Kivételt jelent, ha a bemeneti érték kisebb, mint egy minimális érték, vagy nagyobb, mint egy maximális érték. Ebben a példában több feladatfüggvény is okozhat kivételt.

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

using namespace concurrency;
using namespace std;

int wmain()
{
   const int min = 0;
   const int max = 10;
   
   // Print values in a parallel_for loop. Use a try-catch block to 
   // handle any exceptions that occur in the loop.
   try
   {
      parallel_for(-5, 20, [min,max](int i)
      {
         // Throw an exception if the input value is less than the 
         // minimum or greater than the maximum.

         // Otherwise, print the value to the console.

         if (i < min)
         {
            stringstream ss;
            ss << i << ": the value is less than the minimum.";
            throw exception(ss.str().c_str());
         }
         else if (i > max)
         {
            stringstream ss;
            ss << i << ": the value is greater than than the maximum.";
            throw exception(ss.str().c_str());
         }
         else
         {
            wstringstream ss;
            ss << i << endl;
            wcout << ss.str();
         }
      });
   }
   catch (exception& e)
   {
      // Print the error to the console.
      wcerr << L"Caught exception: " << e.what() << endl;
   }  
}

Az alábbiakban a példához tartozó mintakimenet látható.

8293104567Caught exception: -5: the value is less than the minimum.

[Felső]

Lemondás

Nem minden kivétel jelez hibát. Egy keresési algoritmus például kivételkezeléssel állíthatja le a társított feladatot, amikor megtalálja az eredményt. További információ a lemondási mechanizmusok kódban való használatáról: Lemondás a PPL-ben.

[Felső]

Egyszerűsített feladatok

A könnyűsúlyú feladat olyan feladat, amelyet közvetlenül egy egyidejűség::Scheduler objektumból ütemez. Az egyszerűsített feladatok kevesebb többletterhelést hordoznak magukban, mint a szokásos feladatok. A futtatókörnyezet azonban nem észlel kivételeket, amelyeket az egyszerűsített feladatok okoznak. Ehelyett a kivételt a kezeletlen kivételkezelő fogja elkapni, amely alapértelmezés szerint leállítja a folyamatot. Ezért használjon egy megfelelő hibakezelési mechanizmust az alkalmazásban. Az egyszerűsített feladatokról további információt a Feladatütemezőben talál.

[Felső]

Aszinkron ügynökök

Az egyszerűsített feladatokhoz hasonlóan a futtatókörnyezet sem kezeli az aszinkron ügynökök által kidobott kivételeket.

Az alábbi példa egy módszert mutat be egy concurrency::agent osztályból származó osztály kivételeinek kezelésére. Ez a példa az osztályt points_agent határozza meg. A points_agent::run metódus beolvassa point az objektumokat az üzenetpufferből, és kinyomtatja őket a konzolra. A run metódus kivételt jelez, ha mutatót NULL kap.

A run metódus egy blokkban lévő try-catch összes munkát körülveszi. A catch blokk egy üzenetpufferben tárolja a kivételt. Az alkalmazás ellenőrzi, hogy az ügynök hibát észlelt-e, miután befejezte a feladatát, azáltal, hogy elolvassa a puffer tartalmát.

// eh-agents.cpp
// compile with: /EHsc
#include <agents.h>
#include <iostream>

using namespace concurrency;
using namespace std;

// Defines a point with x and y coordinates.
struct point
{
   int X;
   int Y;
};

// Informs the agent to end processing.
point sentinel = {0,0};

// An agent that prints point objects to the console.
class point_agent : public agent
{
public:
   explicit point_agent(unbounded_buffer<point*>& points)
      : _points(points)
   { 
   }

   // Retrieves any exception that occurred in the agent.
   bool get_error(exception& e)
   {
      return try_receive(_error, e);
   }

protected:
   // Performs the work of the agent.
   void run()
   {
      // Perform processing in a try block.
      try
      {
         // Read from the buffer until we reach the sentinel value.
         while (true)
         {
            // Read a value from the message buffer.
            point* r = receive(_points);

            // In this example, it is an error to receive a 
            // NULL point pointer. In this case, throw an exception.
            if (r == NULL)
            {
               throw exception("point must not be NULL");
            }
            // Break from the loop if we receive the 
            // sentinel value.
            else if (r == &sentinel)
            {
               break;
            }
            // Otherwise, do something with the point.
            else
            {
               // Print the point to the console.
               wcout << L"X: " << r->X << L" Y: " << r->Y << endl;
            }
         }
      }
      // Store the error in the message buffer.
      catch (exception& e)
      {
         send(_error, e);
      }

      // Set the agent status to done.
      done();
   }

private:
   // A message buffer that receives point objects.
   unbounded_buffer<point*>& _points;

   // A message buffer that stores error information.
   single_assignment<exception> _error;
};

int wmain()
{  
   // Create a message buffer so that we can communicate with
   // the agent.
   unbounded_buffer<point*> buffer;

   // Create and start a point_agent object.
   point_agent a(buffer);
   a.start();

   // Send several points to the agent.
   point r1 = {10, 20};
   point r2 = {20, 30};
   point r3 = {30, 40};

   send(buffer, &r1);
   send(buffer, &r2);
   // To illustrate exception handling, send the NULL pointer to the agent.
   send(buffer, reinterpret_cast<point*>(NULL));
   send(buffer, &r3);
   send(buffer, &sentinel);

   // Wait for the agent to finish.
   agent::wait(&a);
  
   // Check whether the agent encountered an error.
   exception e;
   if (a.get_error(e))
   {
      cout << "error occurred in agent: " << e.what() << endl;
   }
   
   // Print out agent status.
   wcout << L"the status of the agent is: ";
   switch (a.status())
   {
   case agent_created:
      wcout << L"created";
      break;
   case agent_runnable:
      wcout << L"runnable";
      break;
   case agent_started:
      wcout << L"started";
      break;
   case agent_done:
      wcout << L"done";
      break;
   case agent_canceled:
      wcout << L"canceled";
      break;
   default:
      wcout << L"unknown";
      break;
   }
   wcout << endl;
}

Ez a példa a következő kimenetet hozza létre.

X: 10 Y: 20
X: 20 Y: 30
error occurred in agent: point must not be NULL
the status of the agent is: done

Mivel a blokk a try-catchwhile cikluson kívül létezik, az ügynök az első hiba észlelésekor befejezi a feldolgozást. Ha a try-catch blokk a while cikluson belül volna, az ügynök egy hiba bekövetkezése után is folytatná a működését.

Ez a példa egy üzenetpufferben tárolja a kivételeket, így egy másik összetevő figyelheti az ügynököt a futtatás során felmerülő hibákért. Ez a példa concurrency::single_assignment objektumot használ a hiba tárolásához. Abban az esetben, ha egy ügynök több kivételt kezel, az single_assignment osztály csak az első, neki átadott üzenetet tárolja. Ha csak az utolsó kivételt szeretné tárolni, használja a Concurrency::overwrite_buffer osztályt. Az összes kivétel tárolásához használja a concurrency::unbounded_buffer osztályt. Az üzenetblokkokról további információt az Aszinkron üzenetblokkok című témakörben talál.

Az aszinkron ügynökökről további információt az Aszinkron ügynökök című témakörben talál.

[Felső]

Összefoglalás

[Felső]

Lásd még

Párhuzamossági futtatókörnyezet
Feladat-párhuzamosság
Párhuzamos algoritmusok
Lemondás a PPL-ben
Feladatütemező
Aszinkron ügynökök