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

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

Context 클래스를 사용하면 현재 실행 컨텍스트를 차단하거나 양보할 수 있습니다. 사용 가능한 리소스가 없어서 현재 컨텍스트를 계속할 수 없는 경우 현재 컨텍스트를 차단하거나 양보하면 유용합니다. 세마포는 현재 실행 컨텍스트에서 리소스를 사용할 수 있을 때까지 기다려야 하는 경우의 한 예입니다. 세마포는 임계 영역 개체와 마찬가지로 한 컨텍스트의 코드에서 리소스에 단독으로 액세스할 수 있도록 하는 동기화 개체입니다. 그러나 임계 영역 개체와 달리 세마포를 사용하면 둘 이상의 컨텍스트에서 동시에 리소스에 액세스할 수 있습니다. 세마포 잠금을 보유하는 컨텍스트의 수가 최대값에 도달하면 각 추가 컨텍스트는 다른 컨텍스트에서 잠금을 해제할 때까지 기다려야 합니다.

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

  1. semaphore라는 클래스를 선언합니다. public 및 private 섹션을 이 클래스에 추가합니다.

    // A semaphore type that uses cooperative blocking semantics.
    class semaphore
    {
    public:
    private:
    };
    
  2. semaphore 클래스의 private 섹션에서 세마포 카운트를 보유하는 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. semaphore 클래스의 public 섹션에서 생성자를 구현합니다. 이 생성자는 잠금을 동시에 보유할 수 있는 최대 컨텍스트 수를 지정하는 LONG 값을 사용합니다.

    explicit semaphore(LONG capacity)
       : _semaphore_count(capacity)
    {
    }
    
  4. semaphore 클래스의 public 섹션에서 acquire 메서드를 구현합니다. 이 메서드는 원자 단위 연산으로 세마포 카운트를 감소시킵니다. 세마포 카운트가 음수가 되면 대기 큐의 끝에 현재 컨텍스트를 추가하고 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. semaphore 클래스의 public 섹션에서 release 메서드를 구현합니다. 이 메서드는 원자 단위 연산으로 세마포 카운트를 증가시킵니다. 증가 연산 전에 세마포 카운트가 음수이면 잠금을 기다리고 있는 컨텍스트가 최소한 하나 이상 있다는 의미입니다. 이 경우에는 대기 큐에서 맨 앞에 있는 컨텍스트의 차단을 해제합니다.

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

예제

Context::BlockContext::Yield 메서드는 런타임에서 다른 작업을 수행할 수 있도록 실행을 양보하므로 이 예제의 semaphore 클래스는 협조적으로 동작합니다.

acquire 메서드는 카운터를 감소시키지만 다른 컨텍스트에서 release 메서드를 호출하기 전에 대기 큐에 컨텍스트를 추가하는 것을 끝내지는 않습니다. 이를 고려하여 release 메서드는 Concurrency::Context::Yield 메서드를 호출하는 회전 루프를 사용하여 acquire 메서드에서 컨텍스트 추가를 끝낼 때까지 기다립니다.

acquire 메서드에서 Context::Block 메서드를 호출하기 전에 release 메서드에서 Context::Unblock 메서드를 호출할 수 있습니다. 런타임에서는 이러한 메서드를 순서에 관계없이 호출할 수 있으므로 이 경합 상태를 제한할 필요가 없습니다. 같은 컨텍스트에 대해 acquire 메서드에서 Context::Block을 호출하기 전에 release 메서드에서 Context::Unblock을 호출하면 해당 컨텍스트가 차단되지 않은 상태로 유지됩니다. 런타임에서는 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

강력한 프로그래밍

RAII(Resource Acquisition Is Initialization) 패턴을 사용하면 semaphore 개체에 대한 액세스를 특정 범위로 제한할 수 있습니다. RAII 패턴에서는 데이터 구조가 스택에 할당됩니다. 이러한 데이터 구조는 만들어질 때 리소스를 초기화하거나 획득하고, 소멸될 때 리소스를 소멸시키거나 해제합니다. RAII 패턴을 사용하면 바깥쪽 범위를 벗어나기 전에 소멸자가 호출됩니다. 따라서 예외가 throw되거나 함수에 return 문이 여러 개 있는 경우 리소스가 올바르게 관리됩니다.

다음 예제에서는 semaphore 클래스의 public 섹션에 정의되어 있는 scoped_lock 클래스를 정의합니다. scoped_lock 클래스는 Concurrency::critical_section::scoped_lockConcurrency::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 클래스

개념

병렬 컨테이너 및 개체

기타 리소스

컨텍스트