Posts C# async Task
Post
Cancel

C# async Task

비동기 작업 : async-await


  • TaskThread처럼 사용하는 대신, 비동기 작업을 위해 사용할 수 있다.

  • async, await 키워드를 이용해 이루어지는 비동기 작업은 기존의 멀티 스레딩과는 다른 방식으로 이루어진다.

  • async-await 작업은 Parallel이 아닌, Concurrent하게 이루어진다. (중요)

  • 동일 스레드에서 여러 개의 작업을 동시 처리할 수 있다. (Concurrent 하므로)

  • 실행되는 스레드가 Sleep()하고 있다면 그 동안은 실행을 중단하고 대기한다.


특징

  • 메소드 내부에서 await로 대기하려면, 해당 메소드의 리턴 타입 앞에 async 키워드를 작성한다.

  • await Task.Delay(n)와 같이 await를 이용한 대기는 해당 스레드를 블록시키지 않는다.

  • 비동기 작업을 기다리고, 끝날 경우 값을 리턴받을 수 있다.

  • async-await로 실행 시킨 작업 역시 ThreadPool에 의해 관리된다.


작업이 실행되는 스레드 환경

WinForm, WPF, Unity Engine과 같이

메인 스레드에서 UI 작업이 이루어지는 프로그램은

await가 실행되기 전에 당시 실행되고 있는 스레드를 캡쳐하여

SynchronizationContext.Current에 저장한다.

그리고 await 이후의 문장들을 캡쳐된 스레드에서 실행시킨다.


그런데 콘솔, 윈도우 서비스 프로그램 같은 경우에는 SynchronizationContext.Currentnull로 둔다.

그리고 await 이후의 문장들을 ThreadPool에서 제공하는 스레드 내에서 실행시킨다.


정리하자면,

  • await 이전의 문장들은 호출 스레드에서 실행된다.

  • UI가 존재하는 프로그램에서는 await 이후 문장들을 호출 스레드에서 실행시키도록 보장한다.

  • 콘솔, 윈도우 서비스의 경우에는 await 이후 문장들을 ThreadPool에서 제공하는 스레드에서 실행시킨다.

  • 그런데 애초에 Task.Run() 또는 new Task().Start()로 실행시켰으면 모두 ThreadPool의 스레드에서 실행된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private async Task ContextExample()
{
    Console.WriteLine($"Thread ID-A : {Thread.CurrentThread.ManagedThreadId}");
    await Task.Delay(1);
    Console.WriteLine($"Thread ID-B: {Thread.CurrentThread.ManagedThreadId}");
}

public void MainMethod()
{
    ContextExample().Wait();
    // ID-A : 1 (메인 스레드)
    // ID-B : 스레드풀에서 제공

    Task.Run(ContextExample).Wait();
    // ID-A : 스레드풀에서 제공
    // ID-B : 스레드풀에서 제공
}


1. 대기할 수 없는 작업


  • async void 타입은 비동기로 시작되고, 의도적으로 대기할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private async void TaskAsync(int n)
{
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(500);
        Console.WriteLine($"Task : {i}");
    }
}

private void MainMethod()
{
    TaskAsync(10);
    Thread.Sleep(10_000);
}


2. 대기할 수 있는 작업


  • async Task 타입은 비동기로 시작되며 async void와 마찬가지로 작업의 결과를 받아올 수는 없다.

  • Task 타입을 리턴하며, 이를 통해 작업을 대기할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private async Task TaskAwaitable(int n)
{
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(100);
        Console.WriteLine($"Task : {i}");
    }
}

private void MainMethod()
{
    // 체인으로 대기
    TaskAwaitable(10).Wait();

    // 객체로 받아 대기
    Task t = TaskAwaitable(10);
    t.Wait();
}


3. 값을 리턴하는 작업


  • 값을 리턴하는 비동기 메소드는 Task<리턴타입> 꼴로 리턴 타입을 지정한다.

  • 비동기 메소드의 결과를 .Result와 같이 참조하려고 하는 경우, 결과를 얻을 때까지 대기하여 해당 지점에서 흐름이 일시 중단된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private async Task<int> TaskAsyncAndReturn()
{
    int sum = 0;
    for (int i = 0; i < 10; i++)
    {
        await Task.Delay(100);
        sum += i;
    }
    return sum;
}

private void MainMethod()
{
    Task<int> task = TaskAsyncAndReturn();
    Console.WriteLine(task.Result); // 결과를 얻을 때까지 대기
    Console.WriteLine("End");
}


  • .Wait() 메소드를 통해 의도적으로 결과를 기다리며 흐름을 중단시킬 수도 있다.
1
2
3
4
5
6
7
8
9
private void MainMethod()
{
    Task<int> task = TaskAsyncAndReturn();
    task.Wait(); // 결과를 얻을 때까지 대기

    // -> 결과를 .Result로 참조하는 대신, .Wait()를 통해 대기 가능

    Console.WriteLine("End");
}


  • .Wait() 이전에 다른 작업 또한 실행시켰다면, 해당 작업도 병렬적으로 수행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private async Task<int> TaskAsyncAndReturn(int n)
{
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(100);
        sum += i;
    }
    return sum;
}

// 동시에 여러 작업을 실행시키는 경우
private void MainMethod()
{
    Task<int> task1 = TaskAsyncAndReturn(10);  // 작업 1 시작
    Task<int> task2 = TaskAsyncAndReturn(20);  // 작업 2 시작

    // 작업1, 작업2 실행 중

    task1.Wait();                    // 작업 1 대기
    Console.WriteLine("End 1");

    // 작업 2 실행 중

    Console.WriteLine(task2.Result); // 작업 2 대기
    Console.WriteLine("End 2");
}

// 한 번에 하나의 작업씩 실행시키는 경우
private void MainMethod2()
{
    Task<int> task1 = TaskAsyncAndReturn(10);  // 작업 1 시작

    task1.Wait();                              // 작업 1 대기

    // 작업1 실행 중

    Console.WriteLine("End 1");

    Task<int> task2 = TaskAsyncAndReturn(20);  // 작업 2 시작

    // 작업 2 실행 중

    Console.WriteLine(task2.Result);           // 작업 2 대기
    Console.WriteLine("End 2");
}


4. 작업의 연계


완료 시 수행할 동작 등록하기

  • Task 수행 종료 시 동작을 .ContinueWith() 메소드를 통해 등록할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private async Task TaskAwaitable(int n)
{
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(100);
        Console.WriteLine($"Task : {i}");
    }
}

public void MainMethod()
{
    Task t = TaskAwaitable(5);
    t.ContinueWith(x => Console.WriteLine("END 1"));
    t.ContinueWith(x => Console.WriteLine("END 2"));
    t.ContinueWith(x => Console.WriteLine("END 3"));

    t.Wait();
}


  • .ContinueWith() 메소드는 Task 타입을 리턴하므로, 체인으로 이어갈 수도 있다.
1
2
3
4
5
6
7
8
public void MainMethod()
{
    TaskAwaitable(8)
        .ContinueWith(_ => Console.WriteLine("End 1"))
        .ContinueWith(_ => Console.WriteLine("End 2"))
        .ContinueWith(_ => Console.WriteLine("End 3"))
        .Wait();
}


완료 즉시 결과 전달하기

  • Task<TResult> 꼴로 리턴 값이 존재하는 경우, .ContinueWith()를 이용해 완료 시 결과를 비동기적으로 전달할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private async Task<int> TaskAsyncAndReturn(int n)
{
    int sum = 0;
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(100);
        sum += i;
    }
    return sum;
}

public void MainMethod()
{
    TaskAsyncAndReturn(10)
        .ContinueWith(x => Console.WriteLine($"Result : {x.Result}"))
        .Wait();
}


5. await를 통한 또 다른 작업의 대기


  • async로 선언된 메소드 내부에서 await를 통해 또다른 Task를 대기할 수 있다.
1
2
3
4
5
6
7
8
9
private async Task SomeTask()
{
    await Task.Delay(500);
}

private async Task TaskInTask()
{
    await SomeTask(); // 다른 async 메소드 실행 및 대기
}


  • 매개변수로 또다른 비동기 메소드를 전달받아 대기할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 매개변수로 전달받는 메소드의 리턴이 단순 Task인 경우
private async Task TaskInTask(Func<Task> insideTaskFunc)
{
    Console.WriteLine("Task Start - 1");
    await insideTaskFunc();
    Console.WriteLine("Task End - 1");
}

// 2. 매개변수로 전달받는 메소드의 리턴 값이 존재하는 경우
private async Task TaskInTask<T>(Func<Task<T>> insideTaskFunc)
{
    Console.WriteLine("Task Start - 2");
    T insideResult = await insideTaskFunc();
    Console.WriteLine($"Task End - 2 => Result : {insideResult}");

    // 이것도 가능
    //Console.WriteLine($"Result : {await insideTaskFunc()}");
}

public void MainMethod()
{
    Task t1 =
        TaskInTask(async () =>
        {
            Console.WriteLine("Inside Start - 1");
            await Task.Delay(1000);
            Console.WriteLine("Inside End - 1");
        });

    Task t2 =
        TaskInTask(async () =>
        {
            Console.WriteLine("Inside Start - 2");
            await Task.Delay(2000);
            Console.WriteLine("Inside End - 2");

            return 123;
        });

    Task.WaitAll(t1, t2);
}


6. 여러 Task를 묶어서 처리하기


[1] WaitAll

  • 인자로 전달한 모든 Task가 종료될 때까지 대기한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private async Task TaskAwaitable(int n)
{
    for (int i = 0; i < n; i++)
    {
        await Task.Delay(100);
        Console.WriteLine($"Task : {i}");
    }
}

public void MainMethod()
{
    Task t1 = TaskAwaitable(5);
    Task t2 = TaskAwaitable(10);

    Task.WaitAll(t1, t2);
}


[2] WaitAny

  • 인자로 전달한 Task 중 하나라도 종료될 때까지 대기한다.
1
2
3
4
5
6
7
public void MainMethod()
{
    Task t1 = TaskAwaitable(5);
    Task t2 = TaskAwaitable(10);

    Task.WaitAny(t1, t2);
}


[3] WhenAll

  • 인자로 전달한 Task들을 한데 묶어서 하나의 Task로 관리한다.

  • 상태, 예외를 종합하여 확인할 때 사용된다.

  • 등록된 모든 Task가 완료되면 WhenAll()이 리턴한 Task도 완료된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
public void MainMethod()
{
    Task t1 = TaskAwaitable(5);
    Task t2 = TaskAwaitable(10);

    Task tAll = Task.WhenAll(t1, t2);

    Console.WriteLine(tAll.Status); // Waiting For Activation

    tAll.Wait(); // 모두 종료될 때까지 대기

    Console.WriteLine(tAll.Status); // Ran To Completion
}


[4] WhenAny

  • 등록된 Task 중 하나라도 완료되면 WhenAll()이 리턴한 Task도 완료된다.
1
2
3
4
5
6
7
8
public void MainMethod()
{
    Task t1 = TaskAwaitable(5);
    Task t2 = TaskAwaitable(10);

    Task tAny = Task.WhenAny(t1, t2);
    tAny.Wait(); // 하나라도 종료될 때까지 대기
}


7. 완료 여부 추적하기 : TaskCompletionSource


어떤 작업을 대기할 API가 제공되지 않는 경우가 있다.

예를 들어

1
2
3
4
5
6
public async void SomeTask()
{
    Console.WriteLine("Task Begin");
    await Task.Delay(1000);
    Console.WriteLine("Task End");
}

이런 async void 메소드가 있을 때, 이 메소드는 그저 비동기적으로 실행만 할 수 있고 대기할 수는 없다.

이럴 때 매개변수로 TaskCompletionSource를 이용하여 완료 여부를 직접 설정하고 대기할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public async void SomeTask(TaskCompletionSource<bool> tcs)
{
    Console.WriteLine("Task Begin");
    await Task.Delay(1000);
    Console.WriteLine("Task End");

    // tcs의 Task를 완료 처리(RanToCompletion 상태로 전환)
    tcs.SetResult(true);
}

public void MainMethod()
{
    TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();

    SomeTask(tcs);

    tcs.Task.Wait();
}

위와 같이 TaskCompletionSource 객체를 미리 만들어서 메소드에 제공하고,

해당 비동기 메소드 내에서는 작업이 끝난 후 .SetResult() 메소드를 통해

작업 완료 처리를 할 수 있다.

그리고 이를 대기할 스레드 내에서는 해당 객체의 .Task를 참조하여 대기할 수 있다.


간단 정리


비동기 메소드의 형태

[1] 대기 불가능

1
2
3
4
private async void Method()
{
    await Something();
}

[2] 대기 가능

1
2
3
4
private async Task Method()
{
    await Something();
}

[3] 대기 가능 및 값 리턴

1
2
3
4
5
6
7
8
9
private async Task<int> Method()
{
    // (1) 차근차근
    int result = await Something();
    return result;

    // (2) 호출 + 대기 + 결과 받기 + 리턴
    return await Something();
}


대기

[1] 비동기 메소드에서 비동기 메소드 대기

1
2
3
4
5
6
7
8
9
10
private async Task Method()
{
    await Something();
}

private async void Main()
{
    Task t = Method();
    await t;
}

[2] 동기 메소드에서 비동기 메소드 대기

1
2
3
4
5
6
7
8
9
10
private async Task Method()
{
    await Something();
}

private void Main()
{
    Task t = Method();
    t.Wait();
}


API

Task.Delay(int)

  • async 메소드 내에서 호출하며, await 키워드를 통해 지정한 시간(ms)을 대기한다.


.Wait()

  • 실행 환경에서 비동기 작업을 대기한다.


Task.WaitAll(params[] Task)

  • 실행 환경에서 비동기 작업들이 모두 종료되기를 기다린다.


.ContinueWith(Action<Task>) .ContinueWith<T>(Func<Task, T>)

  • 해당 비동기 작업 종료 시 연계될 작업을 등록한다.


TaskCompletionSource<T>

  • 작업의 대기가 불가능한 경우, 대기를 위한 매개체가 된다.
  • 작업 내에서 tcs.SetResult(T)를 통해 완료 상태를 통지한다.
  • 대기할 환경에서 await tcs.Task를 통해 완료 여부를 추적하고 대기할 수 있다.


References


This post is licensed under CC BY 4.0 by the author.