Share via


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

이 문서에서는 동시성 런타임의 여러 영역에 적용되는 모범 사례를 설명합니다.

섹션

이 문서는 다음 섹션으로 구성됩니다.

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

동시성 런타임은 외부 동기화 개체가 필요하지 않은 많은 동시성 안전 구문을 제공합니다. 예를 들어 동시성::concurrent_vector 클래스는 동시성 안전 추가 및 요소 액세스 작업을 제공합니다. 여기서 동시성 안전은 포인터 또는 반복기가 항상 유효함을 의미합니다. 요소 초기화 또는 특정 순회 순서를 보장하지 않습니다. 그러나 리소스에 대한 단독 액세스가 필요한 경우 런타임은 동시성::critical_section, 동시성::reader_writer_lock동시성::이벤트 클래스를 제공합니다. 이러한 형식은 협조적으로 동작합니다. 따라서 작업 스케줄러는 첫 번째 작업이 데이터를 대기할 때 처리 리소스를 다른 컨텍스트로 다시 할당할 수 있습니다. 가능하면 협조적으로 동작하지 않는 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

두 작업 간의 협력을 활성화하는 방법에는 여러 가지가 있습니다. 한 가지 방법은 장기 실행 작업에서 때때로 작업 스케줄러에 양보하는 것입니다. 다음 예제에서는 다른 작업이 실행될 수 있도록 동시성::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 현재 스레드가 속한 스케줄러, 경량 작업 또는 다른 운영 체제 스레드에서 다른 활성 스레드만 생성합니다. 이 메서드는 동시성::task_group 또는 동시성::structured_task_group 개체에서 실행되도록 예약되었지만 아직 시작되지 않은 작업을 생성하지 않습니다.

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

[맨 위로 이동]

초과 구독을 사용하여 대기 시간이 높거나 차단되는 작업을 오프셋합니다.

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

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

지정된 URL에서 파일을 다운로드하는 다음 함수 download를 고려합니다. 이 예제에서는 동시성::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 잠재적으로 대기 중인 작업을 수행하므로 초과 구독을 사용하면 현재 태스크가 데이터를 대기할 때 다른 작업을 실행할 수 있습니다. 이 예제 의 전체 버전은 방법: 초과 구독을 사용하여 오프셋 대기 시간을 참조하세요.

[맨 위로 이동]

가능하면 동시 메모리 관리 함수 사용

메모리 관리 함수, 동시성::Alloc동시성::Free를 사용합니다. 비교적 짧은 수명을 가진 작은 개체를 자주 할당하는 세분화된 작업이 있는 경우 동시성 런타임은 실행 중인 각 스레드에 대해 별도의 메모리 캐시를 보유합니다. 및 Free 함수는 Alloc 잠금 또는 메모리 장벽을 사용하지 않고 이러한 캐시에서 메모리를 할당하고 해제합니다.

이러한 메모리 관리 함수에 대한 자세한 내용은 작업 스케줄러를 참조 하세요. 이러한 함수를 사용하는 예제는 방법: Alloc 및 Free를 사용하여 메모리 성능 향상을 참조하세요.

[맨 위로 이동]

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

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

RAII(리소스 취득 초기화) 패턴은 지정된 범위에서 동시성 개체의 수명을 안전하게 관리하는 한 가지 방법입니다. RAII 패턴에 따라 데이터 구조가 스택에 할당됩니다. 해당 데이터 구조는 생성될 때 리소스를 초기화하거나 획득하고 데이터 구조가 소멸될 때 해당 리소스를 삭제하거나 해제합니다. RAII 패턴은 바깥쪽 범위가 종료되기 전에 소멸자가 호출되도록 보장합니다. 이 패턴은 함수에 여러 return 문이 포함된 경우에 유용합니다. 이 패턴은 예외로부터 안전한 코드를 작성하는 데도 도움이 됩니다. throw 문이 스택을 해제하면 RAII 개체의 소멸자가 호출되므로 리소스가 항상 올바르게 삭제되거나 해제됩니다.

런타임은 RAII 패턴을 사용하는 여러 클래스(예 : 동시성::critical_section::scoped_lock동시성::reader_writer_lock::scoped_lock)를 정의합니다. 이러한 도우미 클래스를 범위가 지정된 잠금이라고 합니다. 이러한 클래스는 동시성::critical_section 또는 동시성::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 수행합니다. 이 예제에서는 클래스가 동시성로부터 안전하지 않으므로 개체를 사용하여 critical_section 개체에 account 대한 액세스를 account 동기화합니다. 각 병렬 작업은 개체를 critical_section::scoped_lock 사용하여 작업이 성공하거나 실패할 때 개체의 잠금이 해제되도록 보장 critical_section 합니다. 계정 잔액이 음수이면 예외를 withdraw throw하여 작업이 실패합니다.

// 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 패턴을 사용하여 동시성 개체 의 수명을 관리하는 추가 예제는 연습: 사용자 인터페이스 스레드에서 작업 제거, 방법: 컨텍스트 클래스를 사용하여 협조적 세마포 구현 및 방법: 초과 구독을 사용하여 대기 시간을 오프셋하는 방법을 참조하세요.

[맨 위로 이동]

전역 범위에서 동시성 개체를 만들지 마세요.

전역 범위에서 동시성 개체를 만드는 경우 교착 상태나 메모리 액세스 위반 등의 문제가 애플리케이션에서 발생할 수 있습니다.

예를 들어 동시성 런타임 개체를 만드는 경우 런타임은 아직 만들어지지 않은 경우 기본 스케줄러를 만듭니다. 전역 개체 생성 중에 만들어지는 런타임 개체가 적절하게 런타임이 이 기본 스케줄러를 만들게 합니다. 그러나 이 프로세스에는 내부 잠금이 사용되므로 동시성 런타임 인프라를 지원하는 다른 개체의 초기화를 방해할 수 있습니다. 이 내부 잠금은 아직 초기화되지 않은 다른 인프라 개체에 필요할 수 있기 때문에 교착 상태가 애플리케이션에서 발생할 수 있습니다.

다음 예제에서는 전역 동시성::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 지시문에 의해 만들어진 데이터 섹션)에서 동시성 개체의 사용을 지원하지 않습니다. 프로세스 경계 간에 공유되는 동시성 개체는 런타임을 일관되지 않거나 잘못된 상태로 만들 수 있습니다.

[맨 위로 이동]

참고 항목

동시성 런타임 유용한 정보
PPL(병렬 패턴 라이브러리)
비동기 에이전트 라이브러리
작업 Scheduler
동기화 데이터 구조
동기화 데이터 구조와 Windows API의 비교
방법: Alloc 및 Free를 사용하여 메모리 성능 개선
방법: 초과 구독을 사용하여 대기 오프셋
방법: 컨텍스트 클래스를 사용하여 공동 작업 세마포 구현
연습: 사용자 인터페이스 스레드에서 작업 제거
병렬 패턴 라이브러리의 유용한 정보
비동기 에이전트 라이브러리의 모범 사례