순서

I. 동기 vs 비 동기 프로그램

II. 비 동기 (Async) 구현 방법
1. Task, async, await
2. MultiThread Async 구현
3. Task를 사용하지 않은 Async 함수 호출

III. 다양한 비 동기(Async) 구현 방식
1. 매 Step마다 await
2. 동시 다발적 호출 후 한 번에 await

 


I. 동기(Sync) vs 비 동기(Async) 프로그램

동기와 비 동기는 스레드의 관점에서 이해해야 한다.

동기(Sync) 프로그램
: 코드가 한 줄 한줄 순차적으로 진행되며 완료될 때까지 아무것도 할 수 없다.
    -> 한 개의 스레드가 사용되어 프로그램이 멈추게 된다.

비동기(Async) 프로그램
: 코드의 실행 부분을 새로운 그룹으로 떼어내기 때문에 다른 동작을 할 수 있다.
  -> 다중 스레드가 사용되어 프로그램이 유지된다.

[요리 예시]
동기 프로그램
: 물을 끓이는 동안 끓는 물만 쳐다보고 있어야 함. 아무것도 못하는 상황
비 동기 프로그램
: 물을 끓이는 동안 티비를 봐도 되고 쌀을 씻어도 된다.

 

비 동기 프로그램은 프론트엔드(UI) 개발 시 필수적으로 사용되는데
특정 버튼을 눌렀을 때 우리가 다른 동작을 할 수 있는 이상적인 상황을 만들어준다.

버튼을 눌렀다가(응답 없음)혹은 흔히 말하는 렉이 발현되었다면
메인 스레드의 진행이 막혔기 때문이다.

 

실제 UI 프로그램 예시

1. 단일 스레드(동기 프로그램)
    -> Start Cook을 누르고 Watch TV 등 다른 동작을 못함...
        요리하는 동안에 해당 과정 외에 진행 불가.

Sync Programming Using Thread.Sleep()

2. 멀티 스레드 (비동기 프로그램)
    -> Start Cook을 누르고 그동안 몇 번이고 Watch TV 가능
        즉 UI가 고장 나지 않는다.

Async Programming Using Task.Delay()

 


II. 비 동기 (Async) 구현 방법

 


1. Task, async, await

 

[System.Threading.Tasks.Task class]
:
비동기 작업을 모델링하는 객체이다. -> 멀티스레딩의 주체
: Task(void형) 및 Task<T>(generic)로 정의된다.
: async와 await은 결국 이 Task를 지원하기 위한 형식이다.

[한정자 async]
: 함수 앞에 붙는 한정자로 비 동기 메소드를 나타낸다
: 내부에서 await를 사용하기 위해서는 필수적으로 붙어야 한다.

<함수 반환형>
void : 이벤트 처리기 일 때
Task : 작업 수행하나, 아무 반환 값없는 async method
Task : 값을 반환하는 async method

<async 한정자가 붙을 때>
-> 비 동기 메소드이므로 일반 Type이 반환되어야 한다.
: $ public async void myFunc() {}
: $ public async Task myFunc(){}
: $ public async Task<T> myFunc() {}

<async가 안 붙을 때>
: $ public Task myFunc(){}
: $ public Task<T> myFunc(){}
-> 해당 경우에는 Task, Task<T>를 반환한다.
-> 비 동기 메소드가 아니므로 내부에서 await 사용 불가하다.


[지시자 await]
: await로 호출된 async method의 반환을 대기한다.
: 대기한다는 것은 멀티스레드로 돌지만, 메인 스레드의 코드 진행은 멈춘다는 것이다.
: 여러  method의 반환을 동시에 대기하기 위해 Task.WaitAny(Task[ ])도 사용할 수 있다.

WaitAny vs WhenAny

await WaitAny( Task [ ] ) : Task[ ] 의 모든 반환을 대기한다.
Task myTask = await WhenAny( Task [ ] ) : Task[ ] 의 모든 반환 대기 후,
myTask를 생성한다.

 


2. MultiThread Async 구현

 

아래 예시 코드는 다음 호출구조를 따른다.

1. StartCook버튼을 누른다.
2. startCook() 비 동기 함수 실행
3. 각 SettingTable()을 Task로 실행한다.

// 1. UI 버튼 눌렀을 때 첫 진입 지점
private void Btn_StartCook(object sender, RoutedEventArgs e) {
  // 버튼누르면 해당 비동기함수 실행
  startCook(); 
}

// 2. 비동기함수 startCook() 호출됨
// 안에서 await를 사용할거니 async Task로 정의함.
// Task : 반환형 void임

private async Task startCook() {
  System.Console.Write("startCook() Main Thread id : ");
  System.Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());
  CCook cook = new CCook();
  
  // 함수를 await로 실행 -> 함수 탈출 대기하겠다.
  CCook.SetTable table = await cook.SettingTable();
}

public class CCook {
  public class SetTable{/*생략*/}
  
여기가 비동기 중요한 부분
  public Task<SetTable> SettingTable() {
    // 3. Task.Run ( () => {구현부} ) 로 정의되지만
    // 내부에서 await를 사용하기에 async()로 정의함.
    return Task.Run(async () => {
      System.Console.Write("SettingTable() Thread id : ");
      System.Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString());

      await Task.Delay(2000);
      return new SetTable();
    });
  }
}

Console 출력
startCook() Main Thread id : 1
SettingTable() Thread id : 5

실제로 동작할 Task에서
$ return Task.Run( () => ); 을 사용한 점에 주목하자.
-> 이때는 함수의 정의가 $ public Task myAsyncFunction(); 이 된다. (async한정자가 빠진다.)

마지막 Console 출력에서 보이듯 Thread id가 다르다.

 

But! Task.Run()이 아닌 단순 async 함수 호출은 스레드 분리가 되지 않는다!!
-> Task class가 멀티스레딩의 주체이다!! ( 바로 아래에서 테스트해봄.)

 


 

3. ★Task를 사용하지 않은 async 함수 호출

처음에는 다음과 같은 생각을 했었다.

함수가 async로 한정되어 있을 때
1. 단순 call을 하면 자동으로 스레드 분리되며 비 동기 메소드 실행될 것이고
2. 이때 대기가 필요하면 await를 사용하고 아님 각자 돌고 그렇게 되겠지?

결론은
async로 정의된 함수 내에서 Task.Run()을 하지 않고 그냥 구현하게 되면

1. Main process 진행에 Block은 걸리지 않는다.
  -> 해당 함수 호출하고 바로 다음 줄 실행하면서 내려감
2. 그러나 스레드의 분리는 일어나지 않는다.

테스트해봤다.

이번엔 makeRice와 makeSoup를 만들었고
SettingTable과는 다르게 아래와 같이 구현했다.
(Task.Run을 사용하지 않음 + 반환 굳이 필요 없어서 Task<T> 사용 x)


Async라면 다음과 같이 출력이 이루어질 것이다.
스레드 출력 print는 빼고 보면

"MakeRice In" -> "MakeSoup In" -> "Main End" -> "MakeRice Delay End"

private async Task startCook() {
  CCook cook = new CCook();

  cook.MakeRice(); // 1.진입후 2초 delay
  cook.MakeSoup(); // 2.바로 같이 진입 후 2초 delay
  
  System.Console.WriteLine("Main End"); // 세번째 순서로 출력?
  
  string curThreadId = Thread.CurrentThread.ManagedThreadId.ToString();
  System.Console.WriteLine("startCook() Main Thread id : ", curThreadId);
}

public async Task MakeRice() {
  System.Console.Write("MakeRice In"); // 첫번째 순서로 출력?

  string curThreadId = Thread.CurrentThread.ManagedThreadId.ToString();
  System.Console.WriteLine("MakeRice Thread id : ", curThreadId);
  await Task.Delay(2000);
  System.Console.WriteLine("MakeRice Delay End"); // 네번째 순서로 출력?
}

 public async Task MakeSoup() {
  System.Console.WriteLine("MakeSoup In"); // 두번째 순서로 출력?

  string curThreadId = Thread.CurrentThread.ManagedThreadId.ToString();
  System.Console.WriteLine("MakeSoup Thread id : ", curThreadId);
  await Task.Delay(2000);
}

맞았다.

async로 정의된 함수는 순차적으로 실행되는 것이 아니라,
실행시켜 놓고 바로 기존 함수를 계속 진행한다.

 

그렇다면 async 함수니 자동으로 다른 스레드로 분리되지 않을까?

틀렷다. 모두 동일하다.



Task.Run( () => )의 구현과 다르게 Thread는 모두 공유되었다.
-> 이 말은 다른 async 함수에서 의도치 않게 Main Thread를 Block 시킬 가능성이 있다는 것,

 

<참고>
System.Threading. Thread. Sleep( ms ) 

VS
System.Threading. Tasks. Task. Delay( ms )

=> Sleep은 동기 메소드로 호출되어 메인 스레드를 잡고 대기하는 역할을 한다.
=> Delay는 비 동기 메소드로 호출되어 해당 스레드를 잡고 대기하는 역할을 한다.
    -> await Task.Delay(ms)로 await와 같이 사용된다.

 

따라서 대략적으로 총 4가지 구현방법이 가능할 것이다.


1. 모든 Step마다 await 하여 비 동기지만 하나의 flow로 진행
2. 우선 비 동기 함수를 동시에 다 호출하여 진행하기

위 1, 2 경우 모두 Thread 분리의 여부도 선택할 수 있겠다.

아래 III. 에서 1, 2 경우를 Thread 분리하여 구현해 보았다. 

 


III. 다양한 비 동기(Async) 구현 방식

 

CCook이라는 클래스는 아래와 같이 선언되어 있다.
(그냥 3초 Delay 걸어주는 함수들이라고 생각하자.)

비 동기 + Thread분리 Cook Class 선언 코드

public class CCook{
  public class Rice { }
  public class SetTable { }
  public class Soup { }

  public Task<SetTable> SettingTable() {
    Task.Run( async() => {
      await Task.Delay(3000);
      return new SetTable(); });
  }

  public async Task<Rice> MakeRice() {
    Task.Run( async() => {
      await Task.Delay(3000);
      return new Rice(); });
  }

  public async Task<Soup> MakeSoup() {
    Task.Run( async() => {
      await Task.Delay(3000);
      return new Soup(); });
  }
}

 


1. 모든 Step마다 await

 

Main 구현 코드

private async Task startCook() {
  Stopwatch stopwatch = new Stopwatch();
  stopwatch.Start();
  CCook cook = new CCook();
  TextBlock_Cook.AppendText("요리를 시작합니다\n");

// 여기서부터 await call
  CCook.SetTable table = await cook.SettingTable();
  TextBlock_Cook.AppendText(" 테이블 세팅 완료!\n");

  CCook.Rice rice = await cook.MakeRice();
  TextBlock_Cook.AppendText("밥 짓기  완료!\n");

  CCook.Soup soup = await cook.MakeSoup();
  TextBlock_Cook.AppendText("국 끓이기 완료!\n");

  stopwatch.Stop();

  TextBlock_Cook.AppendText($"소요시간: {stopwatch.ElapsedMilliseconds}\n");
}

모든 Call 별로 await 후 AppendText가 순차적으로 호출된다.

이러면 3초씩 아무것도 못하는 요리과정을
쳐다만 보며 완성하는 Sync (9초 걸림)와 다를 게 없어 보이지만

async Task 이므로 다른 동작을 할 수는 있다.(Multi Threading)
진짜 시간은 Sync와 다를게 없이 순차적으로 똑같이 9초 걸린다.

 


2. 동시다발적 호출 후 한 번에 await

 

Main 구현 코드

private async Task startCook() {
  Stopwatch stopwatch = new Stopwatch();
  stopwatch.Start();
  CCook cook = new CCook();
  TextBlock_Cook.AppendText("요리를 시작합니다\n");

// 1. Task만 먼저 쭉 실행
  Task <CCook.SetTable> SetTableTask = cook.SettingTable();
  Task <CCook.Rice> MakeRiceTask = cook.MakeRice();
  Task <CCook.Soup> MakeSoupTask = cook.MakeSoup();

// 2. 그다음에 Task 변수를 await로 받아온다.
  CCook.SetTable setTable = await SetTableTask;
  CCook.Rice makeRice = await MakeRiceTask;
  CCook.Soup makeSoup = await MakeSoupTask;

  stopwatch.Stop();

  TextBlock_Cook.AppendText($"소요시간: {stopwatch.ElapsedMilliseconds}\n");
}

아까 async 테스트와 같이
"동시다발적"
으로 각 Task가 실행된다.

 


전체코드 백업

(MainWindow.xaml.cs)

<hide/>
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace WPF_TestProj
{
  /// <summary>
  /// MainWindow.xaml에 대한 상호 작용 논리
  /// </summary>
  public partial class MainWindow : Window
  {
    public int TVCnt = 1;

    public MainWindow() {
      InitializeComponent();
    }

    private void Btn_StartCook(object sender, RoutedEventArgs e) {
      // 버튼 눌렀을 때 실행될 async Task startCook() 함수
      startCook();
    }

    private void Btn_WatchTV(object sender, RoutedEventArgs e) {
      TextBlock_WatchTV.Text = $"Watching TV {TVCnt}";
      TVCnt++;
    }

  private async Task startCook() {
      Stopwatch stopwatch = new Stopwatch();
      stopwatch.Start();
      CCook cook = new CCook();
      TextBlock_Cook.AppendText("요리를 시작합니다\n");

      // 동시 다발적 코드
      //Task < CCook.SetTable > SetTableTask = cook.SettingTable();
      //Task<CCook.Rice> MakeRiceTask = cook.MakeRice();
      //Task<CCook.Soup> MakeSoupTask = cook.MakeSoup();

      //CCook.SetTable setTable = await SetTableTask;
      //TextBlock_Cook.AppendText(" 테이블 세팅 완료!\n");
      //CCook.Rice makeRice = await MakeRiceTask;
      //TextBlock_Cook.AppendText("밥 짓기  완료!\n");
      //CCook.Soup makeSoup = await MakeSoupTask;
      //TextBlock_Cook.AppendText("국 끓이기 완료!\n");


      CCook.SetTable table = await cook.SettingTable();
      TextBlock_Cook.AppendText(" 테이블 세팅 완료!\n");

      CCook.Rice rice = await cook.MakeRice();
      TextBlock_Cook.AppendText("밥 짓기  완료!\n");

      CCook.Soup soup = await cook.MakeSoup();
      TextBlock_Cook.AppendText("국 끓이기 완료!\n");

      stopwatch.Stop();
      TextBlock_Cook.AppendText($"소요시간: {stopwatch.ElapsedMilliseconds}\n");
    }
  }

  public class CCook{
    public class Rice { }
    public class SetTable { }
    public class Soup { }

    public void SyncSettingTable() {
      Thread.Sleep(3000);
    }

    public async Task<SetTable> SettingTable() {
      await Task.Delay(3000);
      return new SetTable();
    }

    public async Task<Rice> MakeRice() {
      await Task.Delay(3000);
      return new Rice();
    }

    public async Task<Soup> MakeSoup() {
      await Task.Delay(3000);
      return new Soup();
    }
  }
}

(MainWindow.xaml)

<hide/>
<Window x:Class="WPF_TestProj.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:local="clr-namespace:WPF_TestProj"
  mc:Ignorable="d"
  Title="MainWindow" Height="450" Width="800">
  <Grid>
    <Button Content="Start Cook" HorizontalAlignment="Left" Margin="172,193,0,0" VerticalAlignment="Top" Width="99" Height="40"
      Click="Btn_StartCook"/>
    <Button Content="Watch TV" HorizontalAlignment="Left" Margin="172,70,0,0" VerticalAlignment="Top" Width="99" Height="40"
      Click="Btn_WatchTV"/>
    <TextBox HorizontalAlignment="Left" Height="40" Margin="315,70,0,0" TextWrapping="Wrap" x:Name="TextBlock_WatchTV" VerticalAlignment="Top" Width="190"/>
    <TextBox HorizontalAlignment="Left" TextWrapping="Wrap" x:Name="TextBlock_Cook" VerticalAlignment="Top" Margin="315,193,0,0" Height="180" Width="190"/>
  </Grid>
</Window>

+ Recent posts