[C#] Task, 비동기에 대해

2023. 8. 8. 21:26·Programming/C#
 

[C#] Task .Wait() vs await 차이점

Task.Wait과 await의 차이점 Stack Overflow에서 발견한 흥미로운 질문과 답변입니다. set 출처: https://stackoverflow.com/questions/9519414/whats-the-difference-between-task-start-wait-and-async-await kayuse88.github.io C# await - C# 프

cypsw.tistory.com

이전 포스팅의 예제와, 결론 자체는 틀리지 않았으나 몇몇 오해할만한 소지가 있다고 판단하여 보완 포스팅을 작성한다.

 

1. Async is not include Thread

'Async' 는 'Thread' 를 포함하는 개념이 아니다.

오해할 만한 소지가 있는 내용으로, 이전 예제만 보더라도

비동기가 자연스럽게 Thread와 함께 운용된다고 착각하기 쉽다.

 

Source Code 1

using System.Diagnostics;

namespace WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public async void test()
        {
            await Task.Delay(2000);

            Debug.WriteLine("test");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            test();

            Debug.WriteLine("test2");
        }
    }
}

Output 1

test2
test

 

위 코드를 보면 이전 포스팅과 개념적으로 다를게 없다는 사실을 알 수 있다.

test2 가 먼저 출력되고 분기된 Thread 에서 test 를 실행시키는거 아닌가요?? 맞지 않아요??

 

아니다.

 

이전 예제에서는 ThreadPool 을 통해 Thread 를 생성(Task.Factory.StartNew, Task.Run)하여 실행 했지만

여기선 Thread 를 생성하지 않았다.

간단한 예제로 Thread 가 없다는것을 증명 해 보자.

 

Source Code 2

using System.Diagnostics;

namespace WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public async void test()
        {
            await Task.Delay(10); // 주석처리 전, 후 테스트 확인

            for (int i = 0; i > -1000000; --i)
            {
                if (i % 1000 == 0)
                {
                    Debug.WriteLine(i);
                }
            }

            Debug.WriteLine("test");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            test();

            for (int i = 0; i < 1000000; ++i)
            {
                if (i % 1000 == 0)
                {
                    Debug.WriteLine(i);
                }
            }

            Debug.WriteLine("test2");
        }
    }
}

Output 2

0
1000
2000

...
...

test2

0
-1000
-2000

...
...

test

만약 Thread 를 통해 위 작업을 수행했다면 -1,000,000 ~ 1,000,000 까지 1000 단위 값이 랜덤하게 출력되며

어지러운 출력값을 볼 수 있었을 것이다.

하지만 위 비동기 함수에서는 명백히 '순차적으로' 실행되는 사실을 파악할 수 있다.

 

 

2. Task

 

Task 클래스 (System.Threading.Tasks)

비동기 작업을 나타냅니다.

learn.microsoft.com

MSDN 에서 말하는 Task 의 정의는 아래와 같다.

Task 클래스는 Task 값을 반환하지 않고 일반적으로 비동기적으로 실행되는 단일 작업을 나타냅니다. Task개체는 .NET Framework 4에 처음 도입된 작업 기반 비동기 패턴의 중앙 구성 요소 중 하나입니다. 개체에서 Task 수행하는 작업은 일반적으로 주 애플리케이션 스레드에서 동기적으로 실행되지 않고 스레드 풀 스레드에서 비동기적으로 실행되므로 , 및 IsFaulted 속성뿐만 IsCanceledIsCompleted아니라 속성을 사용하여 Status 작업의 상태를 확인할 수 있습니다. 가장 일반적으로 람다 식은 태스크가 수행할 작업을 지정하는 데 사용됩니다.

 

여기서 말하는 '일반적' 이라는 말에 주목하자.

'대부분의' 상황에서는 Main Thread 에서 동작하지 않고, 추가로 생성된 Thread 와 함께 사용된다는 의미이다.

이는 곧  '항상' 추가된 Thread 와 같이 사용된다는 의미는 아니다.

 

아래 MSDN 코드를 살펴보며 같이 Task 의 특성에 대해서 좀 더 살펴보자.

Source Code  -  Task

using System.Diagnostics;

namespace WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public void test()
        {
            Action<object> action = (object obj) =>
            {
                Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                Task.CurrentId, obj,
                Thread.CurrentThread.ManagedThreadId);
            };

            // Create a task but do not start it.
            Task t1 = new Task(action!, "alpha");

            // Construct a started task
            Task t2 = Task.Factory.StartNew(action!, "beta");
            // Block the main thread to demonstrate that t2 is executing
            t2.Wait();

            // Launch t1 
            t1.Start();
            Console.WriteLine("t1 has been launched. (Main Thread={0})",
                              Thread.CurrentThread.ManagedThreadId);
            // Wait for the task to finish.
            t1.Wait();

            // Construct a started task using Task.Run.
            String taskData = "delta";
            Task t3 = Task.Run(() => {
                Console.WriteLine("Task={0}, obj={1}, Thread={2}",
                                                         Task.CurrentId, taskData,
                                                          Thread.CurrentThread.ManagedThreadId);
            });
            // Wait for the task to finish.
            t3.Wait();

            // Construct an unstarted task
            Task t4 = new Task(action!, "gamma");
            // Run it synchronously
            t4.RunSynchronously();
            // Although the task was run synchronously, it is a good practice
            // to wait for it in the event exceptions were thrown by the task.
            t4.Wait();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            test();
        }
    }
}

Output  -  Task

Task=13, obj=beta, Thread=8
t1 has been launched. (Main Thread=1)
Task=14, obj=alpha, Thread=8
Task=15, obj=delta, Thread=8
Task=16, obj=gamma, Thread=1​

 

  • t1  -  Instance화 된 후 추후에 start 됨.
  • t2  -  Thread 내부에서 시작. (Task. Factory. StartNew())
  • t3  -  Thread 내부에서 시작. (Task.Run())
  • t4  -  RunSynchronously() 하여 주 Thread 에서 동기적으로 실행

 

여기서 좀 이상한 점은 t1, t2, t3가 동일한 Thread 에서 실행되었다는 점이다.

t1 은 최소한 UI Thread 에서 실행되어야 정상이지 않을까?

 

 

3. Task 의 Thread 재사용

위의 Task 의 정의를 다시 살펴보면 

Task 클래스는 Task 값을 반환하지 않고 일반적으로 비동기적으로 실행되는 단일 작업을 나타냅니다. Task개체는 .NET Framework 4에 처음 도입된 작업 기반 비동기 패턴의 중앙 구성 요소 중 하나입니다. 개체에서 Task 수행하는 작업은 일반적으로 주 애플리케이션 스레드에서 동기적으로 실행되지 않고 스레드 풀 스레드에서 비동기적으로 실행되므로 , 및 IsFaulted 속성뿐만 IsCanceledIsCompleted아니라 속성을 사용하여 Status 작업의 상태를 확인할 수 있습니다. 가장 일반적으로 람다 식은 태스크가 수행할 작업을 지정하는 데 사용됩니다.
 
 

ThreadPool 이라는 용어가 눈에 띄는데, 이를 통해 예측할 수 있듯 Task 는 Thread 를 '재사용' 한다.

굳이 재사용 하는 이유는 Thread 를 생성 및 해제 하는 것 보다 이미 생성된 Thread 를 재사용 하는것이 비용적으로 효율이 더 높기 때문이다.

때문에 RunSynchronously() 로 명시한 t4 만 Thread 1(Winform UI Thread) 에서 실행되고

t1 은 t2 를 실행하기 위해 생성된 Thread 를 재사용 하기에 thread 8 에서 동작 하는것이다.

 

추가로 첫번째 예제로 Thread 를 확인 해 본다면 모두 Thread 1 에서 실행됨을 확인할 수 있을 것이다.

Source Code - 3

using System.Diagnostics;

namespace WinFormsApp
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        public async void test()
        {
            await Task.Delay(10);

            for (int i = 0; i > -1000000; --i)
            {
                if (i % 1000 == 0)
                {
                    Debug.WriteLine(i);
                }
            }

            Debug.WriteLine($"test : {Thread.CurrentThread.ManagedThreadId}");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            test();

            for (int i = 0; i < 1000000; ++i)
            {
                if (i % 1000 == 0)
                {
                    Debug.WriteLine(i);
                }
            }

            Debug.WriteLine($"test2 : {Thread.CurrentThread.ManagedThreadId}");
        }
    }
}

Output - 3

0
1000
2000
...
...
test2 : 1

0
-1000
-2000
...
...
test : 1

모두 Thread 1 에서 실행됨을 확인할 수 있다.

 

 

 

4. 결론

 

What is the difference between task and thread?

In C# 4.0, we have Task in the System.Threading.Tasks namespace. What is the true difference between Thread and Task. I did some sample program(help taken from MSDN) for my own sake of learning wi...

stackoverflow.com

Jörg W Mittag 의 답변에서 살펴볼 수 있듯 Task 를 한 문장으로 정의내리는 것은 매우 어려운 일이다.

Task 는 '비동기' 작업을 달성하기 위한 하나의 도구일 뿐으로, Task<T> 는 T를 반환하겠다고 약속하는 Type 일 뿐이다.

위에서 본 Source - 1 에서는 비동기함수 내에서 Task.Delay(10) 을 마주한 뒤, 어짜피 10ms간 await 되어야 하니 현재 Thread 내의 작업 Queue 를 당겨 다음 작업을 수행한 뒤, Delay(10ms) 이 지났다면 test() 를 실행하는 것이다.

명심하자. Task 는 어디까지나 '약속' 일뿐, 정확히 10ms 뒤에 await 단으로 돌아와서 아래있는 코드의 처리를 시작한다고 '보장' 하지 않는다.

  • Async 라고 해서 Thread 를 반드시 포함하지는 않는다.
  • Task 는 일반적 으로 비동기 용도로 사용된다.
  • Task 는 ThreadPool 에서 실행되지만, 반드시 추가적인 'Thread' 를 생성하지 않는다. 가용 가능한 Thread 가 존재하면 해당 Thread 를 '재사용' 한다.
 

 

저작자표시 비영리 동일조건 (새창열림)

'Programming > C#' 카테고리의 다른 글

[C#] MVVM 의 문제점  (0) 2023.08.16
MAUI의 미래에 대해  (0) 2023.06.05
[C#] Forms.Timer vs Threading.Timer  (0) 2022.07.25
[C#] SharedMemory 사용법  (0) 2022.05.12
[C#] UDP Multicast 수신  (0) 2022.04.08
'Programming/C#' 카테고리의 다른 글
  • [C#] MVVM 의 문제점
  • MAUI의 미래에 대해
  • [C#] Forms.Timer vs Threading.Timer
  • [C#] SharedMemory 사용법
Cyp
Cyp
  • Cyp
    Cyp Software Blog
    Cyp
  • 전체
    오늘
    어제
    • Cyp Blog (164)
      • Artificial Intelligence (41)
        • Article (21)
        • Post (2)
        • Basic (15)
        • Preferences (3)
      • Cyber Security (1)
      • Programming (46)
        • C++ (21)
        • C# (19)
        • Python (2)
        • Rust (0)
        • Java (1)
      • Algorithm (17)
        • BACKJOON (15)
      • Operating System (14)
        • WSL (2)
        • Windows (1)
        • Linux (5)
        • Security (3)
      • Tools (26)
        • Docker (3)
        • DataBase (2)
        • SSH (1)
        • Doxygen (2)
        • Etc (17)
      • Miscellaneous (19)
        • Book (2)
        • Hardware (2)
        • Hevel (1)
  • 블로그 메뉴

    • Home
    • Guest Book
  • 공지사항

    • 블로그 업데이트 노트
    • 블로그 운영방침
  • 인기 글

  • 태그

    Bom
    C4819
    utf-8 bom
    y-cruncher
    UTF-8 without BOM
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
Cyp
[C#] Task, 비동기에 대해
상단으로

티스토리툴바