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