Udostępnij za pośrednictwem


Porady: korzystanie z klasy kontekstu do wdrażania a kooperatywnego semafora

W tym temacie pokazano, jak za pomocą klasy concurrency::Context zaimplementować klasę semafora współpracy.

Uwagi

Klasa Context umożliwia blokowanie lub zwracanie bieżącego kontekstu wykonywania. Blokowanie lub zwracanie bieżącego kontekstu jest przydatne, gdy bieżący kontekst nie może kontynuować, ponieważ zasób jest niedostępny. Semafor jest przykładem jednej sytuacji, w której bieżący kontekst wykonywania musi czekać na udostępnienie zasobu. Semafor, taki jak obiekt sekcji krytycznej, jest obiektem synchronizacji, który umożliwia kodowi w jednym kontekście wyłączny dostęp do zasobu. Jednak w przeciwieństwie do obiektu sekcji krytycznej semafor umożliwia dostęp współbieżnie do zasobu więcej niż jeden kontekst. Jeśli maksymalna liczba kontekstów zawiera blokadę semafora, każdy dodatkowy kontekst musi czekać, aż inny kontekst zwolni blokadę.

Aby implementować klasę semafora

  1. Zadeklaruj klasę o nazwie semaphore. Dodaj public sekcje i private do tej klasy.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
  1. private W sekcji semaphore klasy zadeklaruj zmienną std::atomic, która zawiera liczbę semaforów i współbieżność::concurrent_queue obiekt, który przechowuje konteksty, które muszą czekać na uzyskanie semafora.
// The semaphore count.
atomic<long long> _semaphore_count;

// A concurrency-safe queue of contexts that must wait to 
// acquire the semaphore.
concurrent_queue<Context*> _waiting_contexts;
  1. public W sekcji semaphore klasy zaimplementuj konstruktor. Konstruktor przyjmuje wartość określającą maksymalną long long liczbę kontekstów, które mogą jednocześnie przechowywać blokadę.
explicit semaphore(long long capacity)
   : _semaphore_count(capacity)
{
}
  1. public W sekcji semaphore klasy zaimplementuj metodę acquire . Ta metoda dekrementuje liczbę semaforów jako operację niepodzieną. Jeśli liczba semaforów stanie się ujemna, dodaj bieżący kontekst na końcu kolejki oczekiwania i wywołaj metodę concurrency::Context::Block , aby zablokować bieżący kontekst.
// Acquires access to the semaphore.
void acquire()
{
   // The capacity of the semaphore is exceeded when the semaphore count 
   // falls below zero. When this happens, add the current context to the 
   // back of the wait queue and block the current context.
   if (--_semaphore_count < 0)
   {
      _waiting_contexts.push(Context::CurrentContext());
      Context::Block();
   }
}
  1. public W sekcji semaphore klasy zaimplementuj metodę release . Ta metoda zwiększa liczbę semaforów jako operację niepodzielna. Jeśli liczba semaforów jest ujemna przed operacją przyrostu, istnieje co najmniej jeden kontekst, który czeka na blokadę. W takim przypadku odblokuj kontekst znajdujący się przed kolejką oczekiwania.
// Releases access to the semaphore.
void release()
{
   // If the semaphore count is negative, unblock the first waiting context.
   if (++_semaphore_count <= 0)
   {
      // A call to acquire might have decremented the counter, but has not
      // yet finished adding the context to the queue. 
      // Create a spin loop that waits for the context to become available.
      Context* waiting = NULL;
      while (!_waiting_contexts.try_pop(waiting))
      {
         Context::Yield();
      }

      // Unblock the context.
      waiting->Unblock();
   }
}

Przykład

Klasa semaphore w tym przykładzie zachowuje się kooperacyjnie, ponieważ Context::Block metody i Context::Yield dają wykonanie, aby środowisko uruchomieniowe może wykonywać inne zadania.

Metoda acquire dekrementuje licznik, ale może nie zakończyć dodawania kontekstu do kolejki oczekiwania, zanim inny kontekst wywoła metodę release . Aby to uwzględnić, release metoda używa pętli spin, która wywołuje współbieżność::Context::Yield , aby poczekać na acquire zakończenie dodawania kontekstu przez metodę .

Metoda release może wywołać metodę Context::Unblock przed acquire wywołaniem Context::Block metody . Nie musisz chronić się przed tym warunkiem wyścigu, ponieważ środowisko uruchomieniowe umożliwia wywoływanie tych metod w dowolnej kolejności. release Jeśli metoda wywołuje Context::Unblock metodę acquire przed wywołaniami Context::Block tego samego kontekstu, kontekst ten pozostaje odblokowany. Środowisko uruchomieniowe wymaga tylko, aby każde wywołanie było Context::Block zgodne z odpowiednim wywołaniem metody Context::Unblock.

W poniższym przykładzie przedstawiono kompletną semaphore klasę. Funkcja wmain pokazuje podstawowe użycie tej klasy. Funkcja wmain używa algorytmu concurrency::p arallel_for , aby utworzyć kilka zadań wymagających dostępu do semapora. Ponieważ trzy wątki mogą w dowolnym momencie przechowywać blokadę, niektóre zadania muszą czekać na zakończenie innego zadania i zwolnić blokadę.

// cooperative-semaphore.cpp
// compile with: /EHsc
#include <atomic>
#include <concrt.h>
#include <ppl.h>
#include <concurrent_queue.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
   explicit semaphore(long long capacity)
      : _semaphore_count(capacity)
   {
   }

   // Acquires access to the semaphore.
   void acquire()
   {
      // The capacity of the semaphore is exceeded when the semaphore count 
      // falls below zero. When this happens, add the current context to the 
      // back of the wait queue and block the current context.
      if (--_semaphore_count < 0)
      {
         _waiting_contexts.push(Context::CurrentContext());
         Context::Block();
      }
   }

   // Releases access to the semaphore.
   void release()
   {
      // If the semaphore count is negative, unblock the first waiting context.
      if (++_semaphore_count <= 0)
      {
         // A call to acquire might have decremented the counter, but has not
         // yet finished adding the context to the queue. 
         // Create a spin loop that waits for the context to become available.
         Context* waiting = NULL;
         while (!_waiting_contexts.try_pop(waiting))
         {
            Context::Yield();
         }

         // Unblock the context.
         waiting->Unblock();
      }
   }

private:
   // The semaphore count.
   atomic<long long> _semaphore_count;

   // A concurrency-safe queue of contexts that must wait to 
   // acquire the semaphore.
   concurrent_queue<Context*> _waiting_contexts;
};

int wmain()
{
   // Create a semaphore that allows at most three threads to 
   // hold the lock.
   semaphore s(3);

   parallel_for(0, 10, [&](int i) {
      // Acquire the lock.
      s.acquire();

      // Print a message to the console.
      wstringstream ss;
      ss << L"In loop iteration " << i << L"..." << endl;
      wcout << ss.str();

      // Simulate work by waiting for two seconds.
      wait(2000);

      // Release the lock.
      s.release();
   });
}

W tym przykładzie są generowane następujące przykładowe dane wyjściowe.

In loop iteration 5...
In loop iteration 0...
In loop iteration 6...
In loop iteration 1...
In loop iteration 2...
In loop iteration 7...
In loop iteration 3...
In loop iteration 8...
In loop iteration 9...
In loop iteration 4...

Aby uzyskać więcej informacji na temat concurrent_queue klasy, zobacz Parallel Containers and Objects (Kontenery równoległe i obiekty). Aby uzyskać więcej informacji na temat algorytmu parallel_for , zobacz Parallel Algorithms (Algorytmy równoległe).

Kompilowanie kodu

Skopiuj przykładowy kod i wklej go w projekcie programu Visual Studio lub wklej go w pliku o nazwie cooperative-semaphore.cpp , a następnie uruchom następujące polecenie w oknie wiersza polecenia programu Visual Studio.

cl.exe /EHsc cooperative-semaphore.cpp

Niezawodne programowanie

Aby ograniczyć dostęp do obiektu do semaphore danego zakresu, można użyć wzorca Pozyskiwanie zasobów jest inicjowanie (RAII). W ramach wzorca RAII struktura danych jest przydzielana na stosie. Ta struktura danych inicjuje lub uzyskuje zasób podczas jego tworzenia i niszczy lub zwalnia ten zasób, gdy struktura danych zostanie zniszczona. Wzorzec RAII gwarantuje, że destruktor jest wywoływany przed zamknięciem zakresu otaczającego. W związku z tym zasób jest prawidłowo zarządzany, gdy zgłaszany jest wyjątek lub gdy funkcja zawiera wiele return instrukcji.

W poniższym przykładzie zdefiniowano klasę o nazwie scoped_lock, która jest zdefiniowana w public sekcji semaphore klasy . Klasa scoped_lock przypomina klasy concurrency::critical_section::scoped_lock i concurrency::reader_writer_lock::scoped_lock . Konstruktor semaphore::scoped_lock klasy uzyskuje dostęp do danego semaphore obiektu, a destruktor zwalnia dostęp do tego obiektu.

// An exception-safe RAII wrapper for the semaphore class.
class scoped_lock
{
public:
   // Acquires access to the semaphore.
   scoped_lock(semaphore& s)
      : _s(s)
   {
      _s.acquire();
   }
   // Releases access to the semaphore.
   ~scoped_lock()
   {
      _s.release();
   }

private:
   semaphore& _s;
};

Poniższy przykład modyfikuje treść funkcji roboczej, która jest przekazywana do parallel_for algorytmu, tak aby używała RAII, aby upewnić się, że semafor jest zwalniany przed zwróceniem funkcji. Ta technika gwarantuje, że funkcja pracy jest bezpieczna dla wyjątków.

parallel_for(0, 10, [&](int i) {
   // Create an exception-safe scoped_lock object that holds the lock 
   // for the duration of the current scope.
   semaphore::scoped_lock auto_lock(s);

   // Print a message to the console.
   wstringstream ss;
   ss << L"In loop iteration " << i << L"..." << endl;
   wcout << ss.str();

   // Simulate work by waiting for two seconds.
   wait(2000);
});

Zobacz też

Konteksty
Równoległe kontenery oraz obiekty