.NET Framework 또는 Core 프로젝트에서 async
와 await
키워드를 이용하여 비동기 처리를 할 수 있다고 해서 몇 가지 실험을 해봤습니다. 😎
C++ 에서는 스레드풀에 일들(Jobs)을 넣어 백그라운드에서 작업을 진행 시켰는데, 단 두 개의 키워드로 이런 일들이 가능하다고 하니 신기하고 편해 보이기도 합니다. 👍
테스트#
💡 Information
C# 콘솔 프로젝트에서 진행했습니다.
Program.cs 파일만 만들어지는 기본 프로젝트
1. async 키워드를 붙이면?#
일단 async
키워드를 붙이면 내장된 스레드풀에서 돌아가는지 궁금했습니다.
코드는 아래와 같고, 예상 결과는 Main 0과 Main 1이 거의 동시에 출력되고 2초 후에 Async 가 출력될 것이라고 생각했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
async Task AsyncFunc()
{
Thread.Sleep(2000); // 2초짜리 작업
logger.Info("Async"); // 작업 완료 후 "Async" 출력
}
logger.Info("Main 0");
AsyncFunc(); // 비동기 실행함수, 바로 지나칠 것이라고 예상
logger.Info("Main 1");
while (Console.ReadKey().KeyChar != 'x') { } // async logger 대기 필요
logger.Dispose();
|
예상한 결과와 달랐습니다. async
키워드를 붙이더라도 마치 메인 스레드에서 실행되는 것 같았습니다.
여기서 Thread.Sleep()
함수가 아닌, Task.Delay()
함수도 있다는 것을 알았고 해당 함수만 바꿔서 진행해 봤습니다.
2. Task.Delay() 함수 사용#
예상 결과는 1번과 같아야 한다고 생각했습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
async Task AsyncFunc()
{
Task.Delay(2000); // Thread → Task 변경
logger.Info("Async");
}
logger.Info("Main 0");
AsyncFunc();
logger.Info("Main 1");
while (Console.ReadKey().KeyChar != 'x') { }
logger.Dispose();
|
이번에는 Task.Delay()
함수가 지나쳐버려서 모두 같은 시간대에 출력이 되었습니다. 그럼 여기에서 Wait()
를 추가하여 기다려보겠습니다.
3. Wait 추가#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
async Task AsyncFunc()
{
Task.Delay(2000).Wait(); // Thread → Task 변경
logger.Info("Async");
}
logger.Info("Main 0");
AsyncFunc();
logger.Info("Main 1");
while (Console.ReadKey().KeyChar != 'x') { }
logger.Dispose();
|
#테스트 1과 같은 결과가 나왔습니다. Task
로 반환할지라도 마치 메인 스레드에서 실행하는 것 처럼 AsyncFunc()
함수가 끝나자마자 두 로그가 출력이 되었습니다.
그럼 여기서 Wait()
를 뺀 후 await
키워드를 붙여보도록 하겠습니다.
4. await 추가#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
async Task AsyncFunc()
{
await Task.Delay(2000); // Wait() 제거 및 await 추가
logger.Info("Async");
}
logger.Info("Main 0");
AsyncFunc();
logger.Info("Main 1");
while (Console.ReadKey().KeyChar != 'x') { }
logger.Dispose();
|
드디어 1번에서 예상했던 결과와 똑같이 나왔습니다. 그렇다면 Task.Delay()
가 아닌, 새롭게 만든 Task 에서 Thread.Sleep()
를 했을 때 어떻게 나오는지 궁금해졌습니다.
5. Task.Run 생성 및 Thread.Sleep()#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
async Task AsyncFunc()
{
await Task.Run(() => { Thread.Sleep(2000); }); // Delay → Task 생성 및 Thread.Sleep()
logger.Info("Async");
}
logger.Info("Main 0");
AsyncFunc();
logger.Info("Main 1");
while (Console.ReadKey().KeyChar != 'x') { }
logger.Dispose();
|
역시 예상한 결과와 똑같이 나왔습니다. 그럼 마지막으로 Task.Run()
으로 생성한 것은 다른 스레드에서 작동하는 것인지 테스트를 해보겠습니다.
공용 변수를 이용하여 레이스 컨디션이 일어나는지 보면 될 것 같습니다. 😄
6. Task.Run 레이스 컨디션#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| using System.Text;
using nValue.Utility;
Logger logger = new Logger("main");
int sharedVariable = 0; // 공용 변수 선언
async Task AsyncFunc()
{
await Task.Run(() =>
{
for (int i = 0; i < 100000; i++) // 레이스 컨디션 유도 (Task.Run)
sharedVariable++;
});
logger.Info("Async");
}
AsyncFunc();
for (int i = 0; i < 100000; i++) // 레이스 컨디션 유도 (Main)
sharedVariable++;
logger.Info($"sharedVariable:{sharedVariable}");
while (Console.ReadKey().KeyChar != 'x') { }
logger.Dispose();
|
Task.Run
으로 실행하는 함수와 메인 함수에서 for
문을 돌렸을 때 200000 이라는 값이 나오지 않았으며 마치 서로 다른 스레드에서 레이스 컨디션이 일어나 동기화 되지 않는 모습을 보여줬습니다.
따라서 공용 변수를 사용할 때에는 동기화 처리(mutex
) 처리를 해야할 것 같습니다.
Task
는 .NET에서 만들어 놓은 스레드풀에서 작업을 실행해주는 클래스인 것 같으며 await
키워드는 해당 스레드(Task)의 결과를 기다릴 때 사용하는데 블록 상태로 만들지 않게 하는 키워드인 것 같습니다. C++ 에서 std::future
/std::promise
와 비슷한 것 같았습니다.
또한 Wait()
를 호출하면 호출한 스레드는 결과를 반환할 때 까지 블록에 걸리고, await
를 사용하면 블록에 걸리지 않는다는 점이 다르다는 것을 알았습니다. 👍
💡 Summary
async
키워드를 붙인다고 해서 다른 스레드에서 작동하는 것이 아니다.Task
를 반환하는 함수에서 await
키워드가 붙지 않으면 결과를 기다리지 않고 다음 행을 실행한다.Task
반환하는 함수에 Wait()
함수를 호출해도 어차피 메인 스레드에서 실행하는 것과 똑같은 결과를 부른다.async
와 await
키워드 모두 사용해야 백그라운드 작업이 이루어진다.