.NET Framework 또는 Core 프로젝트에서 asyncawait 키워드를 이용하여 비동기 처리를 할 수 있다고 해서 몇 가지 실험을 해봤습니다. 😎

C++ 에서는 스레드풀에 일들(Jobs)을 넣어 백그라운드에서 작업을 진행 시켰는데, 단 두 개의 키워드로 이런 일들이 가능하다고 하니 신기하고 편해 보이기도 합니다. 👍

테스트

💡 Information

C# 콘솔 프로젝트에서 진행했습니다. Program.cs 파일만 만들어지는 기본 프로젝트

1. async 키워드를 붙이면?

일단 async 키워드를 붙이면 내장된 스레드풀에서 돌아가는지 궁금했습니다.

코드는 아래와 같고, 예상 결과는 Main 0Main 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();

test1_result

예상한 결과와 달랐습니다. 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();

test2_result

이번에는 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();

test3_result

#테스트 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();

test4_result

드디어 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();

test5_result

역시 예상한 결과와 똑같이 나왔습니다. 그럼 마지막으로 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();

test6_result

Task.Run 으로 실행하는 함수와 메인 함수에서 for 문을 돌렸을 때 200000 이라는 값이 나오지 않았으며 마치 서로 다른 스레드에서 레이스 컨디션이 일어나 동기화 되지 않는 모습을 보여줬습니다.

따라서 공용 변수를 사용할 때에는 동기화 처리(mutex) 처리를 해야할 것 같습니다.

결론

Task 는 .NET에서 만들어 놓은 스레드풀에서 작업을 실행해주는 클래스인 것 같으며 await 키워드는 해당 스레드(Task)의 결과를 기다릴 때 사용하는데 블록 상태로 만들지 않게 하는 키워드인 것 같습니다. C++ 에서 std::future/std::promise 와 비슷한 것 같았습니다.

또한 Wait() 를 호출하면 호출한 스레드는 결과를 반환할 때 까지 블록에 걸리고, await를 사용하면 블록에 걸리지 않는다는 점이 다르다는 것을 알았습니다. 👍

💡 Summary

  1. async 키워드를 붙인다고 해서 다른 스레드에서 작동하는 것이 아니다.
  2. Task를 반환하는 함수에서 await 키워드가 붙지 않으면 결과를 기다리지 않고 다음 행을 실행한다.
  3. Task 반환하는 함수에 Wait() 함수를 호출해도 어차피 메인 스레드에서 실행하는 것과 똑같은 결과를 부른다.
  4. asyncawait 키워드 모두 사용해야 백그라운드 작업이 이루어진다.