비동기 호출
비동기 호출(asynchronous call)이란 '동기 호출(synchronous call)'과 대비되는 개념이다.
일반적으로 비동기 호출은 입출력(I/O) 장치와 연계되어 있다.
동기 방식의 파일 읽기
//Hosts 파일을 읽어서 내용을 출력한다.
using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
byte[] buf = new byte[fs.Length];
fs.Read(buf, 0, buf.Length);
string txt = Encoding.UTF8.GetString(buf);
Console.WriteLine(txt);
}

동기 호출 (Blocking Call)
FileStream.Read 메서드는 동기 호출(Synchronous Call)에 해당한다.
동기 호출이란 메서드가 작업을 완료할 때까지 호출한 스레드에게 제어를 반환하지 않는 방식을 의미한다.
예를 들어 FileStream.Read는 디스크에서 데이터를 읽는 I/O 작업을 수행한다.
이때 메서드를 호출한 스레드는 파일 읽기 작업이 모두 끝날 때까지 대기해야 한다.
따라서 작업이 완료되기 전까지는 다음 코드가 실행되지 않는다.
디스크 I/O가 완료될 때까지 스레드의 실행이 일시적으로 중단된다.
이러한 이유로 동기 호출은 블로킹 호출(Blocking Call) 이라고도 한다.
왜 Blocking이라고 부르는가
동기 호출에서는
- 메서드 호출
- 작업 수행
- 작업 완료 후 반환
의 순서로 진행되며, 작업이 끝날 때까지 스레드의 실행이 차단(block) 된다.
메서드 호출
↓
I/O 작업 진행
↓
스레드 대기 (block)
↓
작업 완료
↓
제어 반환
스레드가 작업이 끝날 때까지 기다려야 하기 때문에 Blocking Call이라고 부른다.

동기 호출의 단점을 해결하는 방법
동기 호출은 작업이 완료될 때까지 스레드가 대기 상태에 머무르게 된다.
예를 들어 파일 읽기와 같은 I/O 작업이 오래 걸리는 경우,
해당 작업이 끝날 때까지 스레드는 아무 일도 하지 못하고 기다리게 된다.
이러한 문제를 해결하기 위해 비동기 호출(Asynchronous Call) 방식이 사용된다.
비동기 호출은 작업을 시작한 후 작업이 완료될 때까지 기다리지 않고 즉시 제어를 반환한다.
따라서 호출한 스레드는 다른 작업을 계속 수행할 수 있다.
FileStream의 비동기 메서드
FileStream은 비동기 호출을 지원하기 위해 다음과 같은 메서드 쌍을 제공한다.
| 동기 메서드 | 비동기 메서드 |
| Read | BeginRead / EndRead |
| Write | BeginWrite / EndWrite |
이 방식은 Begin / End 패턴(Asynchronous Programming Model, APM) 이라고 불린다.
동작 방식
비동기 호출은 다음과 같은 흐름으로 동작한다.
BeginRead() 호출
↓
파일 읽기 작업 시작
↓
즉시 제어 반환
↓
다른 작업 수행 가능
↓
읽기 작업 완료
↓
EndRead() 호출하여 결과 확인
즉, 작업은 백그라운드에서 진행되고, 필요할 때 EndRead()를 호출하여 작업 결과를 얻는다.
비동기 방식의 파일 읽기 코드
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Reflection.Metadata;
using System.Text;
namespace BCL_Study
{
internal class StrStudy
{
static void Main(string[] args)
{
FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, true);
FileState state = new FileState();
state.Buffer = new byte[fs.Length];
state.Files = fs;
fs.BeginRead(state.Buffer, 0, state.Buffer.Length, readCompleted, state);
//BeginRead 비동기 메서드 호출은 스레드로 곧바로 제어를 반환하기 때문에
//이곳에서 자유롭게 다른 연산을 동시에 진행할 수 있다.
Console.ReadLine();
fs.Close();
}
//읽기 작업이 완료되면 메서드가 호출된다.
static void readCompleted(IAsyncResult ar)
{
FileState state = ar.AsyncState as FileState;
state.Files.EndRead(ar);
string txt = Encoding.UTF8.GetString(state.Buffer);
Console.WriteLine(txt);
}
}
class FileState
{
public byte[] Buffer;
public FileStream Files;
}
}
BeginRead의 동작 방식
BeginRead 메서드는 비동기 방식으로 파일 읽기 작업을 시작한다.
동기 메서드인 Read와 달리, BeginRead는 디스크에서 파일 데이터를 모두 읽어올 때까지 기다리지 않는다.
읽기 작업을 시작한 뒤 즉시 호출한 스레드에 제어를 반환한다.
따라서 BeginRead를 호출한 스레드는 읽기 작업이 진행되는 동안에도 다른 코드를 계속 실행할 수 있다.
읽기 작업 완료 후 동작
파일 읽기 작업이 완료되면 CLR은 ThreadPool에서 유휴 스레드를 하나 가져와
해당 스레드에서 콜백 메서드(예: readCompleted)를 실행한다.
즉, 작업 완료 후의 처리는 ThreadPool 스레드가 담당하게 된다.
중요한 특징
이 방식의 중요한 특징은 다음과 같다.
- BeginRead를 호출한 원래 스레드는 전혀 차단되지 않는다.
- 파일 읽기 작업은 백그라운드에서 진행된다.
- 작업이 완료되면 ThreadPool 스레드가 콜백을 실행한다.

비동기 호출은 I/O 연산이 끝날 때까지 차단되지 않으므로 논블로킹 호출(non-blocking-call)이라고도 한다.
비동기 호출이 스레드를 직접 사용한 방식이나 ThreadPool, QueueUserWorkItem을 사용한 것과 어떤 차이가 있는가?
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Reflection.Metadata;
using System.Text;
namespace BCL_Study
{
internal class StrStudy
{
static void Main(string[] args)
{
ThreadPool.QueueUserWorkItem(readCompleted);
//QueueUserWorkItem 메서드 호출은 곧바로 제어를 반환하기 때문에
//이곳에서 자유롭게 다른 연산을 동시에 진행할 수 있다.
Console.ReadLine();
}
//읽기 작업을 스레드 풀에 대행한다.
static void readCompleted(object state)
{
using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\HOSTS", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
byte[] buf = new byte[fs.Length];
fs.Read(buf, 0, buf.Length);
string txt = Encoding.UTF8.GetString(buf);
Console.WriteLine(txt);
}
}
}
class FileState
{
public byte[] Buffer;
public FileStream Files;
}
}
얼핏 보면 효과는 동일하지만, 읽기 작업을 동기 호출로 ThreadPool의 스레드에 대행했으므로 QueueUserWorkItem 메서드를 호출한 측의 스레드는 다른 작업을 할 수 있다. 하지만 이를 자세히 보면 분명한 차이가 있다.

최초의 스레드가 자유롭게 된 상황은 같지만, 스레드 풀로부터 빌려온 스레드의 사용 시간은 길어졌다.
결론
- 일반적인 목적의 응용 프로그램에서 QueueUserWorkItem과 비교했을 때 비동기 호출로 얻는 이득은 크지 않다.
- 이 정도의 차이가 의미 있는 경우는 동시 접속자 수가 많은 게임 서버나 웹 서버 등이 있다.
System.Delegate의 비동기 호출
일반적으로 비동기 호출은 입출력 장치와의 속도 차이에서 오는 비효율적인 스레드 사용 문제를 극복하는데 사용된다.
.NET에서는 특이하게 입출력 장치뿐만 아니라 일반 메서드에 대해서도 비동기 호출을 할 수 있는 수단을 제공하는데, Delegate가 이 역할을 한다. 즉, 메서드를 델리게이트로 연결해두면 이미 비동기 호출을 위한 기반이 마련된 것이다.


델리게이트의 비동기 호출을 위한 메서드(BeginInvoke/EndInvoke)를 사용하면 calc 인스턴스에 할당된 Calc.Cumsum 메서드의 수행을 ThreadPool의 스레드에서 실행할 수 있다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Reflection.Metadata;
using System.Text;
namespace BCL_Study
{
internal class StrStudy
{
public delegate long CalcMethod(int start, int end);
static void Main(string[] args)
{
CalcMethod calc = new CalcMethod(Calc.Cumsum);
//Delegate 타입의 BeginInvoke 메서드를 호출한다.
//이 때문에 Calc.Cumsum 메서드는 ThreadPool의 스레드에서 실행된다.
IAsyncResult ar = calc.BeginInvoke(1, 100, null, null);
//BeginInvoke로 반환받은 IAsyncResult 타입의 AsyncWaitHandle 속성은 EventWaitHandle 타입이다.
//AsyncWaitHandle 객체는 스레드 풀에서 실행된 Calc.Cumsum의 동작이 완료됐을 때 Signal 상태로 바뀐다.
//따라서 아래와 같이 호출하면 Calc.Cumsum 메서드 수행이 완료될 때까지 현재 스레드를 대기시킨다.
ar.AsyncWaitHandle.WaitOne();
//Calc.Cumsum의 반환값을 얻기 위해 EndInvoke 메서드를 호출한다.
//반환값이 없어도 EndInvoke는 반드시 호출하는 것을 권장한다.
long result = calc.EndInvoke(ar);
Console.WriteLine(result);
}
}
public class Calc
{
public static long Cumsum(int start, int end)
{
long sum = 0;
for(int i = start; i<= end; i++)
{
sum += i;
}
return sum;
}
}
}

BeginInvoke와 EndInvoke의 사용이 복잡해 보일 수 있다. 하지만 동일한 기능을 Thread 타입이나 EventWaitHandle과 ThreadPool의 조합으로 구현했던 것과 비교하면 소스코드가 더 간결하다.
Delegate를 이용한 비동기 코드는 FileStream.BeginRead를 사용했던 방식도 지원한다.
방법도 매우 유사한데 BeginInvoke의 3번째 인자에 콜백 메서드를 지정해 주면 된다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using System.Reflection.Metadata;
using System.Text;
namespace BCL_Study
{
internal class StrStudy
{
public delegate long CalcMethod(int start, int end);
static void Main(string[] args)
{
CalcMethod calc = new CalcMethod(Calc.Cumsum);
calc.BeginInvoke(1, 100, calcCompleted, calc);
Console.ReadLine();
}
static void calcCompleted(IAsyncResult ar)
{
CalcMethod calc = ar.AsyncState as CalcMethod;
long result = calc.EndInvoke(ar);
Console.WriteLine(result);
}
}
public class Calc
{
public static long Cumsum(int start, int end)
{
long sum = 0;
for(int i = start; i<= end; i++)
{
sum += i;
}
return sum;
}
}
}

Begin / End 메서드 패턴의 의미
.NET BCL에서 제공되는 클래스 중
BeginXXX / EndXXX 메서드가 쌍으로 존재한다면
이는 비동기 호출 패턴(APM: Asynchronous Programming Model) 을 의미한다.
이 패턴의 특징은 다음과 같다.
- BeginXXX
- 비동기 작업을 시작한다.
- 즉시 IAsyncResult를 반환한다.
- 호출한 스레드는 차단되지 않는다.
- EndXXX
- 비동기 작업이 끝난 후 결과를 가져온다.
- 필요한 경우 작업이 끝날 때까지 대기한다.
Delegate와의 관계
이 패턴은 Delegate 비동기 호출 방식과 구조가 동일하다.
Delegate에서도 BeginInvoke, EndInvoke 형태로 제공되며 동작 방식이 같다.
EndInvoke → 결과 반환
그래서 많은 .NET API들이 Delegate 비동기 호출 패턴을 기반으로 설계되었다.
Reference
시작하세요! C# 12 프로그래밍 기본 문법부터 실전 예제까지
이미지 출처
[C#] 비동기 호출
비동기 호출 비동기 호출(asynchronous call)이란 "동기 호출(synchronous call)"과 대비되는 개념이다. 일반적으로 비동기 호출은 입출력(I/O) 장치와 연계되어 설명될때가 많다. 예를 들어, 파일의 데이터
andjjip.tistory.com
'C#' 카테고리의 다른 글
| C# BCL(Base Class Library) - 스레딩1 (0) | 2026.03.15 |
|---|---|
| C# BCL(Base Class Library) - 파일 (0) | 2026.03.10 |
| C# BCL(Base Class Library) - 컬렉션 (0) | 2026.03.09 |
| C# BCL(Base Class Library) - 직렬화/역직렬화 (0) | 2026.03.07 |
| C# BCL(Base Class Library) - 문자열 처리 (0) | 2026.03.07 |