방법: 컨텍스트 클래스를 사용하여 공동 작업 세마포 구현

이 항목에서는 동시성::Context 클래스를 사용하여 협조적 세마포 클래스를 구현하는 방법을 보여줍니다.

설명

클래스를 Context 사용하면 현재 실행 컨텍스트를 차단하거나 생성할 수 있습니다. 현재 컨텍스트를 차단하거나 생성하는 것은 리소스를 사용할 수 없기 때문에 현재 컨텍스트를 진행할 수 없는 경우에 유용합니다. 세마포는 현재 실행 컨텍스트가 리소스를 사용할 수 있게 될 때까지 기다려야 하는 한 가지 상황의 예입니다. 중요한 섹션 개체와 같은 세마포는 한 컨텍스트의 코드가 리소스에 단독으로 액세스할 수 있도록 하는 동기화 개체입니다. 그러나 중요한 섹션 개체와 달리 세마포를 사용하면 둘 이상의 컨텍스트가 동시에 리소스에 액세스할 수 있습니다. 최대 컨텍스트 수가 세마포 잠금을 보유하는 경우 각 추가 컨텍스트는 다른 컨텍스트가 잠금을 해제할 때까지 기다려야 합니다.

세마포 클래스를 구현하려면

  1. 이름이 semaphore지정된 클래스를 선언합니다. 이 클래스에 섹션을 private 추가하고 섹션을 추가 public 합니다.
// A semaphore type that uses cooperative blocking semantics.
class semaphore
{
public:
private:
};
  1. private 클래스의 semaphore 섹션에서 세마포 수를 보유하는 std::atomic 변수와 세마포를 획득하기 위해 기다려야 하는 컨텍스트를 보유하는 동시성::concurrent_queue 개체를 선언합니다.
// 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. 클래스의 publicsemaphore 섹션에서 생성자를 구현합니다. 생성자는 잠금을 long long 동시에 보유할 수 있는 최대 컨텍스트 수를 지정하는 값을 사용합니다.
explicit semaphore(long long capacity)
   : _semaphore_count(capacity)
{
}
  1. 클래스의 publicsemaphore 섹션에서 메서드를 구현합니다 acquire . 이 메서드는 세마포 수를 원자성 연산으로 감소합니다. 세마포 수가 음수가 되면 대기 큐의 끝에 현재 컨텍스트를 추가하고 동시성::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 (--_semaphore_count < 0)
   {
      _waiting_contexts.push(Context::CurrentContext());
      Context::Block();
   }
}
  1. 클래스의 publicsemaphore 섹션에서 메서드를 구현합니다 release . 이 메서드는 세마포 수를 원자성 연산으로 증분합니다. 증분 작업 전에 세마포 수가 음수이면 잠금을 기다리는 컨텍스트가 하나 이상 있습니다. 이 경우 대기 큐의 맨 앞에 있는 컨텍스트의 차단을 해제합니다.
// 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();
   }
}

예시

이 예제의 클래스는 semaphore 런타임이 다른 작업을 수행할 수 있도록 및 Context::Yield 메서드가 실행을 생성하기 때문에 Context::Block 협조적으로 동작합니다.

메서드는 acquire 카운터를 감소시키지만 다른 컨텍스트가 메서드를 호출하기 전에 대기 큐에 컨텍스트 추가를 release 완료하지 않을 수 있습니다. 이를 설명하기 위해 메서드는 release 동시성::Context::Yield 메서드를 호출하는 스핀 루프를 사용하여 메서드가 acquire 컨텍스트 추가를 완료할 때까지 기다립니다.

메서드가 release 메서드를 Context::Unblock 호출하기 전에 메서드를 acquire 호출할 Context::Block 수 있습니다. 런타임에서는 이러한 메서드를 순서대로 호출할 수 있으므로 이 경합 상태로부터 보호할 필요가 없습니다. 메서드가 release 동일한 컨텍스트를 acquire 호출하기 전에 메서드를 호출 Context::UnblockContext::Block 하는 경우 해당 컨텍스트가 다시 차단되지 기본. 런타임에는 각 호출이 해당 호출 Context::Block 과 일치해야 합니다 Context::Unblock.

다음 예제에서는 전체 semaphore 클래스를 보여줍니다. 이 함수는 wmain 이 클래스의 기본 사용을 보여 줍니다. 이 함수는 wmain concurrency::p arallel_for 알고리즘을 사용하여 세마포에 액세스해야 하는 여러 작업을 만듭니다. 세 개의 스레드가 언제든지 잠금을 유지할 수 있으므로 일부 작업은 다른 작업이 완료되고 잠금을 해제할 때까지 기다려야 합니다.

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

이 예제에서는 다음 샘플 출력을 생성합니다.

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 명령 프롬프트 창에서 다음 명령을 실행합니다.

cl.exe /EHsc cooperative-semaphore.cpp

강력한 프로그래밍

RAII(리소스 취득 초기화) 패턴을 사용하여 개체에 semaphore 대한 액세스를 지정된 범위로 제한할 수 있습니다. RAII 패턴에 따라 데이터 구조가 스택에 할당됩니다. 해당 데이터 구조는 생성될 때 리소스를 초기화하거나 획득하고 데이터 구조가 소멸될 때 해당 리소스를 삭제하거나 해제합니다. RAII 패턴은 바깥쪽 범위가 종료되기 전에 소멸자가 호출되도록 보장합니다. 따라서 예외가 throw되거나 함수에 여러 return 문이 포함된 경우 리소스가 올바르게 관리됩니다.

다음 예제에서는 클래스의 섹션에 정의된 명명 scoped_lock된 클래스를 public 정의합니다 semaphore . 클래스는 scoped_lock 동시성::critical_section::scoped_lock동시성::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);
});

참고 항목

컨텍스트
병렬 컨테이너 및 개체