|
When starting a new project, do you ask yourself whether your program will be compute-bound or I/O-bound?
새로운 프로젝트를 시작하려고 할때 여러분의 프로그램이 계산-기반적 또는 I/O-기반의 프로그램이 될 지 생각해 보았는가?
You should. I’ve found that in most cases it’s either one or the other.
나는 대부분의 경우 이 둘 중 하나임을 발견했다.
You might be working on an analytics library that’s handed a pile of data and keeps a bunch of processors busy while crunching it all down to a set of aggregates.
분석적 라이브러리 작성은 일단의 데이터를 가지고 프로세서 부하를 일으키며 결과를 내도록 한다.
Alternatively, your code might spend most of its time waiting for stuff to happen, for data to arrive over the network, a user to click on something or perform some complex six-fingered gesture.
또는 데이터가 네트워크로 들어오거나 사용자 클릭 또는 제스쳐 입력을 대기하는 데 대부분의 시간을 보내는 코드일 수 있다.
In this case, the threads in your program aren’t doing very much.
이러한 경우에 여러분 프로그램의 스레드는 많은 일을 수행하지 않는다.
Sure, there are cases where programs are heavy both on I/O and computation.
물론 I/O와 계산 모두에 많은 시간을 소모하는 프로그램도 있다.
The SQL Server database engine comes to mind, but that’s less typical of today’s computer programming.
이를테면 SQL 서버 데이터 엔진이 그러한데, 이것은 오늘날의 컴퓨터 프로그래밍에 있어 흔한 형태는 아니다.
More often than not, your program is tasked with coordinating the work of others.
그보다는 많은 경우에, 여러분 프로그램은 다른 작업들을 조정하는 업무를 하게 된다.
It might be a Web server or client communicating with a SQL database, pushing some computation to the GPU or presenting some content for the user to interact with.
GPU에게 계산을 맏기거나 사용자 인터페이스를 위한 콘텐즈를 제시하는 SQL 데이터베이스와 통신하는 웹 서버 또는 클라이언트가 그런 예이다.
Given all of these different scenarios, how do you decide what threading capabilities your program requires and what concurrency building blocks are necessary or useful?
이렇게 다양한 시나리오 속에서, 여러분은 어떻게 어떤 스레딩 능력이 필요하고 어떤 동시 빌딩 블럭이 유용하고 필요하게 될지 결정할 수 있겠는가?
Well, that’s a tough question to answer generally and something you’ll need to analyze as you approach a new project.
그것은 어려운 질문이고, 새로운 프로젝트를 시작할 때 분석되어야 할 과제이다.
It’s helpful, however, to understand the evolution of threading in Windows and C++ so you can make an informed decision based on the practical choices that are available.
그렇지만 윈도우즈와 C++에서 스레딩의 발전 과정을 이해하는 것은 도움이 된다. 그럼으로 해서 주어진 정보에 기반하여 실질적인 판단을 할 수 있을 것이다.
Threads, of course, provide no direct value whatsoever to the user.
당연히, 스레드는 사용자에게 어떤 직접적인 가치를 제공하지는 않는다.
Your program is no more awesome if you use twice as many threads as another program.
여러분 프로그램이 다른 프로그램보다 두 배의 스레드를 사용한다고 프로그램이 훌륭해지는 것은 아니다.
It’s what you do with those threads that counts.
중요한 것은 그 스레드들이 무엇을 하느냐는 것이다.
To illustrate these ideas and the way in which threading has evolved over time, let me take the example of reading some data from a file.
예를 들어, 파일로부터 데이터를 읽는 경우를 보자.
I’ll skip over the C and C++ libraries because their support for I/O is mostly geared toward synchronous, or blocking, I/O, and this is typically not of interest unless you’re building a simple console program.
C/C++ 라이브러리에 대해서는 넘어가자. 대부분 동기, 또는 블록킹 I/O 인데다가 단순한 콘솔 프로그램을 작성한다면 문제되지 않는다.
Of course, there’s nothing wrong with that. Some of my favorite programs are console programs that do one thing and do it really well. Still, that’s not very interesting, so I’ll move on.
물론 무시하는 것은 아니다. 내가 작성한 많은 프로그램도 콘솔 프로그램이고 매우 유용하다. 넘어가자.
To begin with, I’ll start with the Windows API and the good old—and even aptly named—ReadFile function.
윈도우즈 API 중 ReadFile 함수로 시작해 보면
Before I can start reading the contents of a file, I need a handle to the file, and this handle is provided by the immensely powerful CreateFile function:
파일의 내용을 읽기 위해서는 파일에 대한 핸들이 필요하고 이는 CreateFile로 부터 얻어진다.
auto fn = L"C:\\data\\greeting.txt";
auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT(f);
To keep the examples brief, I’ll just use the ASSERT and VERIFY macros as placeholders to indicate where you’ll need to add some error handling to manage any failures reported by the various API functions.
코드를 단순하게 하기 위해서 에러 처리 루틴을 대신해서 ASSERT와 VERIFY 매트로를 필요한 위치에 사용했음을 알아주기 바란다.
In this code snippet, the CreateFile function is used to open rather than create the file.
이 코드에서 CreateFile 함수는 파일을 생성하기 보다는 열기 위해서 사용되었다.
The same function is used for both operations.
같은 함수로 두 가지 동작 모두를 할 수 있다.
The Create in the name is more about the fact that a kernel file object is created, and not so much about whether or not a file is created in the file system.
함수 이름의 Create는 파일 시스템에서의 파일의 생성이라기 보다는 커널 파일 객체의 생성이라는 측면이 강하다.
The parameters are pretty self-explanatory and not too relevant to this discussion, with the exception of the second-to-last, which lets you specify a set of flags and attributes indicating the type of I/O behavior you need from the kernel.
대부분의 파라미터는 설명이 필요치 않다. 몇 가지 파라미터는 당신이 커널에게 요구하는 I/O 동작의 타입을 특정하기 때문에 중요하다.
In this case I used the FILE_ATTRIBUTE_NORMAL constant, which just indicates that the file should be opened for normal synchronous I/O.
여기에서는 FILE_ATTRIBUTE_NORMAL 상수를 사용함으로써, 파일이 일반적인 동기 i/o 형태로 열리기를 지정한 것이다.
Remember to call the CloseHandle function to release the kernel’s lock on the file when you’re done with it.
파일을 다 사용했을 때 커널의 파일에 대한 락을 해제하기 위해 CloseHandle 함수를 호출해야 함을 기억해야 한다.
A handle wrapper class, such as the one I described in my July 2011 column, “C++ and the Windows API” (msdn.microsoft.com/magazine/hh288076), will do the trick.
내가 지난 번 컬럼에서 말한 핸들 래퍼 클래스를 사용하면 이 부분에 약간의 트릭을 사용할 수 있다. (역자주: 아마도 소멸자에서 CloseHandle을 호출하는 것을 말하나봄..)
I can now go ahead and call the ReadFile function to read the contents of the file into memory:
각설하고, ReadFile 함수를 호출해서 파일의 내용을 메모리로 읽어들인다.
char b[64];
DWORD c;
VERIFY(ReadFile(f, b, sizeof(b), &c, nullptr));
printf("> %.*s\n", c, b);
As you might expect, the first parameter specifies the handle to the file.
첫 번째 파라미터는 파일에 대한 핸들이다.
The next two describe the memory into which the file’s contents should be read.
다음 두 파라미터는 파일의 내용을 읽어들일 메모리에 대한 것이다.
ReadFile will also return the actual number of bytes copied should there be fewer bytes available than were requested.
ReadFile은 실제 읽어들인 데이터의 바이트 수를 리턴한다.
The final parameter is only used for asynchronous I/O, and I’ll get back to that in a moment.
마지막 파라미터는 비동기 I/O에 대한 것이며, 잠시 후 다시 다루도록 하겠다.
In this simplistic example, I then simply print out the characters that were actually read from the file.
단순히 문자들을 출력한다.
Naturally, you might need to call ReadFile multiple times if required.
필연적으로 여러분은 필요시 ReadFile을 여러번 호출해야 할 것이다.
This model of I/O is simple to grasp and certainly quite useful for many small programs, particularly console-based programs.
이러한 모델의 I/O는 매우 단순하고 손쉬워서 많은 작은 프로그램들에 있어서 유용하다. 특히 콘솔-기반의 프로그램들에 있어서는 더욱 그렇다.
But it doesn’t scale very well.
하지만 이런 모델은 확장성이 떨어진다.
If you need to read two separate files at the same time, perhaps to support multiple users, you’ll need two threads.
만약 두 개의 별개의 파일로부터 동시에 읽어야 한다면 (아마도 다중 사용자 지원을 위해), 여러분은 스레드의 도입을 느낄 것이다.
No problem—that’s what the CreateThread function is for. Here’s a simple example:
문제 없다 -- 그것이 CreateThread 함수가 존재하는 이유이니까, 다음 예를 보자:
auto t = CreateThread(nullptr, 0, [] (void *) -> DWORD
{
CreateFile/ReadFile/CloseHandle
return 0;
},
nullptr, 0, nullptr);
ASSERT(t);
WaitForSingleObject(t, INFINITE);
Here I’m using a stateless lambda instead of a callback function to represent the thread procedure.
위에서 스레드 프로시저를 나타내기 위해서 무명 람다식을 사용했다.
The Visual C++ 2012 compiler conforms to the C++11 language specification in that stateless lambdas must be implicitly convertible to function pointers.
VC++ 2012 컴파일러는 C++ 11 언어 규격을 따르는데, 거기서는 무명 람다는 함수 포인터로의 암묵적 변환을 허용한다.
This is convenient, and the Visual C++ compiler does one better by automatically producing the appropriate calling convention on the x86 architecture, which sports a variety of calling conventions.
이는 매우 편리하며, VC++ 컴파일러는 한 가지 더 자동적으로 적절한 호출 형식을 만들어준다.
The CreateThread function returns a handle representing a thread that I then wait on using the WaitForSingleObject function.
CreateThread 함수는 핸들을 나타내는 핸들을 리턴하며, 그것에 대해서 WaitForSingleObject 함수를 이용해서 대기한다.
The thread itself blocks while the file is read.
스레드는 파일 읽기가 완료될 때까지 블록된다.
In this way I can have multiple threads performing different I/O operations in tandem.
이렇게 해서 여러 I/O 오퍼레이션이 동시에 수행되는 다중의 스레드를 가지게 되었다.
I could then call WaitForMultipleObjects to wait until all of the threads have finished.
그리고 나서 WaitForMultipleObjects를 호출해서 모든 스레드가 종료되기를 대기한다.
Remember also to call CloseHandle to release the thread-related resources in the kernel.
커널의 스레드-관련된 리소스를 해제하기 위해서 CloseHandle을 호출하는 것을 잊어서는 안된다.
This technique, however, doesn’t scale beyond a handful of users or files or whatever the scalability vector is for your program.
그렇지만 이러한 방식은 확장성이 필요시 되는 많은 프로그램에서 그다지 도움이 되지 않는다.
To be clear, it’s not that multiple outstanding read operations don’t scale.
좀 더 명확히는 다중의 읽기 오퍼레이션은 확장 가능하기 쉽지 않다.
Quite the opposite. But it’s the threading and synchronization overhead that will kill the program’s scalability.
반대 입장에서는, 프로그램의 확장성을 저해하는 것은 동기화 오버헤드가 아니라는 것이다.
One solution to this problem is to use something called alertable I/O via asynchronous procedure calls (APCs).
이러한 문제에 대한 해결책의 하나는 APC (Asynchronous Procedure Call) 이라는 깨울 수 있는 비동기 I/O를 사용하는 것이다.
In this model your program relies on a queue of APCs that the kernel associates with each thread.
이 모델에서는 여러분의 프로그램은 커널이 각각의 스레드와 연관하는 APC들의 큐에 의존하게 된다.
APCs come in both kernel- and user-mode varieties.
APC는 커널-모드와 유저-모드 변수 쌍을 가진다.
That is, the procedure, or function, that’s queued might belong to a program in user mode, or even to some kernel-mode driver.
이것은 큐 되는 프로시저 또는 함수가 사용자 모드 프로그램에 속할 수도 있고, 또는 커널-모드 드라이버의 것일 수도 있음이다.
The latter is a simple way for the kernel to allow a driver to execute some code in the context of a thread’s user-mode address space so that it has access to its virtual memory.
커널이 드라이버로 하여금 스레드의 유저-모드 주소 공간의 문맥의 코드를 실행하여 그 가상 메모리에 접근하도록 하는 것은 어렵지 않다.
But this trick is also available to user-mode programmers.
하지만 이러한 트릭은 유저-모드 프로그래머에게도 쓸모 있다.
Because I/O is fundamentally asynchronous in hardware anyway (and thus in the kernel), it makes sense to begin reading the file’s contents and have the kernel queue an APC when it eventually completes.
왜냐하면 I/O는 근본적으로 하드웨어적으로는 비동기적이기 때문에, 파일의 내용을 읽기 시작하고 그것이 완료되었을 때에 커널이 APC를 큐하는 것이 자연스럽기 때문이다.
To begin with, the flags and attributes passed to the CreateFile function must be updated to allow the file to provide overlapped I/O so that operations on the file aren’t serialized by the kernel.
CreateFile 함수에 건네지는 플래그와 속성을 변경하여 파일이 중복된 I/O를 허가하도록 하여 파일에 대한 오퍼레이션이 커널에 의해 직렬화되지 않도록 한다.
The terms asynchronous and overlapped are used interchangeably in the Windows API, and they mean the same thing.
비동기와 중복은 Windows API에 있어서 동일한 의미로 사용되며 같은 것을 의미한다.
Anyway, the FILE_FLAG_OVERLAPPED constant must be used when creating the file handle:
어쨌든, FILE_FLAG_OVERLAPPED 상수를 사용해야 한다.
auto f = CreateFile(fn, GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr);
Again, the only difference in this code snippet is that I replaced the FILE_ATTRIBUTE_NORMAL constant with the FILE_FLAG_OVERLAPPED one, but the difference at run time is huge.
코드를 보면, FILE_ATTRIBUTE_NORMAL을 FILE_FLAG_OVERLAPPED 상수로 변경한 것 밖에는 없지만 파일 오퍼레이션에 있어서는 큰 변화를 만든다.
To actually provide an APC that the kernel can queue at I/O completion, I need to use the alternative ReadFileEx function.
커널이 I/O 완료를 큐할 수 있도록 APC를 제공하기 위해서 실질적으로, ReadFileEx 함수를 사용했다.
Although ReadFile can be used to initiate asynchronous I/O, only ReadFileEx lets you provide an APC to call when it completes.
ReadFile 역시 비동기 I/O를 시작할 수 있지만, ReadFileEx 만이 완료 시 호출 APC를 제공한다.
The thread can then go ahead and do other useful work, perhaps starting additional asynchronous operations, while the I/O completes in the background.
스레드는 I/O 완료가 백그라운드에서 진행중일 때에, 곧바로 다른 유용한 작업을 개시할 수 있으며, 아마도 추가적인 비동기 동작 같은 것이 될 것이다.
Again, thanks to C++11 and Visual C++, a lambda can be used to represent the APC.
C++ 11 과 Visual C++ 덕에 우리는 APC를 나타낼 때 람다 식을 사용할 수 있게 되었다.
The trick is that the APC will likely want to access the newly populated buffer, but this isn’t one of the parameters to the APC, and because only stateless lambdas are allowed, you can’t use the lambda to capture the buffer variable.
APC가 새롭게 만들어진 버퍼에 접근하기를 원할 것 같기 때문에, 또한 APC의 파라미터 중 하나가 될 수 없기에, 그리고 오직 무명 람다만 사용할 수 있기 때문에, 여러분은 버퍼 변수를 취하기 위해서 람다를 사용할 수 없다.
The solution is to hang the buffer off the OVERLAPPED structure, so to speak.
해결책은 버퍼를 OVERLAPPED 구조체에 거는 것이다.
Because a pointer to the OVERLAPPED structure is available to the APC, you can then simply cast the result to a structure of your choice. Figure 1 provides a simple example.
OVERLAPPED 구조체에 대한 포인터는 APC에서 가용하기 때문에, 여러분은 단지 구조체의 결과에 대해서 캐스팅해서 사용할 수 있다.
struct overlapped_buffer
{
OVERLAPPED o;
char b[64];
};
overlapped_buffer ob = {};
VERIFY(ReadFileEx(f, ob.b, sizeof(ob.b), &ob.o, [] (DWORD e, DWORD c,
OVERLAPPED * o)
{
ASSERT(ERROR_SUCCESS == e);
auto ob = reinterpret_cast<overlapped_buffer *>(o);
printf("> %.*s\n", c, ob->b);
}));
SleepEx(INFINITE, true);
In addition to the OVERLAPPED pointer, the APC is also provided an error code as its first parameter and the number of bytes copied as its second.
OVERLAPPED 포인터와 더불어서, APC는 또한 에러 코드 및 복사된 바이트 수를 함께 넘겨준다.
At some point, the I/O completes, but in order for the APC to run, the same thread must be placed in an alertable state.
어떤 관점에서, I/O 완료 시 , APC가 실행되기 위해서, 동일한 스레드가 깨울 수 있는 상태에 놓여 있어야 한다.
The simplest way to do that is with the SleepEx function, which wakes up the thread as soon as an APC is queued and executes any APCs before returning control.
이렇게 하기 위한 가장 간단한 방법은 SleepEx 함수를 사용하는 것이다. SleepEx는 APC가 삽입되자마자 제어를 리턴하기 전에 스레드를 깨운다.
Of course, the thread may not be suspended at all if there are already APCs in the queue.
물론, APC가 이미 큐에 있다면, 스레드는 보류되지도 않을 것이다.
You can also check the return value from SleepEx to find out what caused it to resume.
SleepEx의 리턴 값을 체크해서 재개된 이유를 찾아낼 수 있다.
You can even use a value of zero instead of INFINITE to flush the APC queue before proceeding without delay.
또는 INFINITE 대신에 0 값을 사용해서 프로세싱 전에 딜레이 없이 APC 뷰를 플러쉬하도록 할 수도 있다.
Using SleepEx is not, however, all that useful and can easily lead unscrupulous programmers to poll for APCs, and that’s never a good idea.
하지만 SleepEx를 사용하는 것은 막무가네식의 프로그래머로 하여금 APC를 폴링하도록 하게 할 가능성이 높으며, 그것은 그렇게 좋은 방식이 아니다.
Chances are that if you’re using asynchronous I/O from a single thread, this thread is also your program’s message loop.
비동기 I/O를 단일 스레드에서 사용할 때, 이 스레드는 또한 여러분 프로그램의 메시지 루프이기도 하다.
Either way, you can also use the MsgWaitForMultipleObjectsEx function to wait for more than just APCs and build a more compelling single-threaded runtime for your program.
MsgWaitForMultipleObjectsEx 함수를 이용하여 단지 APC만이 아닌 추가적인 것을 대기하고 여러분 프로그램의 단일-스레드 런타임을 좀 더 강압적으로 만들 수 있다.
The potential drawback with APCs is that they can introduce some challenging re-entrancy bugs, so do keep that in mind.
APC의 잠재적인 단점은, 복잡한 재-진입 관련 버그를 만들 수 있다는 것이다. 이것을 항상 염두해 두어야 한다.
As you find more things for your program to do, you might notice that the processor on which your program’s thread is running is getting busier while the remaining processors on the computer are sitting around waiting for something to do.
여러분 프로그램의 스레드가 실행되고 있는 프로세서는 한창 바쁜데, 남은 다른 프로세서들은 여전히 한가할 수 있다.
Although APCs are about the most efficient way to perform asynchronous I/O, they have the obvious drawback of only ever completing on the same thread that initiated the operation.
APC가 비동기 I/O를 수행하는 효과적인 방법임에도 불구하고, 단점도 있다. 그것은 오퍼레이션을 개시했던 동일한 스레드에서 완료해야 한다는 것이다.
The challenge then is to develop a solution that can scale this to all of the available processors.
그렇다면 해결 방법은 가용한 모든 프로세서로 확장 가능한 솔루션을 개발하는 것이다.
You might conceive of a design of your own doing, perhaps coordinating work among a number of threads with alertable message loops, but nothing you could implement would come close to the sheer performance and scalability of the I/O completion port, in large part because of its deep integration with different parts of the kernel.
여러분은 여러분 자신의 디자인 업무로 국한해 생각할 지 모르겠다. 아마도 깨울 수 있는 메시지 루프를 가지는 몇 개의 스레드에 일을 조정해서 나누는 것 쯤으로. 하지만 I/O 완료 포트의 확장성과 성능을 끌어올릴 수 있는 구현에는 여러분이 구현해야 할 것은 별로 없다? 왜냐하면 대부분은 커널의 많은 다른 부분들의 깁숙한 통합으로 이루어지기 때문이다.
While an APC allows asynchronous I/O operations to complete on a single thread, a completion port allows any thread to begin an I/O operation and have the results processed by an arbitrary thread.
비록 APC가 비동기 I/O 오퍼레이션이 단일 스레드에서 허용되기는 하지만 완료 포트는 어떤 스레드가 I/O 오퍼레이션을 개시했건 그리고 어떤 임의 스레드에 의해서 결과가 처리될 수 있도록 해준다.
A completion port is a kernel object that you create before associating it with any number of file objects, sockets, pipes and more.
완료 포트는 그것을 어떤 갯수의 파일 객체, 소켓, 파이프 등과 연관시키기 전에 여러분이 생성할 수 있는 커널 객체이다.
The completion port exposes a queuing interface whereby the kernel can push a completion packet onto the queue when I/O completes and your program can dequeue the packet on any available thread and process it as needed.
완료 포트는 큐잉 인터페이스를 노출한다. 여기에다가 커널은 I/O 완료시 완료 패킷을 삽입한다. 여러분 프로그램은 그 어떤 가용한 스레드에서도 패킷을 큐에서 꺼내서 필요하다면 그것을 처리할 수 있다.
You can even queue your own completion packets if needed.
더군다나 필요하다면 여러분은 여러분이 만든 완료 패킷을 삽입할 수도 있다.
The main difficulty is getting around the confusing API.
어려운 점은 혼돈스러운 API 뿐이다.
Figure 2 provides a simple wrapper class for the completion port, making it clear how the functions are used and how they relate.
다음은 완료 포트에 대한 래퍼 클래스를 보여준다.
class completion_port
{
HANDLE h;
completion_port(completion_port const &);
completion_port & operator=(completion_port const &);
public:
explicit completion_port(DWORD tc = 0) :
h(CreateIoCompletionPort(INVALID_HANDLE_VALUE, nullptr, 0, tc))
{
ASSERT(h);
}
~completion_port()
{
VERIFY(CloseHandle(h));
}
void add_file(HANDLE f, ULONG_PTR k = 0)
{
VERIFY(CreateIoCompletionPort(f, h, k, 0));
}
void queue(DWORD c, ULONG_PTR k, OVERLAPPED * o)
{
VERIFY(PostQueuedCompletionStatus(h, c, k, o));
}
void dequeue(DWORD & c, ULONG_PTR & k, OVERLAPPED *& o)
{
VERIFY(GetQueuedCompletionStatus(h, &c, &k, &o, INFINITE));
}
};
The main confusion is around the double duty that the CreateIoCompletionPort function performs, first actually creating a completion port object and then associating it with an overlapped file object.
CreateIoCompletionPort 함수는 두 가지 일을 한다. 첫 째로 실제 완료 포트 객체를 만드는 일이고, 둘 째는 이것을 overlapped 파일 객체와 연관하는 일이다.
The completion port is created once and then associated with any number of files.
완료 포트는 한 번 만들어지고 나서 여러 파일과 연관지어진다.
Technically, you can perform both steps in a single call, but this is only useful if you use the completion port with a single file, and where’s the fun in that?
기술적으로, 여러분은 한 번의 호출로 이 두 단계를 모두 한다. 하지만 이것은 완료 포트를 하나의 파일과 연관할 때만 유효하다. 하지만 그런 경우는 많지 않다.
When creating the completion port, the only consideration is the last parameter indicating the thread count.
완료 포트를 생성할 때 마지막 파라미터는 스레드 갯수를 나타낸다.
This is the maximum number of threads that will be allowed to dequeue completion packets concurrently.
이 숫자는 완료 패킷을 동시에 꺼낼 수 있는 스레드 갯수를 나타낸다.
Setting this to zero means that the kernel will allow one thread per processor.
이 값을 0으로 한다는 것은 프로세서 하나에 하나의 스레드를 허가한다는 뜻이다.
Adding a file is technically called an association;
파일을 더한다는 것은 기술적으로 연관을 호출함을 나타낸다.
the main thing to note is the parameter indicating the key to associate with the file.
유의할 것은 파일과 연관하는 키를 나타내는 파라미터이다.
Because you can’t hang extra information off the end of a handle like you can with an OVERLAPPED structure, the key provides a way for you to associate some program-specific information with the file.
여러분이 OVERLAPPED 구조체를 사용해서 했던 방식으로 정보를 매달 수 없기 때문에 키(key)는 여러분이 프로그램-특정 정보를 파일과 연관시킬 수 있는 방법을 제공한다.
Whenever the kernel queues a completion packet related to this file, this key will also be included. This is particularly important because the file handle isn’t even included in the completion packet.
커널이 이 파일과 연관된 완료 포트를 큐잉할 때마다, 이 키가 함께 포함된다. 이것은 파일 핸들이 완료 패킷에조차 포함되지 않기 때문에 매우 중요하다.
As I said, you can queue your own completion packets.
말했듯이, 여러분이 만든 완료 패킷도 큐잉할 수 있다.
In this case, the values you provide are entirely up to you.
이런 경우에는 여러분이 제공한 값은 완전히 여러분의 것이다.
The kernel doesn’t care and won’t try to interpret them in any way.
커널은 그 값에 대해서 전혀 고려하지 않는다.
Thus, you can provide a bogus OVERLAPPED pointer and the exact same address will be stored in the completion packet.
따라서 여러분은 위조된 OVERLAPPED 포인터를 제공할 수 있고, 완료 패킷 안에서도 완벽하게 동일한 주소를 꺼낼 수 있는 것이다.
In most cases, however, you’ll wait for the kernel to queue the completion packet once an asynchronous I/O operation completes.
그러나 대부분의 경우에, 여러분은 커널이 비동기 I/O 오퍼레이션을 완료했을 때, 완료 패킷을 큐하는 것을 대기할 것이다.
Typically a program creates one or more threads per processor and calls GetQueuedCompletionStatus, or my dequeue wrapper function, in an endless loop.
전형적으로 프로그램은 하나 이상의 스레드를 생성하고 무한 루프에서 GetQueuedCompletionStatus 또는 dequeue 래퍼 함수를 호출할 것이다.
You might queue a special control completion packet—one per thread—when your program needs to come to an end and you want these threads to terminate.
여러분은 아마도 - 스레드 마다 하나씩 - 여러분 프로그램이 종료해야 할 때, 특별한 제어 완료 패킷을 큐잉 할 것이다.
As with APCs, you can hang more information off the OVERLAPPED structure to associate extra information with each I/O operation:
APC에서처럼, 각각의 I/O 오퍼레이션과 연관시키기 위해 부가적인 정보를 OVERLAPPED 구조체에 매달 것이다.
completion_port p;
p.add_file(f);
overlapped_buffer ob = {};
ReadFile(f, ob.b, sizeof(ob.b), nullptr, &ob.o);
Here I’m again using the original ReadFile function, but in this case I’m providing a pointer to the OVERLAPPED structure as its last parameter. A waiting thread might dequeue the completion packet as follows:
여기에서 다시 한번 ReadFile 함수를 사용한다. 하지만 이번에는 마지막 파라미터로 OVERLAPPED 구조체의 포인터를 제공한다. 대기 스레드는 완료 패킷을 다음과 같이 꺼낼 것이다.
DWORD c;
ULONG_PTR k;
OVERLAPPED * o;
p.dequeue(c, k, o);
auto ob = reinterpret_cast<overlapped_buffer *>(o);
스레드의 풀
If you’ve been following my column for some time, you’ll recall that I spent five months last year covering the Windows thread pool in detail.
내 컬럼을 전에도 본 적이 있다면, 다섯 달 전에 윈도우 스레드 풀에 대해서 상세하게 다룬 컬럼을 기억할 지도 모른다.
It will also be no surprise to you that this same thread pool API is implemented using I/O completion ports, providing this same work-queuing model but without the need to manage the threads yourself.
I/O 완료 포트를 사용하는 그것과 동일한 스레드 풀 API가 구현되어 있음은 놀랄 일이 아니다. 이는 작업-큐잉 모델을 제공한다. 다만 스레드 관리를 여러분이 직접할 필요는 없는.
It also provides a host of features and conveniences that make it a compelling alternative to using the completion port object directly.
이는 또한 완료 포트 객체를 직접적으로 사용하는 것에 대한 대안을 강요하고 있는 특징과 편의를 제공한다.
If you haven’t already done so, I encourage you to read those columns to get up to speed on the Windows thread pool API.
여러분이 아직까지 몰랐다면, 나는 그 컬럼들을 읽어볼 것을 강추하며, 그것을 사용해서 윈도우 스레드 풀 API를 잘 활용할 것을 권장한다.
A list of my online columns is available at bit.ly/StHJtH.
At a minimum, you can use the TrySubmitThreadpoolCallback function to get the thread pool to create one of its work objects internally and have the callback immediately submitted for execution.
여러분은 TrySubmitThreadpoolCallback 함수를 사용해서 스레드 풀을 얻고 그 작업 객체를 내부적으로 생성하고 콜백을 즉시 실행되도록 할 수 있다.
It doesn’t get much simpler than this:
이러한 코드는 대략 다음과 같다.
TrySubmitThreadpoolCallback([](PTP_CALLBACK_INSTANCE, void *)
{
// Work goes here!
},
nullptr, nullptr);
If you need a bit more control, you can certainly create a work object directly and associate it with a thread pool environment and cleanup group.
만약 좀 더 많은 제어권을 가지고 싶다면, 작업 객체를 직접 생성하고 그것을 스레드 풀 환경과 연관시키고 그룹을 클린업할 수 있다.
This will also give you the best possible performance.
이러한 방식은 성능을 최대한 끌어낼 수 있다.
Of course, this discussion is about overlapped I/O, and the thread pool provides I/O objects just for that.
물론 이 글이 overlapped I/O에 대한 것이지만, 스레드 풀은 I/O 객체를 그것을 위해서 제공한다.
I won’t spend much time on this, as I’ve already covered it in detail in my December 2011 column, “Thread Pool Timers and I/O” (msdn.microsoft.com/magazine/hh580731), but Figure 3 provides a new example.
이 주제를 위해서 많은 시간을 할애하지 않겠다. 지난 컬럼을 읽어보기를 권장한다.
OVERLAPPED o = {};
char b[64];
auto io = CreateThreadpoolIo(f, [] (PTP_CALLBACK_INSTANCE,
void * b, void *, ULONG e, ULONG_PTR c, PTP_IO)
{
ASSERT(ERROR_SUCCESS == e);
printf("> %.*s\n", c, static_cast<char *>(b));
},
b, nullptr);
ASSERT(io);
StartThreadpoolIo(io);
auto r = ReadFile(f, b, sizeof(b), nullptr, &o);
if (!r && ERROR_IO_PENDING != GetLastError())
{
CancelThreadpoolIo(io);
}
WaitForThreadpoolIoCallbacks(io, false);
CloseThreadpoolIo(io);
Given that CreateThreadpoolIo lets me pass an additional context parameter to the queued callback, I don’t need to hang the buffer off the OVERLAPPED structure, although I could certainly do that if needed. The main things to keep in mind here are that StartThreadpoolIo must be called prior to beginning the asynchronous I/O operation, and CancelThreadpoolIo must be called should the I/O operation fail or complete inline, so to speak.
StartThreadpoolIo가 비동기 I/O 오퍼레이션 전에 호출되어야 하고, CancelThreadpoolIo가 I/O 오퍼레이션이 실패하거나 완료하기 전에 호출되어야 한다는 것이다.
Taking the concept of a thread pool to new heights, the new Windows API for Windows Store apps also provides a thread pool abstraction, although a much simpler one with far fewer features. Fortunately, nothing prevents you from using an alternative thread pool appropriate for your compiler and libraries. Whether you’ll get it past the friendly Windows Store curators is another story. Still, the thread pool for Windows Store apps is worth mentioning, and it integrates the asynchronous pattern embodied by the Windows API for Windows Store apps.
윈도우 스토어 앱을 위해 새로운 형태의 스레드 풀 API가 제공된다. 좀 더 단순하고 사용하기 편하지만 유연성은 떨어지는. 하지만 충분히 고려하고 살펴볼 만 하다.
Using the slick C++/CX extensions provides a relatively simple API for running some code asynchronously:
ThreadPool::RunAsync(ref new WorkItemHandler([] (IAsyncAction ^)
{
// Work goes here!
}));
Syntactically this is pretty straightforward.
문법적으로 매우 직관적이다.
We can even hope that this will become simpler in a future version of Visual C++ if the compiler can automatically generate a C++/CX delegate from a lambda—at least conceptually—the same as it does today for function pointers.
하지만 컴파일러가 더욱 발전하여 C++/Cx 델리게이트를 람다-최소한 개념적으로라도- 오늘날 함수 포인터와 같이 생성할 수 있는 기능이 된다면...
Still, this relatively simple syntax belies a great deal of complexity.
여전히 상대적으로 단순한 이 문법 뒤에는 상당히 복잡한 그 무엇이 숨어 있다.
At a high level, ThreadPool is a static class, to borrow a term from the C# language, and thus can’t be created.
상위 레벨에서, ThreadPool은 정적 클래스이며, C# 언어에서 차용한다면, 따라서 생성될 수 없다.
It provides a few overloads of the static RunAsync method, and that’s it.
정적 RunAsync 메소드에 대해서는 중복을 거의 제공하지 않는다.
Each takes at least a delegate as its first parameter.
각 태스크는 최소한 하나의 델리게이트를 첫 째 인자로 받는다.
Here I’m constructing the delegate with a lambda.
여기에서 나는 델리게이트를 람다로 구성했다.
The RunAsync methods also return an IAsyncAction interface, providing access to the asynchronous operation.
RunAsync 메소드는 IAsyncAction 인터페이스를 리턴한다. 이는 비동기 오퍼레이션에 대한 접근을 제공한다.
As a convenience, this works pretty well and integrates nicely into the asynchronous programming model that pervades the Windows API for Windows Store apps.
매우 편리하게도, 이와 같은 방식은 윈도우 스토어 앱을 위한 윈도우 API 전반에 불어닥치는 비동기 프로그래밍 모델에 매우 잘 들어맞고 있는 중이다.
You can, for example, wrap the IAsyncAction interface returned by the RunAsync method in a Parallel Patterns Library (PPL) task and achieve a level of composability similar to what I described in my September and October columns, “The Pursuit of Efficient and Composable Asynchronous Systems” (msdn.microsoft.com/magazine/jj618294) and “Back to the Future with Resumable Functions” (msdn.microsoft.com/magazine/jj658968).
이를테면, 여러분은 PPL(Parallel Patterns Library) 태스크의 RunAsync 메소드가 돌려준 IAsyncAction 인터페이스를 래핑해서 일정 수준의 composability를 갖출 수 있다. 이는 ...
However, it’s useful and somewhat sobering to realize what this seemingly innocuous code really represents.
그렇지만 이토록 언뜻 보기에 별 것 아닌 것 처럼 보이는 코드가 만들어낸 그 무엇은 놀랍다.
At the heart of the C++/CX extensions is a runtime based on COM and its IUnknown interface.
C++/CX 확장의 중심에는 COM과 그 IUnknown 인터페이스가 있다.
Such an interface-based object model can’t possibly provide static methods.
그런 인터페이스-기반의 객체 모델은 정적 메소드를 제공할 수가 없다.
There has to be an object for there to be an interface, and some sort of class factory to create that object, and indeed there is.
인터페이스가 존재하기 위한 객체가 존재해야 하고, 몇 가지의 클래스 팩토리가 그 객체를 생성해야 하고, 게다가 거기에 있어야 한다.
The Windows Runtime defines something called a runtime class that’s very much akin to a traditional COM class.
윈도우즈 런타임은 런타임 클래스라고 부르는 무언가를 정의했다. 이는 전통적인 COM 클래스와 비슷하다.
If you’re old-school, you could even define the class in an IDL file and run it through a new version of the MIDL compiler specifically suited to the task, and it will generate .winmd metadata files and the appropriate headers.
여러분은 IDL 파일에 클래스를 정의하고 이를 새로 나온 MIDL 컴파일러로 컴파일해서, 그러면 .winmd 메타데이터 파일이 나오고, 적절한 헤더 파일들을 만들어낼 수 있다.
A runtime class can have both instance methods and static methods.
런타임 클래스는 인스턴스 메소드와 정적 메소드 모두를 가질 수 있다.
They’re defined with separate interfaces.
이 둘은 별도의 인터페이스에 나뉘어서 정의된다.
The interface containing the instance methods becomes the class’s default interface, and the interface containing the static methods is attributed to the runtime class in the generated metadata.
인스턴스 메소드를 포함하는 인터페이스는 클래스의 디폴트 인터페이스가 되며, 정적 메소드를 포함하는 인터페이스는 생성된 메타데이터에서 런타임 클래스에 대한 속성이 된다.
In this case the ThreadPool runtime class lacks the activatable attribute and has no default interface, but once created, the static interface can be queried for, and then those not-so-static methods may be called.
이 경우에 ThreadPool 런타임 클래스는 활성화시킬 수 있는 속성이 부족하고 디폴트 인터페이스가 없다, 하지만 생성되고 나면, 정적 인터페이스가 쿼리될 수 있으며, 이러한 정적이지 않은 메소드들이 호출될 수 있는 것이다.
Figure 4 gives an example of what this might entail. Keep in mind that most of this would be compiler-generated, but it should give you a good idea of what it really costs to make that simple static method call to run a delegate asynchronously.
아래 코드는 이러한 설명을 나타낸다. 이 코드는 컴파일러가 생성한 것임을 명심하라. 하지만 이것은 단순한 정적 메소드가 델리게이트를 비동기적으로 실행하도록 만드는 것에 대해서 잘 설명하고 있다.
class WorkItemHandler :
public RuntimeClass<RuntimeClassFlags<ClassicCom>,
IWorkItemHandler>
{
virtual HRESULT __stdcall Invoke(IAsyncAction *)
{
// Work goes here!
return S_OK;
}
};
auto handler = Make<WorkItemHandler>();
HSTRING_HEADER header;
HSTRING clsid;
auto hr = WindowsCreateStringReference(
RuntimeClass_Windows_System_Threading_ThreadPool,
_countof(RuntimeClass_Windows_System_Threading_ThreadPool)
- 1, &header, &clsid);
ASSERT(S_OK == hr);
ComPtr<IThreadPoolStatics> tp;
hr = RoGetActivationFactory(
clsid, __uuidof(IThreadPoolStatics),
reinterpret_cast<void **>(tp.GetAddressOf()));
ASSERT(S_OK == hr);
ComPtr<IAsyncAction> a;
hr = tp->RunAsync(handler.Get(), a.GetAddressOf());
ASSERT(S_OK == hr);
This is certainly a far cry from the relative simplicity and efficiency of calling the TrySubmitThreadpoolCallback function. It’s helpful to understand the cost of the abstractions you use, even if you end up deciding that the cost is justified given some measure of productivity. Let me break it down briefly.
이는 분명히 TrySubmitThreadpoolCallback 함수를 호출하는 것의 비교적 단순하고 효율적인 그런 것과는 거리가 멀다. 다만 여러분이 사용하고 있는 추상화의 뒷면을 이해하는 것으로 족하다.
The WorkItemHandler delegate is actually an IUnknown-based IWorkItemHandler interface with a single Invoke method.
WorkItemHandler 델리게이트는 실제로 하나의 Invoke 메소드를 가지는 IUnknown-기반의 IWorkItemHandler 인터페이스이다.
The implementation of this interface isn’t provided by the API but rather by the compiler.
이 인터페이스의 구현은 API가 아닌 컴파일러에 의해서 제공되는 것이다.
This makes sense because it provides a convenient container for any variables captured by the lambda and the lambda’s body would naturally reside within the compiler-generated Invoke method.
왜냐하면 람다에 의해서 받아들여지는 변수를 위한 편리한 콘테이너를 제공한다. 그리고 태생적으로 람다의 몸통은 컴파일러-생성된 Invoke 메소드의 안쪽에 거주하기 때문이다.
In this example I’m simply relying on the Windows Runtime Library (WRL) RuntimeClass template class to implement IUnknown for me.
이 예제에서 IUnknown 을 구현하기 위해서 윈도우즈 런타임 라이브러리 (WRL) RuntimeClass 템플릿 클래스에 의존한다.
I can then use the handy Make template function to create an instance of my WorkItemHandler.
그리고 편리한 Make 템플릿 함수를 이용해서 WorkItemHandler 인스턴스를 생성하고 있다.
For stateless lambdas and functions pointers, I’d further expect the compiler to produce a static implementation with a no-op implementation of IUnknown to avoid the overhead of dynamic allocation.
stateless 람다 함수의 포인터에 대해서는, 앞으로 컴파일러가 동적 할당의 오버헤드를 피하기 위해서 IUnknown의 no-op 구현을 가지는 정적 구현을 생성할 것으로 기대한다.
To create an instance of the runtime class, I need to call the RoGetActivationFactory function.
런타임 클래스의 인스턴스를 생성하기 위해서, RoGetActivationFactory 함수를 호출해야 한다.
However, it needs a class ID.
그렇지만 클래스 ID가 필요하다.
Notice that this isn’t the CLSID of traditional COM but rather the fully qualified name of the type, in this case, Windows.System.Threading.ThreadPool.
이는 전통적인 COM에서의 CLSID가 아니다. 타입에 대한 이름이다. 여기에서는 Windows.System.Threading.ThreadPool 이다.
Here I’m using a constant array generated by the MIDL compiler to avoid having to count the string at run time.
여기에서는 MIDL 컴파일러가 생성한 상수 배열을 사용해서 런타임에 문자열을 다루는 것을 피했다.
As if that weren’t enough, I need to also create an HSTRING version of this class ID.
이 클래스 ID에 대한 HSTRING 버전 역시 생성해야 한다.
Here I’m using the WindowsCreateStringReference function, which, unlike the regular WindowsCreateString function, doesn’t create a copy of the source string.
여기에서 나는 WindowsCreateStringReference 함수를 사용했다. 이는 일반적인 WindowsCreateString 함수와 다르게, 소스 문자열의 복사본을 생성하지 않는다.
As a convenience, WRL also provides the HStringReference class that wraps up this functionality.
WRL은 이 기능을 편리하게 사용하도록 HStringReference 클래스를 제공한다.
I can now call the RoGetActivationFactory function, requesting the IThreadPoolStatics interface directly and storing the resulting pointer in a WRL-provided smart pointer.
이제 RoGetActivationFactory 함수를 호출하여, IThreadPoolStatics 인터페이스를 요구하여 포인터를 WRL이 제공하는 스마트 포인터에 저장할 수 있다.
I can now finally call the RunAsync method on this interface, providing it with my IWorkItemHandler implementation as well as the address of an IAsyncAction smart pointer representing the resulting action object.
이제 마지막으로 IWorkItemHandler 구현을 액션 객체를 나타내는 IAsyncAction 스마트 포인터의 주소와 함께 제공하면서 RunAsync 메소드를 이 인터페이스에 대하여 호출한다.
It’s then perhaps not surprising that this thread pool API doesn’t provide anywhere near the amount of functionality and flexibility provided by the core Windows thread pool API or the Concurrency Runtime.
이 스레드 풀 API는 코어 윈도우즈 스레드 풀 API 또는 concurrency 런타임이 제공하는 막대한 기능과 유연성을 제공하지는 않는다.
The benefit of C++/CX and the runtime classes is, however, realized along the boundaries between the program and the runtime itself.
C++/CX와 런타임 클래스들은 그러나, 프로그램과 런타임 자체의 경계에서 구현되었다.
As a C++ programmer, you can be thankful that Windows 8 isn’t an entirely new platform and the traditional Windows API is still at your disposal, if and when you need it.
C++ 프로그래머로서 윈도우즈 8이 완전히 새로운 플랫폼이 아니며 전통적인 윈도우즈 API가 원하면 유효하다는 것은 매우 고마운 일이다.
Kenny Kerr is a software craftsman with a passion for native Windows development. Reach him atkennykerr.ca.
Thanks to the following technical expert for reviewing this article: James P. McNellis
|
첫댓글 글을 번역해서 쓰면서도 대부분 이해가 되지 않는다. COM에 대해 이해할 날은 언제나 올 것인가 ㅋㅋㅋ