비동기 프로그래밍 Task에 대한 메모

나는 C#에는 비동기 프로그래밍을 할시 Thread 보다 Task를 애용하는데 2가지 이유가 있다

1. ThreadPool로 Thread를 가져와 최적,최소의 Thread를 이용하게 만든다.
2. 리턴값, 끝나는 시점, 델리게이트 등등 Thread로부터 없는 기능들을 사용할 수 있다.

Thread는 상대적으로 로우-레벨 이기에 
멀티쓰레딩에 대해 세밀한 조정이 필요한 경우 쓰자


일단 기본적인 예시를 보자.
 
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
using System.Diagnostics;
 
Stopwatch sw = new Stopwatch();
sw.Start();
string water = BoilWater();
string expressed = ExpressCoffee();
 
 
Console.WriteLine($"pouring {water} and {expressed} into cup");
Console.WriteLine("Coffee Done");
Console.WriteLine($" Time : {sw.ElapsedMilliseconds} ms");
 
sw.Reset();
 
 
 
string BoilWater()
{
    Console.WriteLine("Boiling Water...");
    Task.Delay(1000).GetAwaiter().GetResult();
    Console.WriteLine("Finished Boiling Water");
    return "Boiled water";
}
 
string ExpressCoffee()
{
    Console.WriteLine("Expressing Bean");
    Task.Delay(1000).GetAwaiter().GetResult();
    Console.WriteLine("Finished Expressing Bean");
    return "Coffee";
}
 
cs





물 끓이는데 1초, 커피콩 가는데 1초 걸렸으니 딱 2초다.

이제 비동기로 해보자


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
using System.Diagnostics;
 
Stopwatch sw = new Stopwatch();
sw.Start();
var boilTask = BoilWaterAsync();
var expressTask = ExpressCoffeeAsync();
 
string water = await boilTask;
string expressed = await expressTask;
Console.WriteLine($"pouring {water} and {expressed} into cup");
Console.WriteLine("Coffee Done");
Console.WriteLine($" Time : {sw.ElapsedMilliseconds} ms");
 
sw.Reset();
 
async Task<string> BoilWaterAsync()
{
    Console.WriteLine("Boiling Water...");
    await Task.Delay(1000);
    Console.WriteLine("Finished Boiling Water");
    return "Boiled water";
}
async Task<string> ExpressCoffeeAsync()
{
    Console.WriteLine("Expressing Bean");
    await Task.Delay(1000);
    Console.WriteLine("Finished Expressing Bean");
    return "Expresseo";
}
 
cs


이렇게 비동기적으로 처리하니 시간이 감축됐다.
이제 키워드들을 알아보자

async은 해당 메서드가 await를 가지고 있음을 알려주는 역할을 한다.'
await 없어도 정상적으로 작동하나 컴파일러가 경고를 울린다. 

await가 핵심인데,
비동기 작업을 제어하는 지시어로 순서를 정해줄 수 있다.
UI 쓰레드가 정지되지 않고 메시지 루프가 계속 돌 수 있게 하는 코드를
자동적으로 추가해준다는 차이가 있다.
이게 뭔뜻이냐면 마우스 클릭이나 키보드 입력 등과 같은 윈도우 메시지들을 계속 처리할 수 있다는 것을 의미한다.
출처:
https://www.csharpstudy.com/CSharp/CSharp-async-await.aspx


Task.Wait() 와 await의 차이점
출처:
https://cypsw.tistory.com/entry/C-Task-Wait-vs-await-%EC%B0%A8%EC%9D%B4%EC%A0%90


Task.WhenAll 과 Task.WaitAll는 내부적으로 같은 일을 한다.
차이점은 
WhenAll은 계속해서 실행되나 WaitAll은 현재 쓰레드를 멈춘다.
예외처리시 WhenAll은 첫번째 오류만 반환한다.

또 주의할 점으로 다음 코드를 보자
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
47
48
49
50
51
52
53
54
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Threading;
using System.Threading.Tasks;
 
public class NewBehaviourScript : MonoBehaviour
{
 
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            StartTask();
    }
 
    async void StartTask()
    {
        UnityEngine.Debug.Log("Start");
 
        Task t1 = Task.Factory.StartNew(() => Work1(3000));
        Task t2 = Task.Factory.StartNew(() => Work2(1000));

        List<Task> tt = new List<Task> { t1, t2 };
        await Task.WhenAll(tt);
 
 
        Debug.Log("End");
    }
 
    void Work1(int delayTime)
    {
        Debug.Log("Start Work 1");
        Thread.Sleep(delayTime);
        Debug.Log("Work 1 Done");
    }
 
    void Work2(int delayTime)
    {
        Debug.Log("Start Work 2");
// Thread.Sleep
Task.Delay(delayTime).Wait();
        Debug.Log("Work 2 Done");
    }
}
cs

해당 코드는 결과는 어떻게 나올까? 만약
Start -> StartWork1-> StartWork2 -> Work 2 Done -> Work 1 Done -> End
라고 생각했다면 틀렸다. 실제로는 이렇다.
Start -> StartWork2 -> StartWork1 -> Work 2 Done -> End -> Work 1 Done
왜 StartWork2가 먼저 시작되는가? 왜 Task들이 전부 끝나지 않았는데 End가 실행되는가
등등 궁금증을 해결하기위해 일단 고쳐보자

일단 Task의 순서는 Task1 메소드에 Task2를 넣으면 해결된다. 하지만
Task내 시간이 오래걸려 Task1이 먼저 끝나면, Task2가 끝나지 않았는데도 End가 반환된다.
그러면 Task가 가장 오래걸리는 것에 옮기거나, 자식 Task 또한 기다리라고 하면 된다.
1
2
3
4
5
6
7
8
9
10
11
12
    void Work1(CancellationToken token,int delayTime)
    {
        Debug.Log("Start Work 1");
        Task t2 = Task.Factory.StartNew(() => Work2(3000));
        Task.Delay(delayTime).Wait();
        if (token.IsCancellationRequested)
        {
            Debug.Log("Task canceled");
            token.ThrowIfCancellationRequested();
        }
        Debug.Log("Work 1 Done");
    }
cs

하지만 실제로 이렇게 순서를 구하는 짓을 할 필요없다! 
필요가 있다면 코드에 버그가 있다는 뜻이다

그런 다음 궁금중으로, 왜 Task.Whenall는 왜 기다리지 못했나? Task.Delay().Wait()와 Thread.Sleep() 은 뭔가?

await Task.WhenAll는 async void 를 주목하자.
await은 return이다. void는 아무것도 반환하지 않는다. 이렇게되면?
동기화 체인 이 끊어져서 end가 반환된다! 그러면 수정할 방법은 무엇인가?
당연히 반환 할 것을 주면 된다. async void 를  async Task으로 바꾸면 예상한대로 된다!

Task.Delay().Wait()와 Thread.Sleep() 는
둘다 하는 일은 똑같으나 차이점은 Delay에서는 다른 쓰레드로부터 깨워준다.
Task.Delay().Wait()가 더 나은 이유가 cancellationToken을 사용 가능하기 때문이다.

CancellationToken는 작동 중인 Task를 취소시켜주는 token으로, 자세히 배워보자.

각 유닛마다 길찾기를 요청할 때
이동중인 목표물이 어디 갈 수 없는 곳으로 간다고 하자, 그러면 목표물을 찾기위해
찾을 필요도 없는 구역마저 찾는 불상사를 막기위해 해당 시간이 지나면
취소할 필요가 있다.


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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Threading;
using System.Threading.Tasks;
 
public class NewBehaviourScript : MonoBehaviour
{
    [SerializeField] int milliSecond = 777;
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            StartTask();
    }
 
    async void StartTask()
    {
        UnityEngine.Debug.Log("Start");
 
        CancellationTokenSource cs = new CancellationTokenSource();
// 1. 취소 소스 생성
        cs.CancelAfter(milliSecond);
// 2. 취소 예약
        Task t1 = Task.Factory.StartNew(() => PathfindingTask(cs.Token, 10001));
        Task t2 = Task.Factory.StartNew(() => PathfindingTask(cs.Token, 5002));
        Task t3 = Task.Factory.StartNew(() => PathfindingTask(cs.Token, 20003));
 
        List<Task> tt = new List<Task> { t1, t2, t3 };
  // 4. Task 내 취소 Exception 대응
        try
        {
 
            while (tt.Count > 0)
            {
                Task temp = await Task.WhenAny(tt);
                tt.Remove(temp);
            }
 
        }
        catch(Exception ex)
        {
 
        }
 
        Debug.Log("End");
    }
// start
// 3 Unit : Start pathifinding
// 2 Unit : Start pathifinding
// 1 Unit : Start pathifinding
// 2 Unit : Done pathfinding
// 1 Unit : Canceled Pathfinding
// 3 Unit : Canceled Pathfinding
// end
    void PathfindingTask(CancellationToken token, int delayTime,int num)
    {
        Debug.Log($" {num} Unit : Start pathfinding...");
        Task.Delay(delayTime).Wait();
// 3. token 내 cancel 확인
        if (token.IsCancellationRequested)
        {
// 3-2. cancel 예외처리 실행
            Debug.Log($" {num} Unit : Canceled Pathfinding");
            token.ThrowIfCancellationRequested();
        }
        Debug.Log($" {num} Unit : Done pathfinding");
 
    }
}
cs

canceltoken 매개변수는 아래 링크 참조

https://stackoverflow.com/questions/48312544/whats-the-benefit-of-passing-a-cancellationtoken-as-a-parameter-to-task-run






참고자료:

Microsoft programming Guide url
https://docs.microsoft.com/ko-kr/dotnet/csharp/programming-guide/concepts/async/

Task 7가지 생성방법
https://codingcoding.tistory.com/415

Task기반 비동기 패턴 url
https://docs.microsoft.com/ko-kr/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern

kukuta님의 비동기 프로그래밍 url
https://kukuta.tistory.com/363


댓글

이 블로그의 인기 게시물

2D 총게임 반동 표시

Simple Stupid Funnel 후기