블록킹(Blocking)과 비동기(Asynchronous)
이철우
마이크로소프트 문서 - [참고 1] - 를 보면 다음 글귀가 나온다.
The Receive method will block until a datagram arrives from a remote host.
메서드 Receive는 원격 호스트에서 데이터그램이 도착할 때까지 차단됩니다. [구글번역]
간단하게 줄이면, ‘(무엇인가에 의해) 메서드가 차단된다.’가 되고, 부드럽게 바꾸면, ‘무엇인가가 메서드를 차단했다.’가 된다. 여기서 ‘무엇인가’는 무엇일까? [참고 2]의 두 번째 답글에 아래가 나온다.
Synchronization means two or more operations are running in a same context (thread) so that one may block another.
동기화는 둘 이상의 작업이 동일한 컨텍스트(스레드)에서 실행되어 하나가 다른 작업을 차단할 수 있음을 의미합니다. [구글번역]
[참고 2]를 적용하면, ‘무엇인가’는 쓰레드(Thread)임을 알 수 있고, 차단된 작업은 메서드 Receive가 된다. 쓰레드를 하나의 작업으로 본다는 것이 께름칙하다. 쓰레드가 메서드 Receive에 제어(Control)을 주고, 메서드 Receive는 데이터그램을 받지 못했으니, 메서드 Receive가 제어를 쓰레드에 돌려주지 않은 상태라고 해석하면 어떨까?
블록킹이란 어떤 쓰레드한테 제어를 넘겨받은 작업이 제어권을 가지고 있는 상태.
라고 정의하고 싶다.
그렇다면 블록킹은 언제 일어날까? 한 쓰레드가 어떤 작업에게 제어를 넘기는 순간 블록킹이 시작되고, 그 쓰레드가 다시 제어를 넘겨받게 되면 블록킹이 멈추게 된다. 블록킹 상태가 지속되는 기간은 수 나노 초, 수 밀리 초, 수 분, 수 시간 등 다양할 것이다. 우리 경험으로는 블록팅 상태가 수 백 밀리 초가 넘게 지속되면 UI 고객에게 불평을 듣게 되고 다른 방법을 찾게 된다.
질문 1) 블록킹 코드를 작성할 수 있는가?
대개 c# 코드로 가장 간단한 블록킹 코드는
Thread.Sleep(500);
이라고 생각할 것이다. 쓰레드가 Thread.Sleep(500)을 만나면 제어를 넘겨주고 0.5 초 뒤에 제어를 넘겨받는다. 그 쓰레드가 UI 쓰레드라면 UI가 0.5 초 동안 멈추게 된다.
또 다른 블록킹 코드는
var input = Console.ReadLine();
일 것이다. 사용자가 키보드로 무엇인가 입력하고 ‘엔터’ 단추를 누를 때까지 제어를 쓰레드에게 넘기지 않는 블록킹 상태가 지속된다.
아래 코드도 블록킹 코드라고 할 수 있다.
var sum = Sum(100, 23);
int Sum(int left, int right)
{
return left + right;
}
하지만 블록킹 상태 지속 시간이 너무 짧아 대부분 위 메서드 Sum을 호출하는 작업을 블록킹으로 생각하지 않는다. 우리가 작성하는 코드 한 줄 한 줄은 블록킹 지속 시간이 다르지만 대부분 블록킹 코드라고 할 수 있다.
질문 2) 블록킹을 어떻게 회피하는가?
먼저 블록킹을 왜 회피하여야 할까? 어떤 작업이 블록킹 상태에 있으면, 그 쓰레드(또는 context)는 더 다른 작업을 할 수 없게 된다. 하나의 쓰레드로 구동하는 응용프로그램이라면 블록킹 작업이 마무리 될 때까지 ‘멈춤’ 상태가 된다. 멀티 코어, 멀티 쓰레드 시스템의 자원을 효율적으로 활용하지 못하게 된다. 블록킹을 회피하면 많은 경우에 ‘시간 단축’이라는 결과를 낳는다.
대기 코드 - Thread.Sleep(n) - 를 활용한 블록킹 코드와 그 블록킹을 회피하는 코드를 작성하자. 실제 코드에서는 Thread.Sleep(n) 대신에
await Task.Delay(n).ConfigureAwait(false);
을 사용할 것이다. 프로그램은 ‘exit’ 를 입력하면 종료, ‘blocking’을 입력하면 주어진 시간 동안 블록킹 상태가 되고, 다른 실험 문자열을 입력하면 그 실험 문자열을 출력한다.
// BlockingTest.cs
public class BlockingTest
{
public static async Task Run(int blockingTime, bool isBlocking)
{
var exitRequired = false;
Console.WriteLine(" BlockingTest begin.");
while (!exitRequired)
{
var input = Console.ReadLine();
switch (input)
{
case "exit":
exitRequired = true;
break;
case "blocking":
if (isBlocking)
{
Console.WriteLine(" Waiting begin.");
await Task.Delay(blockingTime).ConfigureAwait(false);
Console.WriteLine(" Waiting end.");
}
else
{
_ = Task.Run(async () =>
{
Console.WriteLine(" Waiting begin.");
await Task.Delay(blockingTime).ConfigureAwait(false);
Console.WriteLine(" Waiting end.");
}).ConfigureAwait(false);
}
break;
default:
Console.WriteLine($" {input}");
break;
}
}
Console.WriteLine(" BlockingTest end.");
}
}
// Program.cs
Console.WriteLine("Hello, World!");
var isBlocking = false;
var blockingTime = 7000;
await BlockingTest.Run(blockingTime, isBlocking).ConfigureAwait(false);
Console.ReadLine();
Console.WriteLine("Bye.");
상태 isBlocking = true 로 프로그램을 실행하면, 블록킹 상태가 된 뒤에 다른 실험 문자 입력에 대해 출력이 지연됨을 알 수 있다. 상태 isBlocking = false 로 프로그램을 실행하면, 입력에 대한 출력 응답이 빨라짐을알 수 있다. 쓰레드(context) A가 어떤 블록킹 작업 J에게 제어를 넘기지 않고, J를 수행할 새로운 쓰레드(context) B를 만들어, B에게 J를 넘기고 A는 자신의 다음 작업을 수행하는 것을 ‘비동기’라고 이해할 수 있다.
한편, ‘비동기’는 시스템을 자원을 최대한 활용하기 위해 꼭 적용해야 하는 프로그램 방법이라고 생각한다.
숙제) 위 프로그램 isBlocking = false 모드에서 blockingTime이 지나가기 전에 ‘exit’를 입력하면, while 루프는 종료되지만 ‘Waiting’은 blockingTime이 지나서 종료된다. while 루프와 Waiting 종료를 ‘동기화’하는 코드를 작성하라.
[참고 1] UdpClient.Receive(IPEndPoint) Method
[참고 2] What’s a sync and async method?