10.1 호출자 정보

C++의 __LINE__, __FILE__ 와 같은 매크로 상수를 대체한 C#의 호출자 정보

 

C# 대응 방식 – Caller Info

C#에서는 다음 특성을 이용해 메서드의 호출 위치 정보를 자동으로 전달받을 수 있다:

  • [CallerFilePath] → 호출한 소스 파일 경로
  • [CallerLineNumber] → 호출한 소스 코드 라인 번호
  • [CallerMemberName] → 호출한 메서드/속성 이름

호출자 정보는 매크로를 통한 구현이 아닌 Attribute(특성)과 선택적 매개변수의 조합으로 구성한다.

 

예제 10.1 호출자 정보사용

using System.Runtime.CompilerServices;

LogMessage("TEST");

void LogMessage(string text,
    [CallerMemberName] string memberName = "",
    [CallerFilePath] string filePath = "",
    [CallerLineNumber] int lineNumber = 0)
{
    Console.WriteLine($"텍스트: {text}");
    Console.WriteLine($"Log Message를 호출한 메서드 이름: {memberName}");
    Console.WriteLine($"Log Message를 호출한 소스코드의 파일명: {filePath}");
    Console.WriteLine($"Log Message를 호출한 소스코드의 라인 번호: {lineNumber}");
}

# 출력
텍스트: TEST
Log Message를 호출한 메서드 이름: < Main >$
Log Message를 호출한 소스코드의 파일명: C: \Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs
Log Message를 호출한 소스코드의 라인 번호: 4

CallerMemberName], [CallerFilePath], [CallerLineNumber]는 "속성(Attribute)"이지만, 메서드나 프로퍼티에 직접 다는 것이 아니라 매개변수 에 다는 특성(Attribute)이라는 점이다.

[CallerMemberName] // ❌ 메서드나 속성 위에 직접 붙이면 안 됨
void LogMessage(string text) { ... }

 

'호출자 정보' : 호출하는 측의 정보를 메서드의 인자로 전달하는 것.

 

표10.1 C# 5.0에서 제공되는 호출자 정보

특성 설명
CallerMemberName 호출자 정보가 명시된 메서드를 호출한 측의 메서드 이름
CallerFilePath 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 파일경로
CallerLineNumber 호출자 정보가 명시된 메서드를 호출한 측의 소스코드 라인 번호

 

호출자 정보 특성이 명시된 매개변수는 반드시 선택적 매개변수여야한다.

호출자 정보는 c# 컴파일러에 의해 소스코드 컴파일 시 호출자 정보 특성에 따른 인자값으로 치환되어 빌드된다.

LogMessage("TEST", "Main", @"C:\Users\asd57\source\repos\ConsoleApp5\ConsoleApp5\Program.cs", 10);
//메시지, 해당 메시지 호출 메서드, 메시지 호출 메서드의 경로, 라인넘버

 

 

10.2 비동기 호출

C# 5.0에서는 async, await 예약어가 도입됐다. 해당 예약어들은 비동기방식을 동기방식처럼 호출하는 코드를 작성할 수있다.

 

이전에 6.6절 '스레딩'에서 다룬 6.28 예제(동기 방식의 파일 읽기), 6.29(비동기 방식의 파일 읽이)에선 비동기 호출의 복잡성을 보았다.

다음 코드는 디스크로부터 파일의 내용을 읽는 동기방식의 Read메서드는 명령어가 순차적으로 실행한다.

using System.Text;

using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\services", FileMode.Open,
    FileAccess.Read, FileShare.ReadWrite))
{
    byte[] buf = new byte[fs.Length];
    fs.Read(buf, 0, buf.Length);

    //스레드가 Read 메서드를 완료 후 파일의 내용을 화면에 출력하는 코드를 순차적으로 실행
    string txt = Encoding.UTF8.GetString(buf);
    Console.WriteLine(txt);
}

#출력
# Copyright (c) 1993-2004 Microsoft Corp.
#
# This file contains port numbers for well-known services defined by IANA
#
# Format:
#
# <service name>  <port number>/<protocol>  [aliases...]   [#<comment>]
#

echo                7/tcp
echo                7/udp
discard             9/tcp    sink null
discard             9/udp    sink null
systat             11/tcp    users                  #Active users
systat             11/udp    users                  #Active users
daytime            13/tcp
daytime            13/udp

    ....

 

상기 동기 버전을 비동기 버전으로 변환 시 비동기 버전의 BeginRead 메서드를 호출 시 Read 동작 이후의 코드를 별도 분리해 Completed같은 형식의 메서드에 담아 처리해야하는 불편함이 있었다.

* C# 6.0 부터 catch/finally 블록 내에서도 await을 사용할 수 있도록 개선 함.(11.10 절 '기타 개선 사항')

using System.Text;


using (FileStream fs = new FileStream(@"C:\windows\system32\drivers\etc\services", FileMode.Open,
    FileAccess.Read, FileShare.ReadWrite, 4096, true))  //Async : true
{
    FileState state = new();
    state.Buffer = new byte[fs.Length];
    state.File = fs;

    fs.BeginRead(state.Buffer, 0, state.Buffer.Length, readCompleted, state);
    //Read가 완료된 후의 코드를 readCompleted로 넘겨서 처리.

    Console.ReadLine();
    fs.Close();
}

// 읽기 작업이 완료되면 스레드 풀의 자유 스레드가 readCompleted 메서드를 실행
void readCompleted(IAsyncResult ar)
{
    FileState state = ar.AsyncState as FileState;
    state.File.EndRead(ar);

    string txt = Encoding.UTF8.GetString(state.Buffer);
    Console.WriteLine(txt);
}

class FileState
{
    public byte[] Buffer;
    public FileStream File;
}



#출력
# Copyright (c) 1993-2004 Microsoft Corp.
#
# This file contains port numbers for well-known services defined by IANA
#
# Format:
#
# <service name>  <port number>/<protocol>  [aliases...]   [#<comment>]
#

echo                7/tcp
echo                7/udp
discard             9/tcp    sink null
discard             9/udp    sink null
systat             11/tcp    users                  #Active users
systat             11/udp    users                  #Active users
daytime            13/tcp
    .....

 

동기와 비동기 코드를 보면 동기에서 비동기로 바꿀 시 Read 호출 이후의 코드를 BeginRead에 전달하는 것으로 해결된다.

 

그림 10.1 동기식을 비동기로 바꾸는 요령

 

예제 10.2 async/await이 적용된 비동기 호출

using System.Text;

class Program
{
    static void Main(string[] args)
    {
        AwaitRead();
        Console.ReadLine();
    }

    private static async void AwaitRead()
    {
        using (FileStream fs = 
            new FileStream(@"C:\windows\system32\drivers\etc\services", FileMode.Open, FileAccess.Read,
            FileShare.ReadWrite, 4096, true))
        {
            byte[] buffer = new byte[fs.Length];
            await fs.ReadAsync(buffer, 0, buffer.Length);

            //아래 두라인은 c#컴파일러가 분리해 ReadAsync 비동기 호출이 완료 된 후 호출
            string txt = Encoding.UTF8.GetString(buffer);
            await Console.Out.WriteLineAsync(txt);
        }
    }
}


#출력
# Copyright (c) 1993-2004 Microsoft Corp.
#
# This file contains port numbers for well-known services defined by IANA
#
# Format:
#
# <service name>  <port number>/<protocol>  [aliases...]   [#<comment>]
#

echo                7/tcp
echo                7/udp
discard             9/tcp    sink null
discard             9/udp    sink null
systat             11/tcp    users                  #Active users
    ...

 

닷넷에 구현된 FileStream 타입은 await 비동기 호출에 사용되는 ReadAsync 메서드를 새롭게 제공한다.

ReadAsync 메서드는 BeginRead와 마찬가지로 비동기로 호출된다.

 

...Async 류의 비동기 호출에 await 예약어가 함께 쓰이면 C#컴파일러는 이를 인지한 후 그 이후의 코드를 묶어서 ReadAsync의 비동기 호출이 끝난 후에 실행되도록 코드를 변경해서 컴파일한다. 그 덕에 비동기 호출을 마치 동기 호출처럼 코드를 작성할 수 있다.

 

비동기 처리 여부는 코드를 수행하는 스레드 ID를 출력하는 코드를 넣어 아래와 같이 직접 확인해보자.

using System.Diagnostics.Metrics;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        AwaitRead();
        Console.ReadLine();
    }

    private static async void AwaitRead()
    {
        using (FileStream fs = 
            new FileStream(@"C:\windows\system32\drivers\etc\services", FileMode.Open, FileAccess.Read,
            FileShare.ReadWrite, 4096, true))
        {
            byte[] buffer = new byte[fs.Length];
            
            Console.WriteLine("Before ReadAsync: " + Thread.CurrentThread.ManagedThreadId);
            await fs.ReadAsync(buffer, 0, buffer.Length);
            Console.WriteLine("After ReadAsync: " + Thread.CurrentThread.ManagedThreadId);

            //아래 두라인은 c#컴파일러가 분리해 ReadAsync 비동기 호출이 완료 된 후 호출
            string txt = Encoding.UTF8.GetString(buffer);
            await Console.Out.WriteLineAsync(txt);
        }
    }
}


#출력
Before ReadAsync: 1
After ReadAsync: 9
# Copyright (c) 1993-2004 Microsoft Corp.
#
# This file contains port numbers for well-known services defined by IANA
#
# Format:
#
# <service name>  <port number>/<protocol>  [aliases...]   [#<comment>]
    ...

 

await fs.ReadAsync 메서드가 호출되기 전의 스레드 ID와 호출 후의 스레드 ID가 다르게 출력된다.

await 이후의 코드는 C#컴파일러에 의해 분리돼 ReadAsync 작업이 완료된 후 별도의 스레드에서 실행된다.

이 떄의 스레드 관계를 정리해보면 그림 10.2 처럼 나타낼 수 있고 이는 BeginRead를 이용했던 그림 6.17 상황과 거의 같다.

 

그림 10.2 await를 적용한 후의 스레드 실행관계 (p665 참고)

 

async/await은 문맥 예약어이다. await 그대로는 변수로 사용 가능하나 async가 지정되있으면 예약어로 인식되어 변수로 사용 불가이다.

void test()
{
    int await = 5;
    Console.WriteLine(await);
}

//컴파일 에러x
#출력
5


만일 
async void test()
{
    int await = 5;
    Console.WriteLine(await);
}

async 예약어 사용 시 컴파일에러 발생./

 

async 예약어는 await 토큰을 C# 컴파일러에게 식별자로서 인식할 것이냐, 예약어로서 인식할 것이냐를 알려주는 역할이다.

 

 

10.2.1 닷넷 BCL에 추가된 Async 메서드

async/await의 도입으로 비동기 호출코드가 매우 간결해졌다. 기존의 BCL 라이브러리에 제공되던 복잡한 비동기 처리에 async/await 호출이 가능한 메서드를 추가했다.

 

FileStream과는 다른 방식으로 비동기를 지원하는 WebClient 타입은 HTTP 요청을 전달해 그 응답을 문자열로 반환하는 DownloadString 메서드를 제공한다. 예제 10.3에서 이를 이용한 동기호출을 알아보자.

 

예제 10.3 동기식 코드

using System.Net;
using static IronPython.Modules._ast;
using static System.Net.WebRequestMethods;
using System.Runtime.InteropServices;

WebClient wc = new();
string txt = wc.DownloadString("https://www.naver.com");
Console.WriteLine(txt);

#출력
 < !doctype html > < html lang = "ko" class= "fzoom" > < head > < meta charset = "utf-8" > < meta name = "Referrer" content = "origin" > < meta http - equiv = "X-UA-Compatible" content = "IE=edge" > < meta name = "viewport" content = "width=1190" > < title > NAVER </ title > < meta name = "apple-mobile-web-app-t
    
    ...  HTTP 식으로 출력됨.

 

예제 10.3 비동기식 코드

using System.Net;
using static IronPython.Modules._ast;
using static System.Net.WebRequestMethods;
using System.Runtime.InteropServices;

WebClient wc = new();

//DownloadStringAsync 동작이 완료됐을 때 호출할 이벤트 등록
wc.DownloadStringCompleted += wc_DownloadStringCompleted;

// DownloadString의 비동기 메서드
wc.DownloadStringAsync(new Uri("https://www.naver.com"));

Console.ReadLine();
void wc_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
    Console.WriteLine(e.Result);  // e.Result == HTML 텍스트
}


#출력
Http 식으로 출력

 

C# 5.0의 async/await 를 통해 마치 동기호출을 하는 것처럼 다음과 같이 작성 가능하다.(간단한 코드가 됨)

using System.Net;
using static IronPython.Modules._ast;
using static System.Net.WebRequestMethods;
using System.Runtime.InteropServices;

AwaitDownloadString();
Console.ReadLine();
async void AwaitDownloadString()
{
    WebClient wc = new();
    string txt = await wc.DownloadStringTaskAsync("https://www.naver.com");
    Console.WriteLine(txt);
}


#출력
html 식으로 출력

 

 

6.7.6 절 System.Net.Http.HttpClient의 동기 호출한 HttpClient 예제(p491)는 다음과 같이 비동기 호출을 만들 수 있다.

using System.Net;
using static IronPython.Modules._ast;
using static System.Net.WebRequestMethods;
using System.Runtime.InteropServices;

internal class Program
{
    static HttpClient _client = new HttpClient();
    private static void Main(string[] args)
    {
        AwaitDownloadString();
        Console.ReadLine();
    }
    private static async void AwaitDownloadString()
    {
        // Result 속성을 접근해 동기식으로 호출했던 코드를
        // string txt = _client.GetStringAsync("https://www.naver.com").Result;

        //비동기 호출로 반환
        string txt = await _client.GetStringAsync("https://www.naver.com");
        Console.WriteLine(txt);
    }
}

 

6.7절 네트워크 통신에서 복잡하게 다룬 예제 6.39 'TCP 서버의 비동기 통신'을 다음과 같은 비동기 식 코드로 더 간단히 표현가능하다. 

예제 6.39 (p482)

using System.Net;
using System.Net.Sockets;
using System.Text;

public class AsyncStateData
{
    public byte[] Buffer;
    public Socket Socket;
}

class Program
{
    static void Main(string[] args)
    {
        // ... [ 생략 ]
    }

    private static void serverFunc(object obj)
    {
        using (Socket srvSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
        {
            IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 11200);

            srvSocket.Bind(endPoint);
            srvSocket.Listen(10);
            while (true)
            {
                Socket clientSocket = srvSocket.Accept();

                AsyncStateData data = new AsyncStateData();
                data.Buffer = new byte[1024];
                data.Socket = clientSocket;

                clientSocket.BeginReceive(data.Buffer, 0, data.Buffer.Length,
                    SocketFlags.None, asyncReceiveCallback, data);
            }
        }
    }

    private static void asyncReceiveCallback(IAsyncResult asyncResult)
    {
        AsyncStateData rcvData = asyncResult.AsyncState as AsyncStateData;

        int nRecv = rcvData.Socket.EndReceive(asyncResult);
        string txt = Encoding.UTF8.GetString(rcvData.Buffer, 0, nRecv);

        byte[] sendBytes = Encoding.UTF8.GetBytes($"Hello: {txt}");
        rcvData.Socket.BeginSend(sendBytes, 0, sendBytes.Length, SocketFlags.None, asyncSendCallback, rcvData.Socket);
    }

    private static void asyncSendCallback(IAsyncResult asyncResult)
    {
        Socket sock = asyncResult.AsyncState as Socket;
        sock.EndSend(asyncResult);
    }

    // ... [ 생략 ]
}

 

-> async/await 코드 변경

using System.Net;
using System.Net.Sockets;
using System.Text;

class Program
{
    static void Main(string[] args)
    {
        Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        listener.Bind(new IPEndPoint(IPAddress.Any, 11200));
        listener.Listen(10);

        while (true)
        {
            var client = listener.Accept();
            ProcessTcpClient(client);
        }
    }

    private static async void ProcessTcpClient(Socket client)
    {
        byte[] buffer = new byte[1024];
        int received = await client.ReceiveAsync(buffer);

        string txt = Encoding.UTF8.GetString(buffer, 0, received);

        byte[] sendBuffer = Encoding.UTF8.GetBytes($"Hello: {txt}");
        await client.SendAsync(sendBuffer);
        client.Close();
    }
}

해당 코드는 동기식 호출 패턴과 매우 유사하나 예제 6.39처럼 비동기 방식으로 동작한다.

코드의 양과 복잡도를 비교해서 async/await을 통한 비동기 호출의 장점이 드러난다.

 

이밖에도 I/O를 담당하는 Stream 기반 클래스를 비롯해 몇몇 타입에 기존의 Begin/End 델리게이트로 구현된 비동기 메서드에 대응하는 Async메서드가 추가됐다.

 

10.2.2 Task, Task<TReuslt> 타입

Task는 "지금 당장은 실행하지 않아도 되지만, 나중에 실행될 수 있는 코드 블록"이다.
이 코드를 **스레드풀(백그라운드 스레드)**을 사용해서 실행하거나, 비동기로 처리할 수 있게 해준다.


✅ Task의 주요 기능

기능설명예시
비동기 실행 백그라운드에서 작업 실행 Task.Run(() => Console.WriteLine("Hello"));
병렬 처리 지원 여러 작업을 동시에 실행 여러 Task를 생성하고 .WaitAll() 등으로 동시 처리
결과 반환 Task<TResult>로 값 반환 가능 Task<int>.Run(() => 1 + 2)
예외 처리 예외도 Task 안에 캡처됨 .Exception 속성으로 접근
작업 완료 대기 .Wait() 또는 .Result로 대기 블로킹 방식
작업 완료 후 처리 .ContinueWith()로 후속 작업 연결 체이닝 처리 가능
취소(Cancellation) CancellationToken 지원 사용자 요청으로 작업 중단 가능

 

비동기 호출 vs 병렬 호출

  • Task는 비동기(Async)도 되고 병렬(Parallel)도 된다.
  •  
// 비동기 + 병렬: 백그라운드 스레드에서 실행됨
Task.Run(() => DoSomething());

// 병렬 처리 예: 여러 작업 동시에 실행
Task.WaitAll(
    Task.Run(() => WorkA()),
    Task.Run(() => WorkB())
);

 

 

지금까지 사용했던 async - await 메서드를 보면 다음과 같은 공통점이 있다.

FileStream 타입
-> public Task<int> ReadAsync(byte[] buffer, int offset, int count);

Socket 타입
-> public Task<int> ReceiveAsync(ArraySegment<byte> buffer);
-> public Task<int> SendAsync(ArraySegment<byte> buffer);

await으로 대기할 수 있는 ... Async 메서드의 반환값이 모두 Task 또는 Task<TResult> 유형이다.

 

Task 타입은 반환값이 없는 경우 사용되고, Task<TResult> 타입은 TResult 형식 매개변수로 지정된 반환값이 있는 경우로 구분되는데, await 비동기 처리와는 별도로 원래부터 닷넷에 추가된 병렬 처리 라이브러리(TPL: Task Parallel Library)에 속한 타입이다.

이에 따라 await 없이 Task 타입을 단독 사용할 수 있다.

6.6절 '스레딩' - ThreadPool.QueueUserWorkItem 메서드의 대용으로 사용 가능하다.

 

[정리]

 

  • Task와 Task<TResult>는 .NET의 TPL (Task Parallel Library)**에 속한 타입이다.
  • 이 타입들은 원래부터 병렬 처리(비동기 아님)를 위한 용도로 .NET에 추가된 것이다.
  • await는 C#의 비동기 문법(Async/Await) 기능이고, Task는 TPL의 병렬 처리 기능이다.
  • 따라서 await 없이도 Task는 사용할 수 있다는 말이다.

Task vs Task<TResult>

타입용도설명
Task 반환값 없음 예: Task.Run(() => Console.WriteLine("Hello"));
Task<TResult> 반환값 있음 예: Task<int>.Run(() => 3 + 5); → 나중에 .Result로 값 꺼냄

 

await 없이 사용한 예제

Task t = Task.Run(() => {
    Console.WriteLine("작업 실행 중...");
});
// 비동기 처리 안 하고 그냥 동기로 기다림
t.Wait();  // await 안 씀

 

Task<int> t = Task.Run(() => {
    return 10 + 20;
});
int result = t.Result; // await 안 써도 결과 받음 (동기 방식)
Console.WriteLine(result);

#출력
30

결론

  • Task는 원래 병렬 작업을 표현하는 타입이다.
  • async/await는 나중에 추가된 비동기 문법이고, 이 문법이 Task와 Task<TResult>를 편하게 다루도록 확장한 것이다.
  • await는 선택 사항이다. 원래는 Task.Wait() 또는 Task.Result처럼 동기적으로 사용할 수 있었다.

 

예제 10.4 Task 타입

//기존의 QueueUserWorkItem으로 별도의 스레드에서 작업 수행
using System.Diagnostics;

ThreadPool.QueueUserWorkItem(
    (obj) =>
    {
        Console.WriteLine("process workItem");
    }, null);

// Task 타입을 이용해 별도의 스레드에서 작업 수행
Task task1 = new Task(
    () =>
    {
        Console.WriteLine("process taskItem");
    });

task1.Start();


Task task2 = new Task(
    (obj) =>
    {
        Console.WriteLine("process taskItem(obj)");
    }, null);

task2.Start();

Console.ReadLine();


#출력
process workItem
process taskItem
process taskItem(obj)

 

Task 타입의 생성자는 C# 3.0에 추가됐던 Action 타입의 델리게이트 인자를 받는다. 그리고 Start 메서드가 호출되면 내부적으로 ThreadPool의 자유 스레드를 이용해 Action 델리게이트로 전달된 코드를 수행한다.

 

Task 타입이 ThreadPool의 QueueUserWorkItem과 차별화된 점은 좀 더 세밀하게 제어 가능하다는 점이다.

 

Task vs QueueUserWorkItem

더보기

QueueUserWorkItem과 Task는 .NET에서 백그라운드 작업(비동기, 병렬 처리)을 실행하는 방식이지만, 세대가 다르고 목적도 약간 다르다.


1. QueueUserWorkItem이란?

ThreadPool.QueueUserWorkItem은 .NET 초창기부터 있던 API로, 스레드풀(ThreadPool)에 작업을 큐잉해서 백그라운드에서 실행하게 한다.

ThreadPool.QueueUserWorkItem(state => {
    Console.WriteLine("백그라운드 작업 실행 중");
});
  • 반환값이 없음
  • 작업 완료를 추적할 방법이 없음
  • 오류 처리 어려움
  • 콜백 기반 (Action<object>)

2. Task와의 차이점


 

항목 QueueUserWorkItem Task
등장 시기 .NET 초창기 .NET 4.0 이후 (TPL)
반환값 없음 Task, Task<TResult>로 반환값 있음
완료 추적 불가 가능 (await, .Wait(), .Result, .ContinueWith)
예외 처리 불편함 try-catch와 .Exception으로 편리함
취소 지원 없음 CancellationToken 지원
코드 가독성 낮음 높음 (async/await과 결합 가능)

 

예시 비교

▶ QueueUserWorkItem

ThreadPool.QueueUserWorkItem(state => {
    Console.WriteLine("QWI 작업 실행 중");
});

 

▶ Task

Task.Run(() => {
    Console.WriteLine("Task 작업 실행 중");
});

관계 요약

  • 둘 다 내부적으로는 ThreadPool을 사용하지만,
  • Task는 QueueUserWorkItem보다 더 현대적이고 구조화된 방식을 제공함
  • Task는 비동기 프로그래밍의 핵심 도구로 발전, async/await과 자연스럽게 연동 가능

결론

  • QueueUserWorkItem은 낡은 저수준 API
  • Task는 새로운 고수준 API이며, 대부분의 경우 Task를 사용하는 것이 좋다

 

QueueUserWorkItem의 경우 전달된 작업이 완료되기를 기다리는 코드를 작성하려면 예제 6.27 '개선된 ThreadPool의 사용 예' 처럼 EventWaitHandle 타입과 함께 사용해야 했으나 Task 타입을 이용하면 다음과 같이 코드가 간결하다.

Task taskSleep = new Task(() => { Thread.Sleep(5000); });
taskSleep.Start();
taskSleep.Wait(); // Task의 작업이 완료될 때까지 현재 스레드를 대기함.

 

 

Task 객체를 생성할 필요 없이 Action 델리게이트를 전달하자마자 곧바로 작업을 시작하게 만들 수 있다. 이를 위해 Task 타입은 TaskFactory 타입의 Factory 정적 속성을 제공하며 StartNew 메서드를 사용하면 예제 10.4를 좀 더 단순하게 작성 가능하다.

Task.Factory.StartNew(
    () => { Console.WriteLine("process taskItem"); });
Task.Factory.StartNew(
    (obj) => { Console.WriteLine("process taskItem(obj)"); }, null);

Console.ReadLine();

#출력
process taskItem
process taskItem(obj)

 

Task<TResult> 타입은 Task와 달리 값을 반환할 수 있다. QueueUserWorkItem과 차별화된 Task<TResult> 타입의 장점이 반환값을 반환하는 것인데, 일반적으로 QueueUserWorkItem의 경우 단순 코드를 스레드 풀의 자유드레드에 던져서 실행하는 것만 가능했던 반면, Task<TResult> 타입은 코드의 실행이 완료된 후 원한다면 반환값까지 처리가 가능하다.

 

Task<int> task = new Task<int>(
    () =>
    {
        Random rand = new Random((int)DateTime.Now.Ticks);
        return rand.Next();
    });

task.Start();
task.Wait();
Console.WriteLine($"무작위 숫자값: {task.Result}");

#출력
무작위 숫자값: 973817965

 

Task 타입의 생성자가 Action 델리게이트를 인자로 받았던 반면, Task<TResult> 타입은 Func 델리게이트를 인자로 받는다.

반환값은 작업이 완료된 후 Result 속성을 통해 구할 수 있다. 대상 Task가 완료되지 않았다면 그때까지 대기 기능도 함께 가지고 있는데 task.Wait 메서드 호출이 그 기능이다.

task.Wait 메서드 호출 후 Result를 출력하고 있으나 사실 상 *task.Wait 호출을 제거해도 무방하다.(Task<TResult>.Result는 작업이 끝날 때까지 자동으로 기다렸다가 결과를 리턴하기 때문에, task.Wait()를 굳이 먼저 호출하지 않아도 된다는 말이다.)

 

*6.7.6 절 System.Net.HttpClient 예제 코드에서 await과 함께 호출했음에도 동기방식으로 동작한 것은 Result 속성을 접근했기 때문이다.

더보기

원문 요약

await을 썼음에도 동기처럼(=기다리느라 멈춤) 동작한 이유는, .Result를 접근했기 때문이다.

쉽게 설명

HttpClient에서 await을 써도, 그 뒤에 .Result를 접근하면 비동기 흐름이 깨지고, 동기 방식으로 기다림이 발생한다는 뜻이야.

 

HttpClient client = new HttpClient();

// 비동기로 요청하지만...
Task<HttpResponseMessage> task = client.GetAsync("https://example.com");

// 여기서 Result를 접근하면 **동기처럼 블로킹됨**
HttpResponseMessage response = task.Result;

 

 

  • GetAsync()는 비동기로 동작하지만
  • .Result는 "작업 끝날 때까지 기다려서 결과 꺼냄"동기화
  • 따라서 await을 쓴 의미가 사라짐

바른 방법 (비동기 유지)

HttpClient client = new HttpClient();
HttpResponseMessage response = await client.GetAsync("https://example.com");
  • await을 쓰면 진짜 비동기적으로 흐름을 유지할 수 있음
  • UI도 멈추지 않고, 데드락도 피할 수 있음

 핵심 요점 정리

항목설명
.Result Task가 끝날 때까지 기다림 (동기)
await 진짜 비동기 흐름을 유지
함께 쓰면? await은 무용지물이 됨. 동기처럼 작동

 

 

Result 때문에 발생할 수 있는 데드락(deadlock)

상황: UI 스레드 + await + .Result

▶ 예시 코드 (WPF 또는 WinForms)

public string GetResult()
{
    var client = new HttpClient();
    var task = client.GetStringAsync("https://example.com");
    return task.Result;  // 💥 여기서 데드락 발생 가능
}

무슨 일이 일어나는가?

  1. GetStringAsync()는 비동기로 동작 → 내부적으로 UI 스레드에서 다시 실행하려고 기다림
  2. .Result는 그 작업이 끝날 때까지 현재 스레드(UI 스레드)를 블로킹함
  3. 근데 그 비동기 작업은 UI 스레드에서 재개되어야 끝남
  4. 양쪽이 서로를 기다림데드락 발생

📌 이 현상은 싱글 스레드(SynchronizationContext)가 있는 환경에서만 발생함
(ex: WPF, WinForms, ASP.NET 초기 버전)

 

UI 스레드

├─ .Result → 현재 스레드를 잠금

└─ GetStringAsync의 continuation이 이 스레드를 다시 쓰려고 대기

 

둘이 서로 기다리는 막힌 상태 = 데드락

 

해결 방법

비동기는 끝까지 비동기로 처리해야 안전하다:

public async Task<string> GetResultAsync()
{
    var client = new HttpClient();
    return await client.GetStringAsync("https://example.com");  // ✅ 안전
}

또는 만약 반드시 동기 호출이 필요하다면, **ConfigureAwait(false)**를 사용해 SynchronizationContext를 제거할 수도 있다:

public string GetResult()
{
    var client = new HttpClient();
    var task = client.GetStringAsync("https://example.com").ConfigureAwait(false);
    return task.GetAwaiter().GetResult();  // ⚠️ 이건 좀 더 안전함
}

 

결론

  • .Result, .Wait()은 비동기 흐름을 동기적으로 막아버림
  • 특히 UI 환경에서는 데드락 발생 위험 큼
  • 해결책은 끝까지 async/await로 처리하거나,
    .ConfigureAwait(false)를 명시적으로 사용하는 것

 

ConfigureAwait(false)란?

await을 사용할 때, 기본적으로 .NET은 **"이전 스레드(보통 UI 스레드)로 돌아가서 이어서 실행"**하려고 해.

하지만, 서버나 백그라운드 환경에서는 그게 불필요하거나 해로울 수 있어.
그래서 .ConfigureAwait(false)를 쓰면 다음과 같은 효과가 있어:


✅ 효과 요약

항목기본 await (ConfigureAwait(true))ConfigureAwait(false) 사용 시
이어서 실행되는 스레드 원래 스레드(UI 등) 스레드풀(ThreadPool)
UI 앱에서 사용 시 UI로 돌아감 UI로 돌아가지 않음
데드락 위험 있음 줄어듦
효율성 낮음 높음

 

✅ 예제

❌ 데드락 유발 가능

string data = client.GetStringAsync(url).Result; // UI 스레드 블로킹

✅ 안전하게 비동기 실행

string data = await client.GetStringAsync(url).ConfigureAwait(false); // 백그라운드에서 이어서 실행

 

사용 권장 시점

상황사용 권장 여부
UI 앱 (WPF, WinForms) 가능하면 ConfigureAwait(false)
ASP.NET Core 무조건 ConfigureAwait(false)
라이브러리 코드 무조건 ConfigureAwait(false)
UI 조작 직후 false 쓰면 안 됨 (UI 업데이트 못 함)

 

 

✅ 주의할 점

var html = await client.GetStringAsync(url).ConfigureAwait(false);
label.Text = html;  // ❌ UI 스레드가 아니라 예외 발생

→ UI를 조작할 땐 false 쓰면 안 돼.

 

 

 

Task.Factory의 StartNew 메서드는 Task를 반환하는데, Task<TResult> 반환 용도로 StartNew<TResult> 메서드도 함께 제공된다.

Task<int> taskReturn = Task.Factory.StartNew<int>(() => 1);
taskReturn.Wait();
Console.WriteLine(taskReturn.Result);

 

 

C# 컴파일러는 await 예약어의 대상을 Task, Task<TResult> 타입을 반환하는 메서드로 제한하는데 이미 이러한 타입은 비동기 처리를 위한 내부적인 준비가 돼있기 때문에 c# 5.0 컴파일러가 단순히 await에 따른 코드를 Task에 맡김으로써 비동기 기능 구현에 대한 부담을 덜을 수 있었다.

더보기

핵심 문장 해석 #1

C# 컴파일러는 await 키워드를 사용할 때,
반드시 Task 또는 Task<T>를 반환하는 메서드만 대상으로 삼는다.


✅ 왜 그럴까?

✔️ Task, Task<T>는 비동기 처리를 위한 특별한 타입이기 때문이야.

  • Task는 비동기 작업의 상태, 완료 여부, 예외 정보 등을 다룰 수 있도록 설계돼 있어.
  • 그래서 컴파일러 입장에서는 비동기 흐름을 쉽게 이어붙일 수 있는 구조임.

✅ 컴파일러가 뭘 덜게 되었냐?

C# 5.0부터 컴파일러는 await가 붙은 코드를 일일이 분석하지 않아도 됨.

왜냐하면:

  • Task에 모든 비동기 정보가 포장돼 있으므로
  • 컴파일러는 그냥:
    "얘한테 맡기면 알아서 잘 끝나면 알려주겠군"
    → 이런 식으로 Task에 await 처리 책임을 넘긴다.

즉,

await는 컴파일러가 아니라 Task 객체의 내부 메커니즘에 의해 비동기 흐름을 제어하게 만든다.

C#의 Task 또는 Task<T> 객체는 비동기 작업의 상태, 결과, 예외를 모두 포함하고 있어.
그럼 그 중에서 **"상태"는 어떻게 관리되고 확인하는가?**를 중점으로 설명할게.


✅ 비동기 상태란?

비동기 작업이 현재 어떤 단계에 있는지를 나타내는 것
예: 대기 중(Running), 완료됨(RanToCompletion), 취소됨(Canceled), 실패(예외 발생, Faulted)

Task 객체에서 상태 확인 방법

C#의 System.Threading.Tasks.Task에는 다음과 같은 속성들이 있어:

속성설명

 

속성 설명
IsCompleted 작업이 끝났는지 여부 (성공, 실패, 취소 포함)
IsCompletedSuccessfully 성공적으로 끝났는지
IsFaulted 예외가 발생했는지
IsCanceled 취소됐는지
Status 전체 상태를 나타냄 (열거형: TaskStatus)
Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
    // throw new Exception("error!"); // 에러 발생 테스트
});

Console.WriteLine(task.Status);  // Running
task.Wait();                     // 완료될 때까지 기다림
Console.WriteLine(task.Status);  // RanToCompletion 또는 Faulted

if (task.IsCompletedSuccessfully)
    Console.WriteLine("성공!");
else if (task.IsFaulted)
    Console.WriteLine("실패: " + task.Exception?.Message);




✅ TaskStatus 열거형 (상태 값 종류)

public enum TaskStatus
{
    Created,         // 아직 시작되지 않음
    WaitingForActivation,
    WaitingToRun,
    Running,
    WaitingForChildrenToComplete,
    RanToCompletion, // 성공적으로 끝남
    Canceled,
    Faulted          // 예외 발생
}


✅ 결과는 .Result 또는 await로 받는다

Task<int> t = Task.Run(() => 10);
int result = t.Result;         // 동기 방식 (주의: 블로킹됨)
int result2 = await t;         // 비동기 방식 (추천)

 

✅ 예외 정보 확인

if (t.IsFaulted)
{
    Console.WriteLine(t.Exception); // AggregateException
}

Task.Exception은 항상 AggregateException이므로 .InnerException 접근해야 할 수도 있음

✅ 요약


속성 의미
Status 현재 Task의 상태 (Running, Faulted, 등)
IsCompleted 완료 여부 (성공, 실패, 취소 상관 없음)
IsCompletedSuccessfully 성공적으로 완료되었는지
IsFaulted 실패 여부 (예외 발생)
IsCanceled 취소 여부
Result 반환값 (성공했을 때만 접근 가능)

그래서 비동기 작업 시 async await은 task랑 같이 사용하면 비동기 상태/예외/결과를 처리 및 알려줌으로 좋다!.

 

더보기

핵심 문장 해석 #2

C# 컴파일러는 await가 붙은 메서드가 반드시 Task나 Task<TResult>를 반환하도록 제한한다.
왜냐하면 이 타입들은 **비동기 처리에 필요한 내부 구조(비동기 실행, 완료 추적, 예외 처리 등)**가 이미 갖춰져 있기 때문이다.


✅ 비유로 풀어보기

  • await은 비동기 작업이 끝날 때까지 기다렸다가 나머지 코드를 **"자동으로 분리 실행"**해주는 예약어다.
  • 근데 C# 컴파일러는 직접 쓰레드를 관리하거나 타이밍을 조정하지 않는다.
  • 대신 비동기 로직 처리는 Task에 맡긴다.
  • 왜? Task는 이미:
    • 언제 끝났는지 추적할 수 있고
    • 작업이 끝났을 때 후속 동작을 등록할 수 있고
    • 예외나 결과도 저장할 수 있기 때문

즉, Task는 비동기 작업을 표현하고 관리하는 데 최적화된 타입이고,
C# 컴파일러는 **"그냥 Task한테 맡기자"**는 철학을 따르는 것이다.

 

async Task<int> GetDataAsync()
{
    await Task.Delay(1000);  // 내부적으로 Task가 처리함
    return 42;
}
  • 컴파일러는 await Task.Delay(...)에서
    1. Delay()가 Task를 반환하니까
    2. "이 작업이 끝나면 어떻게 할지"를 Task에게 등록만 해두고
    3. 컨트롤을 반환한다

"비동기 처리의 복잡한 로직은 전부 Task가 알아서 함"


✅ 왜 ValueTask나 CustomAwaitable은 나중에 추가됐나?

  • 초창기 C# 5.0에서는 await는 오직 Task, Task<T>만 대상으로 작동
  • 나중에 C# 7 이후부터는 ValueTask, 커스텀 await 타입도 지원되지만,
  • 여전히 Task 기반이 가장 보편적이고 안정적이다

✅ 결론

  • await은 비동기 작업을 쉽게 쓰게 해주는 예약어지만,
  • 비동기 처리는 Task가 실질적으로 맡는다
  • 그래서 C# 컴파일러도 Task가 있는 메서드만 await 대상으로 인정하고,
  • 덕분에 컴파일러는 복잡한 비동기 제어 로직을 구현할 필요 없이 Task에 위임할 수 있었다

 

10.2.3 async 예약어가 적용된 메서드의 반환 타입 

await과 함께 사용될 메서드는 반드시 Task, Task<T>를 반환하는 것만 가능하다.

이와 함께 async 예약어가 지정되는 메서드에도 void와 Task, Task<T>만 반환할 수 있다는 제약이 있다. 

* C# 7.0에서 추가로 사용자 정의 Task 타입을 구현해 반환 가능함,

 

async void 유형의 경우는 해당 메서드내에서 예외 발생 시 그것이 처리되지 않은 경우 프로세스가 비정상적으로 종료되므로 권장되지 않는다.

그럼에도 ms에서 async void를 허용할 수 밖에 없었던 이유는 System.Windows.Forms.dll을 사용한 윈폼 프로그램에서 이벤트 처리기의 델리게이트로 사용하는 EventHandler 타입이 다음과 같이 정의되었기 떄문이다.

// 윈폼에서 사용하는 EventHandler 델리게이트의 정의
public delegate void EventHandler(object sender, EventArgs e);

 

예를 들어, 윈도우가 로드됐다는 이벤트를 윈폼 응용프로그램에서는 다음과 같이 이벤트 처리기를 정의해 사용한다.

namespace WinFormsApp1
{
    public partial class Form1: Form
    {
        public Form1()
        {
            InitializeComponent();
            this.Load += new System.EventHandler(this.Form1_Load);
        }

        private void Form1_Load(object? sender, EventArgs e)
        {
        }
    }
}

 

Form1_Load 메서드 내에서 await 호출을 하려면 async 예약어를 추가해야 하는데, 이미 정해진 EventHandler 델리게이트의 형식으로 인해 async Task, async Task<T> 는 불가능하고 어쩔 수 없이 하위 호환성을 지키고자 async void로 만들 수 밖에 없었다.

 

따라서 이벤트 처리기를 제외하곤 async void는 가능한 사용을 하지 말고, async Task, async Task<T>를 사용하자.

 

10.2.1절, 10.2.2절의 예제에 있던 async 메서드는 다음과 같이 Task를 반환하는 유형으로 바꾸면 좋다.

private static async Task AwaitRead() { .... 생략 ....}
private static async Task AwaitDownloadString() { ....생략....}
private static async Task ProcessTcpClient(TcpClient client) { ....생략....}

 

10.2.4 Async 메서드가 아닌 경우의 비동기 처리

C#의 await 예약어가 Task, Task<TResult> 타입을 반환하는 메서드를 대상으로 비동기 처리를 자동화했다는 점은 또 다른 활용 사례를 낳는다.

Async 처리가 적용되지 않은 메서드에 대해 Task를 반환하는 부가 메서드를 만드는 것으로 await 비동기 처리를 할 수 있는 것이다.

예를 들어, File.ReadAllText 메서드는 그에 대응되는 비동기 버전의 메서드를 제공하지 않는다.

string text = File.ReadAllText(@"C:\windows\system32\drivers\etc\HOSTS");
Console.WriteLine(text);

이 작업을 비동기로 처리하려면, 별도의 스레드를 이용하거나 델리게이트의 BeginInvoke로 처리해야했다. 그 결과 예제 5.10처럼 코드가 복잡해진다.

 

예제 10.5 ReadAllText 메서드를 비동기로 처리

class Program
{
    public delegate string ReadAllTextDelegate(string path);

    static void Main(string[] args)
    {
        string filePath = @"C:\windows\system32\drivers\etc\HOSTS";

        // 델리게이트를 이용한 비동기 처리
        ReadAllTextDelegate func = File.ReadAllText;
        func.BeginInvoke(filePath, actionCompleted, func);

        Console.ReadLine(); // 비동기 스레드가 완료될 때까지 대기하는 용도
    }

    private static void actionCompleted(IAsyncResult ar)
    {
        ReadAllTextDelegate func = ar.AsyncState as ReadAllTextDelegate;
        string fileText = func.EndInvoke(ar);

        //파일의 내용을 화면에 출력
        Console.WriteLine(fileText);
    }
}

##다만 상기 코드는 예외 발생( NET Core 또는 .NET 5 이상에서 델리게이트의 BeginInvoke / EndInvoke가 더 이상 지원되지 않기 때문에 발생)

 

위 코드를 Task<TResult>로 바꾸면 await을 이용해 쉽게 비동기 호출을 적용할 수 있다. 이를 위해 ReadAllText기능을 감싸는 비동기 버전의 메서드를 하나 더 만들기만 하면 된다.

 

static Task<string> ReadAllTextAsync(string filePath)
{
    return Task.Factory.StartNew(() =>
    {
        return File.ReadAllText(filePath);
    });
}

 

ReadAllText 메서드가 string을 반환하기에 Task<string>이 사용됐다.

이제 await을 적용해 비동기 호출을 간단히 끝내보자.

static async Task AwaitFileRead(string filePath)
{
    string fileText = await ReadAllTextAsync(filePath);
    Console.WriteLine(fileText);

    //Task 반환 타입을 갖는 메서드이나 async 예약어가 지정됐으므로
    //C# 컴파일러가 적절하게 코드를 자동으로 변환해주기에 return 문이 필요 없다.
}
static Task<string> ReadAllTextAsync(string filePath)
{
    return Task.Factory.StartNew(() =>
    {
        return File.ReadAllText(filePath);
    });
}

이 방법을 사용하면 닷넷 BCL에 Async 메서드로 제공되지 않았던 모든 동기 방식의 메서드를 비동기로 변환 가능하다.

닷넷 BCL 뿐만 아니라 사용자 정의 메서드도 비동기 코드로 적용 가능하다.

 

10.2.5 비동기 호출의 병렬 처리

병렬로 비동기 호출을 하는 것은 await과 Task의 조합으로 할 수 있다.

 

예제 10.6 5초 + 3초 = 8초가 걸리는 작업

int result3 = Method3();
int result5 = Method5();

Console.WriteLine(result3 + result5);
int Method3()
{
    Thread.Sleep(3000); // 3초가 걸리는 작업을 대신한  Sleep 처리
    return 3;
}
int Method5()
{
    Thread.Sleep(5000);
    return 5;
}

 

상기 코드 실행 시 8초의 시간이 걸려 작업이 완료된다.

Method3과 Method5를 병렬 수행 하면 5초만에 작업을 끝낼 수 있으며, 기존의 작업을 Thread를 이용해 처리 가능하다.

Dictionary<string, int> dict = new();

Thread t3 = new Thread((result) =>
{
    Thread.Sleep(3000);
    dict.Add("t3Result", 3);
});


Thread t5 = new Thread((result) =>
{
    Thread.Sleep(5000);
    dict.Add("t5Result", 5);
});

t3.Start(dict);
t5.Start(dict);

t3.Join(); // 3초 작업이 완료되기를 대기
t5.Join(); // 5초 작업이 완료되기를 대기

//약 5초 후에 모든 결괏값을 얻어 처리 가능
Console.WriteLine(dict["t3Result"] + dict["t5Result"]);

#출력
8

 

위 병렬처리 작업을 Task<TResult> 타입으로도 구현가능.

 

예제 10.7 2개의 작업을 병렬로 처리하지만 모든 작업이 완료될 때까지 대기

//Task를 이용한 병렬 처리;

var task3 = Method3Async();
var task5 = Method5Async();


//task3 작업과 task5 작업이 완료될 때까지 현재 스레드를 대기
Task.WaitAll(task3, task5);
Console.WriteLine(task3.Result + task5.Result);

Task<int> Method3Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(3000);
        return 3;
    });
}

Task<int> Method5Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(5000);
        return 5;
    });
}

#출력
8

 

Task.WaitAll은 인자로 들어온 모든 Task의 작업이 완료될 때 까지 대기.

Task.WaitAll은 동기 호출로 스레드의 실행을 막게된다. 그렇다면 WaitAll 조차도 비동기 호출로 처리하고자 한다면? Task<TResult>와 await을 조합하면 해결된다.

 

예제 10.8| 2개의 작업을 병렬로 비동기 호출

//async, await 와 Task를 이용한 비동기 병렬 처리;
DoAsyncTask();
Console.ReadLine();
async Task DoAsyncTask()
{
    var task3 = Method3Async();
    var task5 = Method5Async();

    await Task.WhenAll(task3, task5);

    Console.WriteLine(task3.Result + task5.Result);
}

Task<int> Method3Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(3000);
        return 3;
    });
}

Task<int> Method5Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(5000);
        return 5;
    });
}

 

예제 10.7과 예제 10.8의 차이는 전자는 병렬처리만 하며 후자는 병렬과 비동기 모두 진행된다. 즉 예제 10.7에선 2개의 작업을 실행 한 다음 결과를 받기 위해 현재 Main 문에서 실행 중인 스레드가 아무 일도 못하고 작업이 완료되는 순간까지 대기한다. 반면 10.8은 Task.WhenAll과 await의 조합으로 Main, DoAsyncTask 메서드를 수행하는 스레드가 task3, task5 작업이 완료될 때까지 대기하지 않고 곧바로 작업을 계속 수행한다.  물론, await 이후에 나온 Console.WriteLine 코드는 C# 컴파일러에 의해 task3과 task5가 완료된 시점에 비동기로 실행되도록 변경된다. 

 

await을 제거하면 10.7과 동일한 효과(병렬처리만 진행)나타난다.(입력이 동시에 안됨)

//async, await 와 Task를 이용한 비동기 병렬 처리;
DoAsyncTask();
Console.ReadLine();
async Task DoAsyncTask()
{
    var task3 = Method3Async();
    var task5 = Method5Async();

     Task.WhenAll(task3, task5);

    Console.WriteLine(task3.Result + task5.Result);
}

Task<int> Method3Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(3000);
        return 3;
    });
}

Task<int> Method5Async()
{
    return Task.Factory.StartNew(() =>
    {
        Thread.Sleep(5000);
        return 5;
    });
}

 

 

 

+ Recent posts