내용이 상당이 많으므로 관심 있는 분만 클릭하세요~
한용희 (마이크로소프트웨어 필자) |
2004/08/20 |
비동기 프로그래밍이라는 것은 한 가지 일을 할 때 그 일이 끝날 때까지 기다리는 것이 아니라 그 일은 그 일 나름대로 진행하면서 동시에 자신의 일을 계속 할 수 있는 것을 말한다. 이러한 기능이 가능하게 하려면 결국 쓰레드를 써야만 한다. 이 쓰레드를 이용해 비동기 프로그래밍 기법을 흉내내 보자. 쓰레드를 이용한 비동기 프로그래밍 <리스트 1>은 쓰레드를 이용해 다른 작업을 동시에 하는 것을 보여준 예이다. 0부터 9까지 출력하는 프로그램으로 별개의 쓰레드를 하나 더 돌려서 이들을 동시에 처리하고 있다. |
| |||
|
MainWork : 0
SubWork : 0
SubWork : 1
MainWork : 1
SubWork : 2
MainWork : 2
MainWork : 3
SubWork : 3
MainWork : 4
SubWork : 4
MainWork : 5
SubWork : 5
MainWork : 6
SubWork : 6
SubWork : 7
MainWork : 7
SubWork : 8
MainWork : 8
MainWork : 9
메인 작업 완료
SubWork : 9
부 작업 완료
즉 쓰레드를 이용하면 이처럼 두 가지 작업을 동시에 처리할 수 있다. 그런데 쓰레드의 생성자를 보면 ThreadStart라는 델리게이트를 취하고 있음을 볼 수 있을 것이다. 이 ThreadStart 델리게이트의 형식을 보면 다음과 같다.
public delegate void ThreadStart();
리턴형은 void이고, 인자로는 아무런 값을 받지 않는 델리게이트이다. 따라서 우리가 쓰레드를 사용해 다른 함수를 가동할 때는 인자도 없고 리턴 값도 없는 함수만을 사용해야 된다는 얘기다. 그런데 세상사라는 것이 그리 간단하지 않은 것이, 앞의 경우만 보더라도 0부터가 아닌 임의의 숫자로 시작하고 싶어서 그 숫자를 인자로 넘기고 싶을 때가 있을 것이다. 이럴 때에는 어떻게 해야 할 것인가? <리스트 2>의 예제를 보자.
| |||
|
MainWork : 0
SubWork : 5
MainWork : 1
SubWork : 6
MainWork : 2
SubWork : 7
MainWork : 3
SubWork : 8
MainWork : 4
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료
인자가 필요할 때에는 이렇게 해서 문제를 해결했는데, 이제 남은 또 다른 가능성은 리턴 값이 필요할 때이다. 되돌려 받는 값이 없다면 caller 측에서는 호출하고 잊어버리면 그만이다. 이를 ‘fire-and-forget style programming’이라고 한다. 그런데 되돌려 받는 값이 있다면 문제가 되기 시작한다. 상대방에게 비동기적으로 수행하라고 일을 시켜 놓았으니, 언제 끝날지 모르기 때문이다.
이를 확인하는 방법은 크게 두 가지가 있는데, caller 측에서 비동기 호출이 끝났는지 확인해서 끝났다면 리턴 값을 받아오는 방법과 caller 측에서 델리게이트를 넘겨주고 호출된 쪽에서 연산이 다 끝나면 그 델리게이트를 호출해줘서 리턴 값을 받아오는 방법이 있다. 이를 그림으로 나타내면 <그림 1>과 같다.
<그림 1> 비동기 호출로부터 결과값을 받는 방법 |
이번에는 비동기 부분에서 넘겨온 리턴 값을 가지고, 메인 부분에서 그 부분을 시작 값으로 하여 출력하는 프로그램을 만들 것이다. 지금까지 쓰레드 부분을 메인 부분에서 만들어 줬는데, 이번에는 메인 부분의 코드를 간결하게 하기 위하여 새로운 랩핑 클래스를 만들고 그곳에서 쓰레드를 담당하게 할 것이다. 쓰레드를 시작하는 함수는 Begin×××라는 이름을 붙여주고 결과 값을 받아 오는 함수는 End×××라고 붙여주자. <리스트 3>을 보자.
| |||
|
리턴 값이 준비돼 있는지 안 돼 있는지 확인하기 위해 IsCim pleted하는 메쏘드를 준비했다. 또한 콜백(callback) 형식으로 델리게이트를 넘길 때를 위해 그에 대한 델리게이트도 마련해뒀다. CAsync 클래스의 BeginWork 함수에서 인자와 콜백 함수를 넘겨주게 되는데 이때 만약 콜백 함수가 필요없다면 null을 넘겨주면 된다. 그러면 대신 IsCompleted하는 함수로 비동기 연산의 종료 여부를 확인할 수 있다.
한편 EndWork 함수에서는 비동기 연산이 종료되지도 않았는데 이 함수를 호출하면 준비되지 않는 결과 값을 가져가는 오류를 미연에 방지하기 위해 결과 값이 나올 때까지 블럭되게 한 후, 결과 값이 나오면 리턴하도록 했다. 그럼 먼저 caller 측에서 비동기 연산의 종료를 확인하는 예제를 보자(<리스트 4>).
| |||
|
이번 예제에서는 콜백 함수가 필요없으므로 BeginWork 함수에서 null을 넘겨줬다. 그리고 대신 IsCompleted 함수를 이용해 종료 여부를 확인하는 동안 메인에서는 자신의 일을 할 수 있도록 했다. 종료된 후에는 EndWork 함수를 이용하여 결과 값을 가져와서 메인 부분의 일을 처리했다. 이 프로그램의 결과는 다음과 같다.
................................................................
................................................................
................................................SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
.................................................................
.................................................................
..................................SubWork : 8
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료
이번에는 콜백 함수를 이용해 결과 값을 받아 오는 예제를 살펴보자(<리스트 5>). 콜백 함수에서는 비동기 함수의 리턴 값을 인자로 받아 와서 그 일을 하고 수행하고 있다.
| |||
|
................................................................................
................................................................................
....................................................SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
................................................................................
................................................................................
............................SubWork : 8
SubWork : 9
부 작업 완료
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
메인 작업 완료
이상이 비동기 프로그래밍의 대략적인 내부 구현이다. 실제 닷넷 프레임워크 내부적으로는 이보다 훨씬 더 복잡하게 진행이 되지만 대략적인 것은 이와 비슷하다. 그럼 이제는 실제 델리게이트를 이용해 비동기 프로그래밍을 해 보자.
델리게이트를 이용한 비동기 프로그래밍의 실제
이번 예제는 앞서 우리가 만든 프로그램과 비슷하다. 시작 숫자를 인자로 넘겨주면 그 숫자부터 프린트하는 프로그램이다. 이를 비동기 호출로 하기 위해 그에 대한 델리게이트를 다음과 같이 선언했다.
public delegate void SubWork(int i);
반환형을 다루는 예제는 조금 뒤에 다룰 것이므로, 지금은 반환형이 없는 델리게이트를 이용하자. 인자로는 시작 숫자를 넘겨줬다. 우리가 이렇게 델리게이트를 만들면 지난 시간에 소개했듯이 컴파일러는 이를 바탕으로 하여 다음과 같은 클래스를 만들게 된다.
public class SubWork : System.MulticastDelegate
{
// 생성자
public SubWork (object target, int32 methodPtr);
public void virtual Invoke( int i );
public virtual IAsyncResult BeginInvoke( int i,
AsyncCallback callback, Object object);
public virtual void EndInvoke( IAsyncResult result);
}
BeginInvoke는 앞에서 BeginWork와 비슷한 역할을 한다. 먼저 델리게이트가 받을 인자가 오고, 그 다음에 콜백 함수가 오고, 추가로 상태를 지정할 수 있는 인자를 쓸 수 있다. 이는 추가 정보를 넘겨줄 필요가 있을 때에만 쓰는 것이므로, 필요없다면 안 써도 된다. 그럼 이를 실제로 테스트해 보자(<리스트 6>).
| |||
|
MainWork : 0
SubWork : 3
MainWork : 1
SubWork : 4
SubWork : 5
MainWork : 2
MainWork : 3
SubWork : 6
MainWork : 4
SubWork : 7
MainWork : 5
SubWork : 8
MainWork : 6
SubWork : 9
MainWork : 7
MainWork : 8
MainWork : 9
이와 같이 fire-and-forget형 프로그래밍의 경우 간단하지만 만약 반환 값을 다뤄야 할 경우는 약간 복잡해진다. 닷넷 플랫폼에서는 반환 값을 다루기 위한 4가지 스타일의 프로그래밍 기법을 제공한다.
1. Use Callbacks : 콜백 델리게이트를 이용해 비동기 부분에서 연산이 다 끝나면 델리게이트를 호출해 주는 방식
2. Poll Completed : caller 부분에서 연산이 다 끝났는지 IsCompleted라는 속성을 이용하여 계속 확인해 보는 방식
3. Begin Invoke, End Invoke : caller 측에서 결과 값을 받기 위하여 블러킹돼 기다리는 방식
4. Begin Invoke, Wait Handle, End Invoke : 앞의 방식과 비슷하나 wait handle에서 제한 시간을 설정해 줌으로써 계속 블러킹되는 것을 방지할 수 있다.
그럼 이들에 대한 예제를 하나씩 살펴보도록 하자. 이 예제는 SubWork에서 반환 값을 주는데, 역시 앞의 예제와 비슷하게 main 부분에서는 시작 값으로 사용될 값을 넘겨주게 된다. 먼저 <리스트 7>을 통해 1번의 경우부터 보도록 하자.
| |||
|
public delegate void AsyncCallback(IAsyncResult ar)
즉 리턴 값은 없고, 인자로 IAsyncResult라는 것을 받고 있다. 이는 닷넷 프레임워크에서 콜백 함수를 호출할 때 그 인자를 자동으로 넘겨주므로 걱정하지 않아도 된다. 이때 넘어오는 인자에서 AsyncDelegate라는 속성을 이용하면 해당 델리게이트를 받아올 수 있다. 따라서 이를 이용해 EndInvoke를 호출하여 결과 값을 받아오는 것이다. 결과는 다음과 같다.
SubWork : 3
SubWork : 4
SubWork : 5
................................................................................
................................................................................
.............................SubWork : 6
SubWork : 7
SubWork : 8
SubWork : 9
................................................................................
...............................MainWork : 4
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
<리스트 8>은 2의 경우로서 caller 측에서 계속 폴링(polling)하면서 연산이 다 끝났는지 확인하는 방법이다.
| |||
|
................................................................................
................................................................................
...........................................MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
................................................................................
................................................................................
......................................MainWork : 9
SubWork : 3
SubWork : 4
SubWork : 5
................................................................................
................................................................................
......................................SubWork : 6
SubWork : 7
SubWork : 8
SubWork : 9
MainWork : 4
MainWork : 5
MainWork : 6
MainWork : 7
MainWork : 8
MainWork : 9
<리스트 9>는 3의 경우인데, 이는 결국 caller 측에서 비동기 호출된 부분이 끝날 때까지 블러킹되고 있으므로, 별로 권장하는 방법은 아니다. 이렇게 되면 비동기 호출을 할 의미가 없기 때문이다. 그러나 이도 결과 값을 받는 방법의 하나이므로 알아두자. 이번 예제는 콘솔 프로그램이 아닌 윈도우용 프로그램으로 만들었다. EndInvoke를 실행했을 때 caller 측이 블러킹된다는 것을 보여주기 위해서 윈도우용 프로그램으로 만들었다.
| |||
|
<화면 1> 윈도우에서 비동기 연산을 한 결과 화면 |
이번에는 마지막 방법인 4에 대해 알아보자. 이 방법은 3번 방법과 비슷한데 한 가지 다른 점은 time out을 정해줄 수 있어서 마냥 블러킹되는 것이 아니라 일정 시간이 넘어버리면 끊어버릴 수 있는 기능을 제공한다. 이번에는 비동기 연산을 0.1초 안에 해내지 못하면 블러킹을 해제하고 main 부분을 실행하는 프로그램을 만들어 본다.
| |||
|
SubWork : 3
SubWork : 4
SubWork : 5
SubWork : 6
SubWork : 7
도중 차단
MainWork : 4
MainWork : 5
MainWork : 6
SubWork : 8
SubWork : 9
MainWork : 7
MainWork : 8
MainWork : 9
리턴 값을 이용하지 않을 경우
이상으로 비동기 호출에서 결과 값을 얻기 위한 4가지 방법을 소개했다. 그러나 비동기 호출에서 결과 값을 받기 위해 꼭 리턴 값을 이용해야 하는 것은 아니다. 우리가 인자로 넘겨줄 때 ref형이나, out형으로 넘겨주면 꼭 리턴 값을 이용하지 않더라도 그 결과 값을 받는 방법이 된다. 그러나 여기서 한 가지 주의할 것은 인자(parameter) 값의 업데이트도 비동기적으로 일어난다는 것이다. <리스트 11>을 보자.
| |||
|
EndInvoke를 호출하기 이전의 값 : 42
EndInvoke를 호출하고 난 후의 값 : 84
그런데 여기서 만약 reference type을 파라미터로 넘기게 되면 비동기 호출 부분에서는 이 값을 실시간적으로 업데이트를 한다. 즉 EndInvoke를 호출할 필요없이, 자신이 연산을 하면서 값을 지속적으로 갱신을 하는 것이다. 이번에는 reference type인 byte형 배열을 파라미터로 넘겨서 그 값을 확인해 보겠다. <리스트 12>를 보자.
| |||
|
그 주석 처리한 부분의 주석을 없애 버리면 연산이 다 끝날 때까지 기다리기 때문에 언제나 정확한 값을 얻을 수 있을 것이다. 그러나 우리는 과연 비동기 호출이 값을 실시간으로 바꾸는지 확인하기 위한 것이므로 주석 처리를 했다. 결과 값은 컴퓨터 사양에 따라 다르고 할 때마다 다른 값이 나온다. 다음은 필자 컴퓨터에서 실행한 결과이다.
결과를 보면 5번 방까지는 제대로 들어갔는데, 그 이후로 값이 안 들어와 있다. 이는 caller 측에서 비동기 연산이 아직 다 끝나지 않았는데, 이는 그 값을 꺼내봤기 때문이다.
b[0]=0
b[1]=1
b[2]=4
b[3]=9
b[4]=16
b[5]=25
b[6]=0
b[7]=0
b[8]=0
b[9]=0
쓰레드 풀을 이용하자
이번 호에서는 쓰레드를 이용한 비동기 프로그래밍의 원리와 4가지 구현 방법에 대해 알아봤다. 그런데 여기서 아직 해결하지 못한 문제점이 남아 있다. 닷넷 프레임워크는 우리가 매번 비동기 호출을 할 때마다 쓰레드를 새로 만들어서 하는 것일까?
만약 그렇다면, 쓰레드라는 것이 적당한 수가 유지된다면 문제가 안 되지만 과도한 쓰레드의 생성은 오히려 쓰레드를 교체하면서 생기는 컨텍스트 체인지 오버헤드(context change overhead)가 있을 것이다. 닷넷 프레임워크에서는 이 문제를 쓰레드 풀(pool)을 이용해서 해결하고 있다. 다음 연재에서는 이 쓰레드 풀을 이용한 비동기 호출에 대해 알아보고, 최종적으로 게임 서버를 완성할 것이다. @
'programming > c#' 카테고리의 다른 글
[펌][C#과 플래시로 온라인 게임 만들기] ③ 게임서버 완성 (0) | 2004.08.25 |
---|---|
Automate Microsoft Excel from Microsoft Visual C# .NET (0) | 2004.06.24 |
MTS를 이용하는 .Net Component 만들기 (0) | 2004.06.08 |