다음을 통해 공유


동시성 런타임의 유용한 일반 정보

업데이트: 2011년 3월

이 문서에서는 동시성 런타임의 여러 영역에 적용되는 유용한 정보에 대해 설명합니다.

단원

이 문서에는 다음과 같은 단원이 포함되어 있습니다.

  • 가능한 경우 협력 동기화 구문 사용

  • 결과가 없는 긴 작업은 허용하지 않음

  • 초과 구독을 사용하여 차단되거나 대기 시간이 긴 작업 오프셋

  • 가능한 경우 동시 메모리 관리 함수 사용

  • RAII을 사용하여 동시성 개체의 수명 관리

  • 전역 범위에서 동시성 개체를 만들지 않음

  • 공유 데이터 세그먼트에서 동시성 개체를 사용하지 않음

가능한 경우 협력 동기화 구문 사용

동시성 런타임에서는 외부 동기화 개체가 필요 없는 여러 개의 동시성이 보장되는 구문을 제공합니다. 예를 들어 Concurrency::concurrent_vector 클래스에서는 동시성이 보장되는 추가 및 요소 액세스 작업을 제공합니다. 그러나 리소스에 대한 단독 액세스가 필요한 경우 런타임에서 Concurrency::critical_section, Concurrency::reader_writer_lockConcurrency::event 클래스를 제공합니다. 이러한 형식은 협조적으로 동작하므로 첫 번째 작업이 데이터를 기다릴 때 작업 스케줄러에서 다른 컨텍스트에 처리 리소스를 다시 할당할 수 있습니다. 가능한 경우 Windows API에서 제공하며 협조적으로 동작하지 않는 메커니즘과 같은 다른 동기화 메커니즘 대신 이 동기화 형식을 사용하십시오. 이러한 동기화 형식과 코드 예제에 대한 자세한 내용은 동기화 데이터 구조동기화 데이터 구조와 Windows API의 비교를 참조하십시오.

[맨 위로 이동]

결과가 없는 긴 작업은 허용하지 않음

작업 스케줄러가 협조적으로 동작하므로 작업 간에 공평하지 않습니다. 따라서 특정 작업으로 인해 다른 작업이 시작되지 않을 수 있습니다. 이러한 상황이 허용되는 경우도 있지만 교착 또는 고갈 상태가 발생하는 경우도 있습니다.

다음 예제에서는 할당된 처리 리소스 수보다 많은 작업을 수행합니다. 첫 번째 작업은 작업 스케줄러에 양보하지 않으므로 첫 번째 작업이 완료될 때까지 두 번째 작업이 시작되지 않습니다.

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

using namespace Concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

이 예제의 결과는 다음과 같습니다.

1: 250000000
1: 500000000
1: 750000000
1: 1000000000
2: 250000000
2: 500000000
2: 750000000
2: 1000000000

두 작업 간에 협조되도록 하는 여러 방법이 있습니다. 이 중 하나는 장기 실행 작업에서 작업 스케줄러에 때때로 양보하는 것입니다. 다음 예제에서는 다른 작업이 실행될 수 있도록 작업 스케줄러에 실행을 양보하는 Concurrency::Context::Yield 메서드를 호출하도록 task 함수를 수정합니다.

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

이 예제의 결과는 다음과 같습니다.

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Context::Yield 메서드는 현재 스레드가 속해 있는 스케줄러의 다른 활성 스레드, 간단한 작업 또는 다른 운영 체제 스레드만 생성합니다. 이 메서드는 Concurrency::task_group 또는 Concurrency::structured_task_group 개체에서 실행되도록 예약되어 있지만 아직 시작되지 않은 작업에 양보하지 않습니다.

장기 실행 작업 간에 협조되도록 하는 다른 방법이 있습니다. 큰 작업은 작은 하위 작업으로 나눌 수 있습니다. 긴 작업이 실행되는 동안 초과 구독을 사용하도록 설정할 수 있습니다. 초과 구독을 사용하면 사용 가능한 하드웨어 수보다 많은 스레드를 만들 수 있습니다. 초과 구독은 긴 작업에 디스크 또는 네트워크 연결에서 데이터를 읽는 등의 긴 대기 시간이 포함되어 있는 경우 특히 유용합니다. 간단한 작업 및 초과 구독에 대한 자세한 내용은 작업 스케줄러(동시성 런타임)를 참조하십시오.

[맨 위로 이동]

초과 구독을 사용하여 차단되거나 대기 시간이 긴 작업 오프셋

동시성 런타임에서는 작업이 서로 협조적으로 차단하고 양보할 수 있도록 하는 Concurrency::critical_section과 같은 동기화 기본 형식을 제공합니다. 하나의 작업이 협조적으로 차단하거나 양보하는 경우 첫 번째 작업이 데이터를 기다릴 때 작업 스케줄러에서 다른 컨텍스트에 처리 리소스를 다시 할당할 수 있습니다.

동시성 런타임에서 제공하는 협조적 차단 메커니즘을 사용할 수 없는 경우가 있습니다. 예를 들어 사용하는 외부 라이브러리에서 다른 동기화 메커니즘을 사용할 수 있습니다. Windows API ReadFile 함수를 사용하여 데이터 또는 네트워크 연결에서 데이터를 읽는 경우와 같이 대기 시간이 길 수 있는 작업을 수행하는 경우를 다른 예로 들 수 있습니다. 이러한 경우 초과 구독을 사용하여 다른 작업이 유휴 상태일 때 또 다른 작업이 실행되도록 설정할 수 있습니다. 초과 구독을 사용하면 사용 가능한 하드웨어 수보다 많은 스레드를 만들 수 있습니다.

지정된 URL에서 파일을 다운로드하는 download 함수를 살펴봅니다. 이 예제에서는 Concurrency::Context::Oversubscribe 메서드를 사용하여 활성 스레드 수를 임시로 증가시킵니다.

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());

   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

GetHttpFile 함수는 잠재적으로 숨어 있는 작업을 수행하므로 초과 구독을 통해 현재 작업이 데이터를 기다릴 때 다른 작업이 실행되도록 할 수 있습니다. 이 예제의 전체 버전은 방법: 초과 구독을 사용하여 대기 오프셋을 참조하십시오.

[맨 위로 이동]

가능한 경우 동시 메모리 관리 함수 사용

비교적 수명이 짧은 작은 개체를 자주 할당하는 세분화된 작업이 있는 경우 Concurrency::AllocConcurrency::Free와 같은 메모리 관리 함수를 사용합니다. 동시성 런타임은 각 실행 스레드를 위해 별도의 메모리 캐시를 보유합니다. AllocFree 함수는 잠금 또는 메모리 차단을 사용하지 않고도 이러한 캐시에서 메모리를 해제하거나 할당합니다.

이러한 메모리 관리 함수에 대한 자세한 내용은 작업 스케줄러(동시성 런타임)를 참조하십시오. 이러한 함수를 사용하는 예제를 보려면 방법: Alloc 및 Free를 사용하여 메모리 성능 개선을 참조하십시오.

[맨 위로 이동]

RAII을 사용하여 동시성 개체의 수명 관리

동시성 런타임은 예외 처리를 사용하여 취소와 같은 기능을 구현합니다. 따라서 런타임을 호출하는 경우 또는 런타임을 호출하는 다른 라이브러리를 호출하는 경우 예외로부터 안전한 코드를 작성하십시오.

RAII(Resource Acquisition Is Initialization) 패턴은 지정된 범위에서 동시성 개체의 수명을 안전하게 관리하는 한 가지 방법입니다. RAII 패턴에서는 데이터 구조가 스택에 할당됩니다. 이러한 데이터 구조는 만들어질 때 리소스를 초기화하거나 획득하고, 소멸될 때 리소스를 소멸시키거나 해제합니다. RAII 패턴을 사용하면 바깥쪽 범위를 벗어나기 전에 소멸자가 호출됩니다. 이 패턴은 함수에 return 문이 여러 개 포함된 경우에 유용합니다. 또한 이 패턴을 사용하여 예외로부터 안전한 코드를 작성할 수 있습니다. throw 문으로 인해 스택이 해제되면 RAII 개체의 소멸자가 호출되므로 리소스가 항상 올바르게 삭제되거나 해제됩니다.

런타임은 RAII 패턴을 사용하는 Concurrency::critical_section::scoped_lockConcurrency::reader_writer_lock::scoped_lock과 같은 여러 클래스를 정의합니다. 이러한 도우미 클래스를 범위 지정 잠금이라고 합니다. 이러한 클래스는 Concurrency::critical_section 또는 Concurrency::reader_writer_lock 개체를 사용할 때 여러 가지 이점을 제공합니다. 이러한 클래스의 생성자는 제공된 critical_section 또는 reader_writer_lock 개체에 대한 액세스 권한을 얻고 소멸자는 해당 개체에 대한 액세스를 해제합니다. 범위 지정 잠금은 상호 제외 개체가 삭제될 때 해당 개체에 대한 액세스를 자동으로 해제하므로 내부 개체의 잠금을 수동으로 해제하지 않습니다.

외부 라이브러리에서 정의되기 때문에 수정할 수 없는 account 클래스를 살펴봅니다.

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

다음 예제에서는 account 개체에 대한 여러 트랜잭션을 병렬로 수행합니다. account 클래스는 동시성이 보장되지 않으므로 이 예제에서는 critical_section 개체를 사용하여 account 개체에 대한 액세스를 동기화합니다. 각 병렬 작업은 critical_section::scoped_lock 개체를 사용하여 작업이 성공하거나 실패할 때 critical_section 개체가 잠금 해제되도록 합니다. 계정 잔고가 마이너스일 경우 예외를 throw하여 withdraw 작업이 실패합니다.

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace Concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

이 예제를 실행하면 다음과 같은 샘플 결과가 출력됩니다.

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
        negative balance: -76

RAII 패턴을 사용하여 동시성 개체의 수명을 관리하는 추가 예제를 보려면 연습: 사용자 인터페이스 스레드에서 작업 제거, 방법: 컨텍스트 클래스를 사용하여 공동 작업 세마포 구현방법: 초과 구독을 사용하여 대기 오프셋을 참조하십시오.

[맨 위로 이동]

전역 범위에서 동시성 개체를 만들지 않음

전역 범위에서 동시성 개체를 만들면 응용 프로그램에서 교착 상태가 발생할 수 있습니다.

동시성 런타임 개체를 만드는 경우 런타임은 아직 만들어진 기본 스케줄러가 없을 경우 만듭니다. 전역 개체를 생성하는 동안 만들어지는 런타임 개체의 경우에도 마찬가지입니다. 그러나 이 프로세스에는 내부 잠금이 사용되므로 동시성 런타임 인프라를 지원하는 다른 개체의 초기화를 방해할 수 있습니다. 아직 초기화되지 않은 다른 인프라 개체에 이 내부 잠금이 필요할 수 있으므로 응용 프로그램에서 교착 상태가 발생할 수 있습니다.

다음 예제에서는 전역 Concurrency::Scheduler 개체를 만드는 방법을 보여 줍니다. 이 패턴은 Scheduler 클래스뿐 아니라 동시성 런타임에서 제공하는 다른 모든 형식에도 적용됩니다. 이 패턴을 따를 경우 응용 프로그램이 교착 상태가 될 수 있으므로 이 패턴을 따르지 않는 것이 좋습니다.

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace Concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

Scheduler 개체를 만드는 올바른 방법의 예제를 보려면 작업 스케줄러(동시성 런타임)를 참조하십시오.

[맨 위로 이동]

공유 데이터 세그먼트에서 동시성 개체를 사용하지 않음

동시성 런타임에서는 data_seg #pragma 지시문을 통해 생성되는 데이터 섹션과 같은 공유 데이터 섹션에 동시성 개체를 사용하도록 지원하지 않습니다. 프로세스 경계 간에 공유되는 동시성 개체는 런타임을 일관성 없거나 잘못된 상태에 둘 수 있습니다.

[맨 위로 이동]

참고 항목

작업

방법: Alloc 및 Free를 사용하여 메모리 성능 개선

방법: 초과 구독을 사용하여 대기 오프셋

연습: 사용자 인터페이스 스레드에서 작업 제거

개념

동시성 런타임 유용한 정보

PPL(병렬 패턴 라이브러리)

비동기 에이전트 라이브러리

작업 스케줄러(동시성 런타임)

동기화 데이터 구조

동기화 데이터 구조와 Windows API의 비교

기타 리소스

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

병렬 패턴 라이브러리의 유용한 정보

비동기 에이전트 라이브러리의 유용한 정보

변경 기록

날짜

변경 내용

이유

2011년 3월

전역 범위에서 발생할 수 있는 교착 상태에 대한 정보를 추가했습니다.

고객 의견