본문 바로가기

High Level Technique/Window System

비동기 I/O (Asynchronous I/O)

비동기 I/O (Asynchronous I/O)


만들어 놓은 프로그램이 CPU 사용량을 확인했을데 들쭉날쭉하다면 뭔가 잘못만들어 졌을 가능성이 매우 큽니다.

사용량이 큰 폭으로 사용된다면 성능저하가 일어나기 때문입니다.



가령 모든 데이터를 받아낸 뒤에 실행이 된다면 데이터를 받아오는 동안에는 어떠한 작업도 하지 못하게 됩니다. 예를 들어 동영상을 볼 때 동영상 데이터를 모두 받아온 뒤 영상이 나온다면 그 동안은 영상을 볼 수가 없게 되는 것이죠. 이러한 방식은 동기 I/O (Synchronous I/O)라 합니다.


ANSI 표준 함수를 이용해 만들어진 프로그램들은 Synchronous I/O 방식으로 만들어 집니다. 한번 호출되면 완료될 때까지 유지되는 현상을 Blocking이라 하는데 이러한 함수들을 가리켜 Blocking 함수라고 하며 이 함수들을 이용한 입출력 연산을 Synchronous I/O라고 하는 것입니다.



우리가 동영상을 보고자 할 때 대부분 실행과 동시에 영상이 나오게 됩니다. 물론 엄청난 사양으로 인해 동기I/O 방식으로 처리되었을 수도 있습니다.


하지만 그만한 사양을 가지고 있는 사람들은 극히 드물죠. 따라서 데이터 수신과, 영상 출력이 동시에 이루어지도록 합니다. 이러한 방식을 비동기I/O라고 합니다.




중첩 I/O (Overlapped I/O)


비동기 I/O 방식 중에 대표적인 것이 중첩 I/O 입니다. 가령 fread 함수를 이용하여 텍스트를 출력해주는 프로그램이 있다고 하면 fread가 끝난 뒤에 출력이 이루어질 것입니다.


앞서 ANSI 표준 함수로 만들어진 프로그램은 Blocking 함수 Synchronous I/O 라고 했습니다. 그렇다면 이러한 방식이 아니라 Non-Blocking 방식의 함수를 사용해야 하는데 어떤 함수가 있을까요?


ReadFile 함수가 Non-Blocking 함수입니다. 이 함수는 실행이 되자마자 바로 반환합니다. 따라서 지속적으로 데이터가 들어와도 바로 반환되기 때문에 I/O 중첩이 일어나도 실행이 됩니다.


이러한 이유로 중첩 I/O라고 부릅니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
#define buf_size 1024
 
int CommToClient(HANDLE);
 
int _tmain(int argc, TCHAR* argv[])
{
    LPTSTR pipeName = _T("\\\\.\\pipe\\simple_pipe");
    HANDLE hPipe;
 
    while (1)
    {
        hPipe = CreateNamedPipe(pipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, PIPE_UNLIMITED_INSTANCES, buf_size / 2, buf_size / 220000, NULL);
        if (hPipe == NULL)
        {
            printf("CreateNamedPipe failed \n");
            return -1;
        }
 
        BOOL isSuccess;
        isSuccess = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);
 
        if (isSuccess)
        {
            CommToClient(hPipe);
        }
        else
        {
            CloseHandle(hPipe);
        }
 
        return 1;
    }
 
    return 0;
}
 
int CommToClient(HANDLE hPipe)
{
    TCHAR fileName[MAX_PATH];
    TCHAR dataBuf[buf_size];
    BOOL isSuccess;
    DWORD fileNameSize;
 
    isSuccess = ReadFile(hPipe, fileName, MAX_PATH*sizeof(TCHAR), &fileNameSize, NULL);
 
    if (!isSuccess || fileNameSize == 0)
    {
        printf("Pipe read message error! \n");
        return -1;
    }
 
    FILE* fileptr = _tfopen(fileName, _T("r"));
    if (fileptr == NULL)
    {
        printf("File Open Error \n");
        return -1;
    }
 
 
    OVERLAPPED overlappedInst;
 
    memset(&overlappedInst, 0sizeof(overlappedInst));
    overlappedInst.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL);
 
    DWORD bytesWritten = 0, bytesRead = 0, bytesWrite = 0, bytesTransfer = 0;
 
    while (!feof(fileptr))
    {
        bytesRead = fread(dataBuf, 1, buf_size, fileptr);
        bytesWrite = bytesRead;
        isSuccess = WriteFile(hPipe, dataBuf, bytesWrite, &bytesWritten, &overlappedInst);
 
        if (!isSuccess && GetLastError() != ERROR_IO_PENDING)
        {
            printf("Pipe write message error! \n");
            break;
        }
 
        WaitForSingleObject(overlappedInst.hEvent, INFINITE);
        GetOverlappedResult(hPipe, &overlappedInst, &bytesTransfer, FALSE);
        printf("Treansferred data size: %u \n", bytesTransfer);
    }
 
    FlushFileBuffers(hPipe);
    DisconnectNamedPipe(hPipe);
    CloseHandle(hPipe);
 
    return 1;
}
cs



16라인 CreateNamedPipe에서 FILE_FALG_OVERLAPPED 인자를 넣고 있는데 이렇게 생성된 파이프는 Non-Blocking 모드로 동작한다.


67라인에서는 hEvent의 값을 CreateEvent()로 넣어주고 있다. 초기 상태가 Signaled 상태로 되어있는데  입출력 완료를 확인하기 위한 용도로 반복적으로 사용되므로 71라인 while문에서 Non-Signaled 상태로 변경시켜야 합니다. 하지만 코드상 Non-Signaled 상태로 바꾸는 코드는 없는데, ReadFile 함수의 특성에 따라 자동으로 Non-Signaled 상태가 됩니다.


중첩 I/O에서는 WriteFile, ReadFile 함수가 NULL을 반환했다고 오류로 취급을 하면 안된다. 하나의 I/O연산이 끝나기 전에 새로운 I/O 작업이 들어올 수 있기 때문입니다.


따라서 NULL을 반환했다고 한다면 GetLastError()을 이용해서 I/O 요청에 의한 것인지 아닌지를 확인해야 합니다.




완료루틴 I/O (Completion Routin I/O)


중첩 I/O를 보면 입력 및 출력이 완료되었음을 확인하는 번거로운 작업을 항상 고려해야 했습니다. 


가령 A, B, C I/O 작업이 완료되었을 때 D, E, F 루틴이 실행된다고 했을 때를 완료루틴이라고 합니다.


A I/O 가 끝나고 D라는 루틴이 실행되어야 한다면 구현을 해야 하는데, 중첩으로 이루어진 A, B, C 중 어느것이 완료가 되었는지 구분해야합니다.


위와 같이 루틴을 정해야 할 때 WritefileEx, ReadFileEx 함수를 사용합니다. 이 두 함수는 비동기 중에서 확장 I/O를 위한 함수입니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
 
TCHAR strdata[] = _T("Nobody was father off base than the pundits who said \n Royal Liverppol was outdated and not worthy of hosting ~ \n for the first time since 1967. The Hoylake track ~ \n Here's the solution to modern golf technology -- firm, \n fast fairways, penal bunkers, firm greens and, with any ~ \n");
 
VOID WINAPI FileIoCompletionRoutine(DWORD, DWORD, LPOVERLAPPED);
 
int _tmain(int argc, TCHAR* argv[])
{
    TCHAR fileName[] = _T("data.txt");
    HANDLE hFile = CreateFile(fileName, GENERIC_WRITE, FILE_SHARE_WRITE, 0, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, 0);
 
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("File creation fault! \n");
        return -1;
    }
 
    OVERLAPPED overlappedInst;
    memset(&overlappedInst, 0sizeof(overlappedInst));
    overlappedInst.hEvent = (HANDLE)1234;
    WriteFileEx(hFile, strdata, sizeof(strdata), &overlappedInst, FileIoCompletionRoutine);
 
    SleepEx(INFINITE, TRUE);
 
    CloseHandle(hFile);
    return 0;
}
 
VOID WINAPI FileIoCompletionRoutine(DWORD errorCode, DWORD numOfBytesTransfered, LPOVERLAPPED overlapped)
{
    printf("File write result \n");
    printf("Error code : %u \n", errorCode);
    printf("Transfered bytes len: %u \n", numOfBytesTransfered);
    printf("The other info: %u \n", (DWORD)overlapped->hEvent);
}
cs


위 예제를 실행시키면 잘 실행이된다. 25라인의 SleepEx를 주석처리하면 출력이 정상적으로 안되는 것을 볼 수 있다.


하지만 파일은 정상적으로 만들어진 것을 볼 수 있다. 두 차이는 메인 쓰레드가 알림 가능한 상태가 아니기 때문이다.





알림 가능한 상태 (Alertable State)


I/O 연산이 완료되어 완료루틴을 실행할 차례가 되었다면 바로 완료루틴이 실행되어야 하는데 Windows는 완료 루틴 실행 타이밍을 프로그래머가 결정할 수 있도록 한다.


SleepEx()가 완료루틴을 실행하라는 신호를 주는 용도로 사용된다.


A,B,C I/O 연산들이 실행이되고 D라는 연산이 추가적으로 실행될려는 찰라 A,B,C가 모두 완료되어서 완료루틴이 실행된다면 D는 ABC가 모두 끝날 때까지 기다려야만 한다.


완료루틴이 실행되는 타이밍을 프로그래머가 정할 수 있기 때문에 알림 가능한 상태에 바지지 않게만 하면 얼마든지 중첩시킬 수 있다.


WaitForSingleObjectEx, WaitForMultipleObjectsEx 가 쓰레드를 알림 가능한 상태로 만드는 기능을 가지고 있다.



'High Level Technique > Window System' 카테고리의 다른 글

메모리 관리  (0) 2017.01.10
SEH (Structured Exception Handling)  (0) 2017.01.09
캐쉬와 가상메모리  (0) 2017.01.05
쓰레드 풀 (Thread Pool)  (0) 2017.01.04
생산자/소비자 모델  (1) 2017.01.04