Поделиться через


Практическое руководство. Использование класса Context для реализации семафора, поддерживающего параллельный доступ

В этом разделе описано использование класса Concurrency::Context для реализации класса семафора, поддерживающего параллельный доступ.

Класс Context позволяет блокировать или разрешать текущий контекст выполнения. Блокирование или разрешение текущего контекста полезно, если невозможно продолжать работу в текущем контексте из-за недоступности ресурса. Семафор представляет собой пример ситуации, когда текущему контексту выполнения приходится ждать, когда ресурс станет доступным. Семафор, как и объект критической секции, представляет собой объект синхронизации, позволяющий коду из одного контекста осуществлять монопольный доступ к ресурсу. Однако в отличие от объекта критической секции семафор позволяет осуществлять одновременный доступ к ресурсу нескольким контекстам. По достижении максимального числа контекстов устанавливается блокировка семафора, и каждый последующий контекст должен дождаться снятия блокировки предыдущим.

Реализация класса семафора

  1. Объявите класс с именем semaphore. Добавьте в этот класс разделы public и private.

    // A semaphore type that uses cooperative blocking semantics.
    class semaphore
    {
    public:
    private:
    };
    
  2. В разделе private класса semaphore объявите переменную типа LONG, содержащую счетчик семафора, и объект Concurrency::concurrent_queue, содержащий контексты, которые должны ожидать освобождения семафора.

    // The semaphore count.
    LONG _semaphore_count;
    
    // A concurrency-safe queue of contexts that must wait to 
    // acquire the semaphore.
    concurrent_queue<Context*> _waiting_contexts;
    
  3. Реализуйте конструктор в разделе public класса semaphore. Конструктор принимает значение LONG, задающее максимальное число контекстов, при одновременном выполнении которых устанавливается блокировка.

    explicit semaphore(LONG capacity)
       : _semaphore_count(capacity)
    {
    }
    
  4. Реализуйте метод acquire в разделе public класса semaphore. Этот метод уменьшает значение счетчика семафора в результате атомарной операции. Если значение счетчика семафора становится отрицательным, добавьте текущий контекст в конец очереди ожидания и вызовите метод Concurrency::Context::Block для блокирования текущего контекста.

    // 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 (InterlockedDecrement(&_semaphore_count) < 0)
       {
          _waiting_contexts.push(Context::CurrentContext());
          Context::Block();
       }
    }
    
  5. Реализуйте метод release в разделе public класса semaphore. Этот метод увеличивает значение счетчика семафора в результате атомарной операции. Если значение счетчика семафора отрицательное до начала операции увеличения, это означает, что хотя бы один контекст ожидает снятия блокировки. В этом случае необходимо разблокировать контекст в начале очереди ожидания.

    // Releases access to the semaphore.
    void release()
    {
       // If the semaphore count is negative, unblock the first waiting context.
       if (InterlockedIncrement(&_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;
          if (!_waiting_contexts.try_pop(waiting))
          {
             Context::Yield();
          }
    
          // Unblock the context.
          waiting->Unblock();
       }
    }
    

Пример

Класс semaphore в этом примере функционирует параллельно, так как методы Context::Block и Context::Yield передают выполнение, чтобы среда выполнения могла работать над другими задачами.

Метод acquire уменьшает показания счетчика, но этому методу не всегда удается завершить добавление контекста в очередь ожидания до вызова метода release другим контекстом. Поэтому метод release использует круговой цикл, заставляющий метод Concurrency::Context::Yield дожидаться завершения добавления контекста методом acquire.

Метод release может вызвать метод Context::Unblock до того, как метод acquire вызовет метод Context::Block. Не обязательно обеспечивать защиту против такого состояния гонки, так как среда выполнения позволяет вызывать эти методы в любой последовательности. Если метод release вызывает метод Context::Unblock до того, как acquire вызывает Context::Block для одного и того же контекста, контекст не блокируется. Среда выполнения требует только соответствия каждого вызова метода Context::Block определенному вызову метода Context::Unblock.

В следующем примере продемонстрирован полный класс semaphore. Функция wmain показывает базовое использование этого класса. Функция wmain использует алгоритм Concurrency::parallel_for, чтобы создать несколько задач, для выполнения которых необходим доступ к семафору. Так как в этих потоках в любой момент может быть установлена блокировка, некоторые задачи вынуждены дожидаться завершения других задач и снятия блокировки.

// cooperative-semaphore.cpp
// compile with: /EHsc
#include <windows.h>
#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 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 (InterlockedDecrement(&_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 (InterlockedIncrement(&_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;
         if (!_waiting_contexts.try_pop(waiting))
         {
            Context::Yield();
         }

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

private:
   // The semaphore count.
   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();
   });
}

В данном примере получается следующий результат.

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

Дополнительные сведения о классе concurrent_queue см. в разделе Параллельные контейнеры и объекты. Дополнительные сведения об алгоритме parallel_for см. в разделе Параллельные алгоритмы.

Компиляция кода

Скопируйте пример кода и вставьте его в проект Visual Studio или файл с именем cooperative-semaphore.cpp, затем выполните в окне командной строки Visual Studio 2010 следующую команду.

cl.exe /EHsc cooperative-semaphore.cpp

Отказоустойчивость

Для ограничения доступа к объекту semaphore до заданного масштаба можно воспользоваться шаблоном Получение ресурса есть инициализация (RAII). При использовании шаблона RAII структура данных располагается в стеке. Эта структура данных инициализирует или приобретает ресурс во время создания структуры и уничтожает или освобождает ресурс во время уничтожения структуры данных. Шаблон RAII гарантирует, что деструктор вызывается до выхода из внешней области видимости. Следовательно, ресурс эффективно управляется при создании исключения или если функция содержит несколько операторов return.

В следующем примере определяется класс с именем scoped_lock, определенный в разделе public класса semaphore. Класс scoped_lock похож на классы Concurrency::critical_section::scoped_lock и Concurrency::reader_writer_lock::scoped_lock. Конструктор класса semaphore::scoped_lock получает, а деструктор предоставляет доступ к заданному объекту semaphore.

// 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;
};

В следующем примере изменяется основная часть рабочей функции, передаваемой алгоритму parallel_for для использования шаблона RAII для освобождения семафора до возвращения функции. Эта техника обеспечивает выполнение рабочей функции без создания исключений.

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);
});

См. также

Ссылки

Класс Context

Основные понятия

Параллельные контейнеры и объекты

Другие ресурсы

Контексты