방프리

24.01.07 Chapter3. 태스크 기반 비동기 프로그래밍 (Item 29) 본문

C#/More Effective C#

24.01.07 Chapter3. 태스크 기반 비동기 프로그래밍 (Item 29)

방프리 2024. 1. 7. 16:25

Item 29 : 동기, 비동기 메서드를 함께 사용해서는 안 된다.

 

비동기 메서드와 동기 메서드를 사용할 때 두 가지 규칙을 지켜야 한다.

1. 비동기 작업이 완료될 때까지 기다리는 동기 메서드를 만들지 말라.
2. 수행 시간이 오래 걸리는 CPU 중식 작업을 비동기로 수행하지 말라.

위 규칙을 지키지 않으면 교착상태 (DeadLock)에 빠지기 쉽다.

public static async Task<int> ComputeUsageAsync()
{
    try
    {
        var operand = await GetLeftOperandForIndex(19);
        var operand2 = await GetRightOperandForIndex(23);
        return operand + operand2;
    }
    catch (KeyNotFoundException e)
    {
        return 0;
    }
}

public static int ComputeUsage()
{
    try
    {
        var operand = GetLeftOperandForIndex(19).Result;
        var operand2 = GetLeftOperandForIndex(23).Result;
        return operand + operand2;
    }
    catch (AggregateException e)
    when (e.InnerExceptions.FirstOrDefault().GetType() 
        == typeof(KeyNotFoundException))
    {
        return 0;
    }
}

await 구문을 사용하는 로직이 가독성이 훨씬 올라갈 뿐더러, 예외에 대한 대처도 가능하다.
만약 Result 혹은 Task.Wait()에서 예외가 발생했을 땐 AggregateException 예외를 일단 던진 다음 프로그래머는
AggregateException 내부의 예외를 통해서 정확한 이슈를 파악해야 한다.

private static async Task SimulatedWorkAsync()
{
    await Task.Delay(1000);
}

public static void SyncOverAsyncDeadlock()
{
    var delayTask = SimulatedWorkAsync();
    delayTask.Wait();
}

위의 코드는 애플리케이션의 종류에 따라 교착상태에 빠질 수 있다. 각 애플리케이션마다 SynchronizationContext의
동작 방식이 다르기 때문이다.
(Console에서는 SynchronizationContext는 Thread Pool에서 여러 Thread를 가져오지만 GUI 또는 ASP.NET에서는
하나의 Thread만 가져올 수 있기 때문)

저자는 Main 메서드에서 대해서도 주의점을 이야기 했지만 C# 7.1부터는 Main()도 async를 지원하기 때문에
대략적으로만 알아두자

나는 이 항목이 기존 동기 메서드를 비동기 메서드로 변경하는 상황에 처했을 때 쉽게 해결하기 위한 임시 방편 코드를
넣는 걸 생각하는 개발자들에게 조언한다 생각했다.

private int GetUserSilver()
{
    var userCached = _cached.Key("user");
    return userCached.Monetary;
}

private int GetUserMonetaryByType(MonetaryType type)
{
    return type switch
    {
        case MonetaryType.Silver => GetUserSilver(),
        _ => 0,
    };
}

//위의 MonetaryType에서 한 타입의 경우 동시성 이슈 때문에 DB에서 Get 해야함
private int GetUserSilver()
{
    var userCached = _cached.Key("user");
    return userCached.Monetary;
}

private async Task<int> GroupMonetary()
{
    var query = "...";
    var dbConn = ...
    var result = await dbConn.GetResult();
    //...
    return result;
}

//이렇게 코딩하지 말자.
private int GetUserMonetaryByType(MonetaryType type)
{
    var result = 0;
    switch (type)
    {
        case MonetaryType.Silver:
            result = GetUserSilver();
            break;
        case MonetaryType.GroupCoin:
            result = GroupMonetary().Result;
            Task.Delay(1000); //Task가 언제 끝날지 모르지만 대략 이때 정도에는 끝나겠지라는 생각
            break;
        default:
            throw new Exception("Not Supported type");
            break;
     }

    return result;
}

 

Comments