병렬 패턴 라이브러리의 유용한 정보
이 문서에서는 PPL(병렬 패턴 라이브러리)을 효율적으로 사용하는 방법을 설명합니다. PPL은 세부적인 병렬 처리를 수행하기 위한 범용 컨테이너, 개체 및 알고리즘을 제공합니다.
PPL에 대한 자세한 내용은 PPL(병렬 패턴 라이브러리)을 참조하세요.
섹션
이 문서는 다음 섹션으로 구성됩니다.
작은 루프 본문을 병렬화하지 않음
비교적 작은 루프 본문을 평행화하면 관련된 예약 오버헤드가 병렬 처리의 이점보다 더 커질 수 있습니다. 두 배열에 각 요소 쌍을 추가하는 다음 예제를 고려해 보세요.
// small-loops.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
using namespace concurrency;
using namespace std;
int wmain()
{
// Create three arrays that each have the same size.
const size_t size = 100000;
int a[size], b[size], c[size];
// Initialize the arrays a and b.
for (size_t i = 0; i < size; ++i)
{
a[i] = i;
b[i] = i * 2;
}
// Add each pair of elements in arrays a and b in parallel
// and store the result in array c.
parallel_for<size_t>(0, size, [&a,&b,&c](size_t i) {
c[i] = a[i] + b[i];
});
// TODO: Do something with array c.
}
각 병렬 루프 반복에 대한 작업이 너무 작아서 병렬 처리 오버헤드의 이점이 없습니다. 루프 본문에서 더 많은 작업을 수행하거나 루프를 직렬로 수행하여 이 루프의 성능을 향상시킬 수 있습니다.
[맨 위로 이동]
가능한 가장 높은 수준의 Express 병렬 처리
낮은 수준에서만 코드를 병렬 처리하는 경우 프로세서 수가 증가할 때 크기가 조정되지 않는 분기-조인 구문이 도입될 수 있습니다. 포크 조인 구문은 하나의 작업이 작업을 더 작은 병렬 하위 작업으로 나누고 해당 하위 작업이 완료되기를 기다리는 구문입니다. 각 하위 작업은 재귀적으로 추가 하위 작업으로 나뉠 수 있습니다.
분기-조인 모델은 다양한 문제를 해결하는 데 유용할 수 있지만 동기화 오버헤드로 인해 확장성이 감소하는 경우도 있습니다. 예를 들어 이미지 데이터를 처리하는 다음 직렬 코드를 고려해 보세요.
// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
int width = bmp->GetWidth();
int height = bmp->GetHeight();
// Lock the bitmap.
BitmapData bitmapData;
Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);
// Get a pointer to the bitmap data.
DWORD* image_bits = (DWORD*)bitmapData.Scan0;
// Call the function for each pixel in the image.
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width; ++x)
{
// Get the current pixel value.
DWORD* curr_pixel = image_bits + (y * width) + x;
// Call the function.
f(*curr_pixel);
}
}
// Unlock the bitmap.
bmp->UnlockBits(&bitmapData);
}
각 루프 반복은 서로 독립적이기 때문에 다음 예제와 같이 대부분의 작업을 병렬 처리할 수 있습니다. 이 예제에서는 concurrency::p arallel_for 알고리즘을 사용하여 외부 루프를 병렬화합니다.
// Calls the provided function for each pixel in a Bitmap object.
void ProcessImage(Bitmap* bmp, const function<void (DWORD&)>& f)
{
int width = bmp->GetWidth();
int height = bmp->GetHeight();
// Lock the bitmap.
BitmapData bitmapData;
Rect rect(0, 0, bmp->GetWidth(), bmp->GetHeight());
bmp->LockBits(&rect, ImageLockModeWrite, PixelFormat32bppRGB, &bitmapData);
// Get a pointer to the bitmap data.
DWORD* image_bits = (DWORD*)bitmapData.Scan0;
// Call the function for each pixel in the image.
parallel_for (0, height, [&, width](int y)
{
for (int x = 0; x < width; ++x)
{
// Get the current pixel value.
DWORD* curr_pixel = image_bits + (y * width) + x;
// Call the function.
f(*curr_pixel);
}
});
// Unlock the bitmap.
bmp->UnlockBits(&bitmapData);
}
다음 예제에서는 루프에서 ProcessImage
함수를 호출하여 분기-조인 구문을 보여 줍니다. 각 하위 작업이 완료될 때까지 각 ProcessImage
호출이 반환되지 않습니다.
// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
병렬 루프의 각 반복이 거의 작업을 수행하지 않거나 병렬 루프에서 수행하는 작업의 균형이 맞지 않는 경우(즉, 일부 루프 반복이 다른 반복보다 오래 걸리는 경우) 자주 작업을 분기 및 조인하는 데 필요한 예약 오버헤드가 병렬 실행의 이점보다 클 수 있습니다. 프로세서 수가 증가하면 이 오버헤드도 증가합니다.
이 예제에서 예약 오버헤드를 줄이기 위해 내부 루프를 병렬 처리하기 전에 외부 루프를 병렬 처리하거나 파이프라인과 같은 다른 병렬 구문을 사용할 수 있습니다. 다음 예제에서는 concurrency::p arallel_for_each 알고리즘을 사용하여 외부 루프를 병렬화하도록 함수를 수정 ProcessImages
합니다.
// Processes each bitmap in the provided vector.
void ProcessImages(vector<Bitmap*> bitmaps, const function<void (DWORD&)>& f)
{
parallel_for_each(begin(bitmaps), end(bitmaps), [&f](Bitmap* bmp) {
ProcessImage(bmp, f);
});
}
파이프라인을 사용하여 이미지 처리를 병렬로 수행하는 유사한 예제는 연습: 이미지 처리 네트워크 만들기를 참조하세요.
[맨 위로 이동]
parallel_invoke 사용하여 분열 및 정복 문제 해결
분할 및 정복 문제는 재귀를 사용하여 작업을 하위 작업으로 분할하는 포크 조인 구문의 한 형태입니다. 동시성::task_group 및 동시성::structured_task_group 클래스 외에도 동시성::p arallel_invoke 알고리즘을 사용하여 나누기 및 정복 문제를 해결할 수도 있습니다. parallel_invoke
알고리즘은 작업 그룹 개체보다 구문이 더 간결하며 고정된 개수의 병렬 작업이 있는 경우에 유용합니다.
다음 예제에서는 parallel_invoke
알고리즘을 사용하여 바이토닉 정렬 알고리즘을 구현하는 방법을 보여 줍니다.
// Sorts the given sequence in the specified order.
template <class T>
void parallel_bitonic_sort(T* items, int lo, int n, bool dir)
{
if (n > 1)
{
// Divide the array into two partitions and then sort
// the partitions in different directions.
int m = n / 2;
parallel_invoke(
[&] { parallel_bitonic_sort(items, lo, m, INCREASING); },
[&] { parallel_bitonic_sort(items, lo + m, m, DECREASING); }
);
// Merge the results.
parallel_bitonic_merge(items, lo, n, dir);
}
}
오버헤드를 줄이기 위해 parallel_invoke
알고리즘은 호출 컨텍스트에서 일련의 작업 중 마지막 작업을 수행합니다.
이 예제의 전체 버전은 방법: parallel_invoke 사용하여 병렬 정렬 루틴 작성을 참조 하세요. 알고리즘에 대한 parallel_invoke
자세한 내용은 병렬 알고리즘을 참조 하세요.
[맨 위로 이동]
취소 또는 예외 처리를 사용하여 병렬 루프에서 중단
PPL은 작업 그룹 또는 병렬 알고리즘에서 수행하는 병렬 작업을 취소하는 두 가지 방법을 제공합니다. 한 가지 방법은 동시성::task_group 및 동시성::structured_task_group 클래스에서 제공하는 취소 메커니즘을 사용하는 것입니다. 다른 방법은 작업 함수의 본문에서 예외를 발생시키는 것입니다. 병렬 작업 트리를 취소하는 경우 취소 메커니즘이 예외 처리보다 더 효율적입니다. 병렬 작업 트리는 일부 작업 그룹에 다른 작업 그룹이 포함된 관련 작업 그룹의 그룹입니다. 취소 메커니즘은 하향식으로 작업 그룹 및 자식 작업 그룹을 취소합니다. 반대로 예외 처리는 상향식으로 작동하고 예외가 위쪽으로 전파될 때 각 자식 작업 그룹을 개별적으로 취소해야 합니다.
작업 그룹 개체로 직접 작업하는 경우 동시성::task_group::cancel 또는 concurrency::structured_task_group::cancel 메서드를 사용하여 해당 작업 그룹에 속한 작업을 취소합니다. 병렬 알고리즘(예: parallel_for
)을 취소하려면 부모 작업 그룹을 만들고 해당 작업 그룹을 취소합니다. 예를 들어 병렬로 배열에서 값을 검색하는 parallel_find_any
함수를 고려해 보세요.
// Returns the position in the provided array that contains the given value,
// or -1 if the value is not in the array.
template<typename T>
int parallel_find_any(const T a[], size_t count, const T& what)
{
// The position of the element in the array.
// The default value, -1, indicates that the element is not in the array.
int position = -1;
// Call parallel_for in the context of a cancellation token to search for the element.
cancellation_token_source cts;
run_with_cancellation_token([count, what, &a, &position, &cts]()
{
parallel_for(std::size_t(0), count, [what, &a, &position, &cts](int n) {
if (a[n] == what)
{
// Set the return value and cancel the remaining tasks.
position = n;
cts.cancel();
}
});
}, cts.get_token());
return position;
}
병렬 알고리즘에서 작업 그룹을 사용하므로 병렬 반복 중 하나가 부모 작업 그룹을 취소하는 경우 전체 작업이 취소됩니다. 이 예제의 전체 버전은 방법: 취소를 사용하여 병렬 루프에서 중단을 참조 하세요.
병렬 작업을 취소하는 경우 예외 처리가 취소 메커니즘보다 덜 효율적인 방법이지만 예외 처리가 적합한 경우도 있습니다. 예를 들어 for_all
메서드는 tree
구조의 각 노드에서 작업 함수를 재귀적으로 수행합니다. 이 예제 _children
에서 데이터 멤버는 개체를 포함하는 std::list 입니다 tree
.
// Performs the given work function on the data element of the tree and
// on each child.
template<class Function>
void tree::for_all(Function& action)
{
// Perform the action on each child.
parallel_for_each(begin(_children), end(_children), [&](tree& child) {
child.for_all(action);
});
// Perform the action on this node.
action(*this);
}
tree::for_all
메서드의 호출자는 트리의 각 요소에 대해 작업 함수를 호출할 필요가 없는 경우 예외를 발생시킬 수 있습니다. 다음 예제에서는 제공된 tree
개체에서 값을 검색하는 search_for_value
함수를 보여 줍니다. search_for_value
함수는 트리의 현재 요소가 제공된 값과 일치하는 경우 예외를 발생시키는 작업 함수를 사용합니다. search_for_value
함수는 try-catch
블록을 사용하여 예외를 캡처하고 결과를 콘솔에 출력합니다.
// Searches for a value in the provided tree object.
template <typename T>
void search_for_value(tree<T>& t, int value)
{
try
{
// Call the for_all method to search for a value. The work function
// throws an exception when it finds the value.
t.for_all([value](const tree<T>& node) {
if (node.get_data() == value)
{
throw &node;
}
});
}
catch (const tree<T>* node)
{
// A matching node was found. Print a message to the console.
wstringstream ss;
ss << L"Found a node with value " << value << L'.' << endl;
wcout << ss.str();
return;
}
// A matching node was not found. Print a message to the console.
wstringstream ss;
ss << L"Did not find node with value " << value << L'.' << endl;
wcout << ss.str();
}
이 예제의 전체 버전은 방법: 예외 처리를 사용하여 병렬 루프에서 중단을 참조하세요.
PPL에서 제공하는 취소 및 예외 처리 메커니즘에 대한 자세한 내용은 PPL 및 예외 처리의 취소를 참조하세요.
[맨 위로 이동]
취소 및 예외 처리가 개체 소멸에 미치는 영향 이해
병렬 작업 트리에서 취소된 작업은 자식 작업이 실행되지 않도록 합니다. 따라서 자식 작업 중 하나가 리소스 해제와 같이 애플리케이션에 중요한 작업을 수행하는 경우 문제가 발생할 수 있습니다. 또한 작업 취소로 인해 예외가 개체 소멸자를 통해 전파되고 애플리케이션에서 정의되지 않은 동작이 발생할 수 있습니다.
다음 예제에서 Resource
클래스는 리소스를 설명하고 Container
클래스는 리소스를 보유하는 컨테이너를 설명합니다. 해당 소멸자에서 Container
클래스는 Resource
멤버 중 두 개에서 cleanup
메서드를 병렬로 호출한 다음 세 번째 Resource
멤버에서 cleanup
메서드를 호출합니다.
// parallel-resource-destruction.h
#pragma once
#include <ppl.h>
#include <sstream>
#include <iostream>
// Represents a resource.
class Resource
{
public:
Resource(const std::wstring& name)
: _name(name)
{
}
// Frees the resource.
void cleanup()
{
// Print a message as a placeholder.
std::wstringstream ss;
ss << _name << L": Freeing..." << std::endl;
std::wcout << ss.str();
}
private:
// The name of the resource.
std::wstring _name;
};
// Represents a container that holds resources.
class Container
{
public:
Container(const std::wstring& name)
: _name(name)
, _resource1(L"Resource 1")
, _resource2(L"Resource 2")
, _resource3(L"Resource 3")
{
}
~Container()
{
std::wstringstream ss;
ss << _name << L": Freeing resources..." << std::endl;
std::wcout << ss.str();
// For illustration, assume that cleanup for _resource1
// and _resource2 can happen concurrently, and that
// _resource3 must be freed after _resource1 and _resource2.
concurrency::parallel_invoke(
[this]() { _resource1.cleanup(); },
[this]() { _resource2.cleanup(); }
);
_resource3.cleanup();
}
private:
// The name of the container.
std::wstring _name;
// Resources.
Resource _resource1;
Resource _resource2;
Resource _resource3;
};
이 패턴 자체에는 문제가 없지만 두 작업을 병렬로 실행하는 다음 코드를 고려해 보세요. 첫 번째 작업은 Container
개체를 만들고 두 번째 작업은 전체 작업을 취소합니다. 예를 들어 이 예제에서는 두 개의 동시성::event 개체를 사용하여 개체를 만든 후에 Container
취소가 발생하고 취소 작업이 발생한 후 개체가 Container
제거되도록 합니다.
// parallel-resource-destruction.cpp
// compile with: /EHsc
#include "parallel-resource-destruction.h"
using namespace concurrency;
using namespace std;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a task_group that will run two tasks.
task_group tasks;
// Used to synchronize the tasks.
event e1, e2;
// Run two tasks. The first task creates a Container object. The second task
// cancels the overall task group. To illustrate the scenario where a child
// task is not run because its parent task is cancelled, the event objects
// ensure that the Container object is created before the overall task is
// cancelled and that the Container object is destroyed after the overall
// task is cancelled.
tasks.run([&tasks,&e1,&e2] {
// Create a Container object.
Container c(L"Container 1");
// Allow the second task to continue.
e2.set();
// Wait for the task to be cancelled.
e1.wait();
});
tasks.run([&tasks,&e1,&e2] {
// Wait for the first task to create the Container object.
e2.wait();
// Cancel the overall task.
tasks.cancel();
// Allow the first task to continue.
e1.set();
});
// Wait for the tasks to complete.
tasks.wait();
wcout << L"Exiting program..." << endl;
}
이 예제는 다음과 같은 출력을 생성합니다.
Container 1: Freeing resources...Exiting program...
이 코드 예제에는 예상과 다르게 동작하게 만들 수 있는 다음과 같은 문제가 포함되어 있습니다.
부모 작업이 취소되면 자식 작업인 concurrency::p arallel_invoke 호출도 취소됩니다. 따라서 이러한 두 리소스가 해제되지 않습니다.
부모 작업을 취소하면 자식 작업에서 내부 예외가 발생합니다.
Container
소멸자는 이 예외를 처리하지 않으므로 예외가 상향 전파되고 세 번째 리소스가 해제되지 않습니다.자식 작업에서 발생하는 예외는
Container
소멸자를 통해 전파됩니다. 소멸자에서 발생하면 애플리케이션이 정의되지 않은 상태가 됩니다.
이러한 작업이 취소되지 않도록 보장할 수 없는 경우 리소스 해제와 같은 중요한 작업을 수행하지 않는 것이 좋습니다. 또한 형식의 소멸자에서 발생할 수 있는 런타임 기능을 사용하지 않는 것이 좋습니다.
[맨 위로 이동]
병렬 루프에서 반복적으로 차단 안 함
동시성::p arallel_for 또는 concurrency::p arallel_for_each와 같은 병렬 루프는 차단 작업으로 인해 런타임이 짧은 시간 동안 많은 스레드를 만들 수 있습니다.
작업이 완료되거나 협조적으로 차단 또는 양보하는 경우 동시성 런타임에서 추가 작업을 수행합니다. 하나의 병렬 루트 반복이 차단되는 경우 런타임에서 다른 반복을 시작할 수 있습니다. 사용 가능한 유휴 스레드가 없는 경우 런타임에서 새 스레드를 만듭니다.
병렬 루프의 본문이 가끔 차단되는 경우 이 메커니즘은 전반적인 작업 처리량을 최대화하는 데 도움이 됩니다. 그러나 많은 반복이 차단되는 경우 런타임에서 추가 작업을 실행할 많은 스레드를 만들 수 있습니다. 이 경우 메모리 부족 상태 또는 하드웨어 리소스의 사용률 저하가 발생할 수 있습니다.
루프의 각 반복에서 동시성::send 함수를 호출하는 다음 예제를 parallel_for
고려해 보세요. send
는 협조적으로 차단되기 때문에 런타임에서 send
가 호출될 때마다 추가 작업을 실행할 새 스레드를 만듭니다.
// repeated-blocking.cpp
// compile with: /EHsc
#include <ppl.h>
#include <agents.h>
using namespace concurrency;
static_assert(false, "This example illustrates a non-recommended practice.");
int main()
{
// Create a message buffer.
overwrite_buffer<int> buffer;
// Repeatedly send data to the buffer in a parallel loop.
parallel_for(0, 1000, [&buffer](int i) {
// The send function blocks cooperatively.
// We discourage the use of repeated blocking in a parallel
// loop because it can cause the runtime to create
// a large number of threads over a short period of time.
send(buffer, i);
});
}
이 패턴을 방지하려면 코드를 리팩터링하는 것이 좋습니다. 이 예제에서는 직렬 for
루프에서 send
를 호출하여 추가 스레드 생성을 방지할 수 있습니다.
[맨 위로 이동]
병렬 작업을 취소할 때 차단 작업 수행 안 함
가능하면 동시성::task_group::cancel 또는 동시성::structured_task_group::cancel 메서드를 호출하여 병렬 작업을 취소하기 전에 차단 작업을 수행하지 마세요.
작업(task)이 협조적 차단 작업(operation)을 수행하면 첫 번째 작업(task)이 데이터를 기다리는 동안 런타임에서 다른 작업(work)을 수행할 수 있습니다. 대기 중인 작업이 차단 해제되면 런타임에서 다시 예약합니다. 일반적으로 런타임은 이전에 차단 해제된 작업을 다시 예약하기 전에 최근에 차단 해제된 작업을 다시 예약합니다. 따라서 런타임이 차단 작업 중에 불필요한 작업을 예약하여 성능 저하가 발생할 수 있습니다. 병렬 작업을 취소하기 전에 차단 작업을 수행하는 경우 차단 작업으로 인해 cancel
호출이 지연될 수 있습니다. 이 경우 다른 작업이 불필요한 작업을 수행합니다.
제공된 조건자 함수를 충족하는 제공된 배열의 요소를 검색하는 parallel_find_answer
함수를 정의하는 다음 예제를 고려해 보세요. 조건자 함수가 반환 true
되면 병렬 작업 함수는 개체를 Answer
만들고 전체 작업을 취소합니다.
// blocking-cancel.cpp
// compile with: /c /EHsc
#include <windows.h>
#include <ppl.h>
using namespace concurrency;
// Encapsulates the result of a search operation.
template<typename T>
class Answer
{
public:
explicit Answer(const T& data)
: _data(data)
{
}
T get_data() const
{
return _data;
}
// TODO: Add other methods as needed.
private:
T _data;
// TODO: Add other data members as needed.
};
// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
// The result of the search.
Answer<T>* answer = nullptr;
// Ensures that only one task produces an answer.
volatile long first_result = 0;
// Use parallel_for and a task group to search for the element.
structured_task_group tasks;
tasks.run_and_wait([&]
{
// Declare the type alias for use in the inner lambda function.
typedef T T;
parallel_for<size_t>(0, count, [&](const T& n) {
if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
{
// Create an object that holds the answer.
answer = new Answer<T>(a[n]);
// Cancel the overall task.
tasks.cancel();
}
});
});
return answer;
}
new
연산자는 차단될 수 있는 힙 할당을 수행합니다. 런타임은 태스크가 동시성::critical_section::lock 호출과 같은 협조적 차단 호출을 수행할 때만 다른 작업을 수행합니다.
다음 예제에서는 불필요한 작업을 방지하여 성능을 향상시키는 방법을 보여 줍니다. 이 예제는 Answer
개체에 대한 스토리지를 할당하기 전에 작업 그룹을 취소합니다.
// Searches for an element of the provided array that satisfies the provided
// predicate function.
template<typename T, class Predicate>
Answer<T>* parallel_find_answer(const T a[], size_t count, const Predicate& pred)
{
// The result of the search.
Answer<T>* answer = nullptr;
// Ensures that only one task produces an answer.
volatile long first_result = 0;
// Use parallel_for and a task group to search for the element.
structured_task_group tasks;
tasks.run_and_wait([&]
{
// Declare the type alias for use in the inner lambda function.
typedef T T;
parallel_for<size_t>(0, count, [&](const T& n) {
if (pred(a[n]) && InterlockedExchange(&first_result, 1) == 0)
{
// Cancel the overall task.
tasks.cancel();
// Create an object that holds the answer.
answer = new Answer<T>(a[n]);
}
});
});
return answer;
}
[맨 위로 이동]
병렬 루프에서 공유 데이터에 쓰기 안 함
동시성 런타임은 공유 데이터에 대한 동시 액세스를 동기화하는 여러 데이터 구조(예 : 동시성::critical_section)를 제공합니다. 이러한 데이터 구조는 여러 작업이 가끔 리소스에 대한 공유 액세스를 요구하는 경우 등 여러 경우에서 유용합니다.
concurrency::p arallel_for_each 알고리즘과 critical_section
개체를 사용하여 std::array 개체의 소수 개수를 계산하는 다음 예제를 살펴보겠습니다. 이 예제는 각 스레드가 공유 변수 prime_sum
에 액세스하기 위해 대기해야 하므로 크기가 조정되지 않습니다.
critical_section cs;
prime_sum = 0;
parallel_for_each(begin(a), end(a), [&](int i) {
cs.lock();
prime_sum += (is_prime(i) ? i : 0);
cs.unlock();
});
이 예제에서는 잦은 잠금 작업이 사실상 루프를 직렬화하기 때문에 성능이 저하될 수도 있습니다. 또한 동시성 런타임 개체가 차단 작업을 수행하는 경우 첫 번째 스레드가 데이터를 기다리는 동안 스케줄러에서 다른 작업을 수행할 추가 스레드를 만들 수 있습니다. 많은 작업이 공유 데이터를 대기 중이어서 런타임에서 많은 스레드를 만드는 경우 애플리케이션 성능이 저하되거나 리소스 부족 상태가 될 수 있습니다.
PPL은 공유 리소스에 대한 액세스를 잠금 없는 방식으로 제공하여 공유 상태를 제거하는 데 도움이 되는 동시성::결합 가능한 클래스를 정의합니다. combinable
클래스는 세분화된 계산을 수행한 다음 이러한 계산을 최종 결과로 병합할 수 있게 해주는 스레드 로컬 스토리지를 제공합니다. combinable
개체는 환산(reduction) 변수로 간주될 수 있습니다.
다음 예제에서는 critical_section
개체 대신 combinable
개체로 합계를 계산하여 앞의 예제를 수정합니다. 이 예제는 각 스레드가 합계의 자체 로컬 복사본을 유지하기 때문에 크기가 조정됩니다. 이 예제에서는 동시성::combinable::combine 메서드를 사용하여 로컬 계산을 최종 결과에 병합합니다.
combinable<int> sum;
parallel_for_each(begin(a), end(a), [&](int i) {
sum.local() += (is_prime(i) ? i : 0);
});
prime_sum = sum.combine(plus<int>());
이 예제의 전체 버전은 방법: 결합 가능을 사용하여 성능 향상을 참조하세요. 클래스에 대한 combinable
자세한 내용은 병렬 컨테이너 및 개체를 참조 하세요.
[맨 위로 이동]
가능하면 거짓 공유를 방지합니다.
거짓 공유 는 별도의 프로세서에서 실행되는 여러 동시 작업이 동일한 캐시 줄에 있는 변수에 쓸 때 발생합니다. 한 작업이 변수 중 하나에 쓰는 경우 두 변수에 대한 캐시 라인이 모두 무효화됩니다. 캐시 라인이 무효화될 때마다 각 프로세서가 캐시 라인을 다시 로드해야 합니다. 따라서 거짓 공유로 인해 애플리케이션 성능이 저하될 수 있습니다.
다음 기본 예제에서는 각각 공유 카운터 변수를 증가시키는 두 개의 동시 작업을 보여 줍니다.
volatile long count = 0L;
concurrency::parallel_invoke(
[&count] {
for(int i = 0; i < 100000000; ++i)
InterlockedIncrement(&count);
},
[&count] {
for(int i = 0; i < 100000000; ++i)
InterlockedIncrement(&count);
}
);
두 작업 간에 데이터 공유를 제거하기 위해 두 개의 카운터 변수를 사용하도록 예제를 수정할 수 있습니다. 이 예제에서는 작업이 완료된 후 최종 카운터 값을 계산합니다. 그러나 이 예제에서는 count1
및 count2
변수가 동일한 캐시 라인에 있을 가능성이 크기 때문에 거짓 공유를 보여 줍니다.
long count1 = 0L;
long count2 = 0L;
concurrency::parallel_invoke(
[&count1] {
for(int i = 0; i < 100000000; ++i)
++count1;
},
[&count2] {
for(int i = 0; i < 100000000; ++i)
++count2;
}
);
long count = count1 + count2;
거짓 공유를 제거하는 한 가지 방법은 카운터 변수가 개별 캐시 라인에 있도록 하는 것입니다. 다음 예제에서는 count1
및 count2
변수를 64바이트 경계에 맞춥니다.
__declspec(align(64)) long count1 = 0L;
__declspec(align(64)) long count2 = 0L;
concurrency::parallel_invoke(
[&count1] {
for(int i = 0; i < 100000000; ++i)
++count1;
},
[&count2] {
for(int i = 0; i < 100000000; ++i)
++count2;
}
);
long count = count1 + count2;
이 예제에서는 메모리 캐시의 크기가 64바이트 이하라고 가정합니다.
작업 간에 데이터를 공유해야 하는 경우 동시성::결합 가능한 클래스를 사용하는 것이 좋습니다. combinable
클래스는 거짓 공유의 가능성이 더 낮도록 스레드 지역 변수를 만듭니다. 클래스에 대한 combinable
자세한 내용은 병렬 컨테이너 및 개체를 참조 하세요.
[맨 위로 이동]
작업 수명 동안 변수가 유효한지 확인합니다.
작업 그룹 또는 병렬 알고리즘에 람다 식을 제공하는 경우 캡처 절은 람다 식의 본문이 값이나 참조로 바깥쪽 범위의 변수에 액세스하는지를 지정합니다. 변수를 참조로 람다 식에 전달하는 경우 작업이 완료될 때까지 해당 변수의 수명이 유지되도록 보장해야 합니다.
object
클래스 및 perform_action
함수를 정의하는 다음 예제를 고려해 보세요. perform_action
함수는 object
변수를 만들고 해당 변수에 대해 비동기적으로 일부 작업을 수행합니다. perform_action
함수가 반환되기 전에 작업이 완료되도록 보장되지 않으므로 작업을 실행 중일 때 object
변수가 제거되면 프로그램 작동이 중단되거나 지정되지 않은 동작이 나타납니다.
// lambda-lifetime.cpp
// compile with: /c /EHsc
#include <ppl.h>
using namespace concurrency;
// A type that performs an action.
class object
{
public:
void action() const
{
// TODO: Details omitted for brevity.
}
};
// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable asynchronously.
object obj;
tasks.run([&obj] {
obj.action();
});
// NOTE: The object variable is destroyed here. The program
// will crash or exhibit unspecified behavior if the task
// is still running when this function returns.
}
애플리케이션의 요구 사항에 따라 다음 기술 중 하나를 사용하여 변수가 각 작업의 전체 수명 동안 유효하도록 보장할 수 있습니다.
다음 예제에서는 값으로 object
변수를 작업에 전달합니다. 따라서 작업이 변수의 자체 복사본에서 작동합니다.
// Performs an action asynchronously.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable asynchronously.
object obj;
tasks.run([obj] {
obj.action();
});
}
object
변수가 값으로 전달되기 때문에 이 변수에 발생하는 상태 변경 내용은 원래 복사본에 나타나지 않습니다.
다음 예제에서는 동시성::task_group::wait 메서드를 사용하여 함수가 반환되기 전에 perform_action
작업이 완료되는지 확인합니다.
// Performs an action.
void perform_action(task_group& tasks)
{
// Create an object variable and perform some action on
// that variable.
object obj;
tasks.run([&obj] {
obj.action();
});
// Wait for the task to finish.
tasks.wait();
}
이제 함수가 반환되기 전에 작업이 완료되기 때문에 perform_action
함수가 더 이상 비동기적으로 동작하지 않습니다.
다음 예제에서는 object
변수에 대한 참조를 사용하도록 perform_action
함수를 수정합니다. 호출자는 작업이 완료될 때까지 object
변수의 수명이 유효하도록 보장해야 합니다.
// Performs an action asynchronously.
void perform_action(object& obj, task_group& tasks)
{
// Perform some action on the object variable.
tasks.run([&obj] {
obj.action();
});
}
포인터를 사용하여 작업 그룹 또는 병렬 알고리즘에 전달하는 개체의 수명을 제어할 수도 있습니다.
람다 식에 대한 자세한 내용은 람다 식을 참조하세요.
[맨 위로 이동]
참고 항목
동시성 런타임 유용한 정보
PPL(병렬 패턴 라이브러리)
병렬 컨테이너 및 개체
병렬 알고리즘
PPL에서의 취소
예외 처리
연습: 이미지 처리 네트워크 만들기
방법: parallel_invoke를 사용하여 병렬 정렬 루틴 작성
방법: 취소를 사용하여 병렬 루프 중단
방법: combinable을 사용하여 성능 개선
비동기 에이전트 라이브러리의 모범 사례
동시성 런타임의 유용한 일반 정보