C# async는 컴파일러에게 해당 메서드가 await를 가지고 있음을 알려주는 역활을 한다. async라고 표시된 메서드는 await를 1개 이상 가질 수 있는데, 하나도 없는 경우라도 컴파일은 가능하지만 Warning 메시지가 표시된다. async를 표시한다고 해서 자동으로 비동기 방식으로 프로그램을 수행하는 것은 아니고, 일종의 보조 역활을 하는 컴파일러 지시어로 볼 수 있다. async 메서드의 리턴 타입은 대부분의 경우 Task(리턴값이 있는 경우) 혹은 Task (리턴값이 없는 경우) 인데, 예를 들어 리턴값이 string일 경우 async Taskmethod() 와 같이 정의하고 return "문자열"과 같이 문자열만 리턴한다. C# 컴파일러는 return 문의 문자열을 자동으로 Task로 변환해 준다. 또 다른 async 메서드의 리턴 타입으로 void 타입이 있는데, 특히 이벤트핸들러를 위해 void 리턴을 허용하고 있다. 실제 핵심 키워드는 await인데, 이 await는 일반적으로 Task 혹은 Task객체와 함께 사용된다. Task 이외의 클래스도 사용 가능한데, awaitable 클래스, 즉 GetAwaiter() 라는 메서드를 갖는 클래스이면 함께 사용 가능하다. UI 프로그램에서 await는 Task와 같은 awaitable 클래스의 객체가 완료되기를 기다리는데, 여기서 중요한 점은 UI 쓰레드가 정지되지 않고 메시지 루프를 계속 돌 수 있도록 필요한 코드를 컴파일러가 await 키워드를 만나면 자동으로 추가한다는 점이다. 메시지 루프가 계속 돌게 만든다는 것은 마우스 클릭이나 키보드 입력 등과 같은 윈도우 메시지들을 계속 처리할 수 있다는 것을 의미한다. await는 해당 Task가 끝날 때까지 기다렸다가 완료 후, await 바로 다음 실행문부터 실행을 계속한다. await가 기다리는 Task 혹은 실행 메서드는 별도의 Worker Thread에서 돌 수도 있고, 또는 UI Thread에서 돌 수도 있다. 아래 예제는 버튼 클릭으로 Run()이라는 async 메서드를 실행하고, Run 메서드 안에서 비동기 Task를 만들어 실행하고 결과를 기다리는 await 문의 예를 보여주고 있다. await는 LongCalcAsync() 라는 메서드가 끝나기를 기다렸다가 끝나면 결과를 sum에 할당한 후 다음 문장들을 계속 실행한다. 특히 여기서 주목할 만한 것은 결과값을 Label 컨트롤에 뿌려줄 때, Invoke()나 BeginInvoke()를 쓸 필요가 없다는 점이다. Background Thread에서 비동기 Task가 끝난 후, await가 다시 Caller가 갖고 있던 쓰레드 즉 UI Thread로 다음 문장들을 실행하게 하기 때문이다. 예제// 예제1 private void button1_Click(object sender, EventArgs e) { Run(); //UI Thread에서 실행 } private async void Run() { // 비동기로 Worker Thread에서 도는 task1 // Task.Run(): .NET Framework 4.5+ var task1 = Task.Run(() => LongCalcAsync(10)); // task1이 끝나길 기다렸다가 끝나면 결과치를 sum에 할당 int sum = await task1; // UI Thread 에서 실행 // Control.Invoke 혹은 Control.BeginInvok 필요없음 this.label1.Text = "Sum = " + sum; this.button1.Enabled = true; } private int LongCalcAsync(int times) { int result = 0; for (int i = 0; i < times; i++) { result += i; Thread.Sleep(1000); } return result; } .NET 4.5 Async 혹은 TaskAsync 메서드들 C# 5.0과 함께 선보인 .NET 4.5는 기존의 동기화(Synchronous) 메서드들과 구분하여 C#의 await (혹은 VB의 Await)를 지원하기 위해 많은 Async 메서드들을 추가하였다. 이 새 메서드들은 기본적으로 기존의 Synchronous 메서드명 뒤에 Async를 붙여 명명되었는데, 만약 기존에 Async로 끝나는 메서드가 이미 있었던 경우에는 TaskAsync를 메서드명에 붙여 명명하였다. 예제System.IO.Stream.Read() : 기존 동기 메서드 System.IO.Stream.ReadAsync() : 4.5 Async 메서드 WebClient.DownloadStringAsync() : 기존 비동기 메서드 WebClient.DownloadStringTaskAsync() : 4.5 TaskAsync 메서드 [Advanced Topic] await가 기다리는 Task는 대부분의 경우 Background Worker Thread에서 실행된다. 하지만 await를 썼다고 해서 자동으로 그 Task(혹은 메서드)가 Worker Thread에서 도는 것은 아니다. 아래 예제는 await를 사용했지만, 해당 Task(LongCalc2)는 (별도로 Worker Thread를 생성하지 않고) UI 쓰레드에서 실행된다. 만약 Worker Thread를 생성하려면, Task.Run() 등과 메서드를 사용하여 비동기 작업을 지정할 수 있다. 예제// 예제3 private void button1_Click(object sender, EventArgs e) { Run(); //UI Thread에서 실행 } private async void Run() { int sum = await LongCalc2(10); this.label1.Text = "Sum = " + sum; this.button1.Enabled = true; } private async Task LongCalc2(int times) { //UI Thread에서 실행 Debug.WriteLine(Thread.CurrentThread.ManagedThreadId); int result = 0; for (int i = 0; i < times; i++) { result += i; await Task.Delay(1000); } return result; } await : Task.ContinueWith() [Advanced Topic] 앞에서 이야기 하였듯이 await는 해당 Task가 끝난 후 await 문장이 있었던 곳으로부터 계속 다음 문장들을 실행하도록 되어있다. 이러한 기능은 .NET 4.0에서 소개 되었던 Task클래스의 ContinueWith()를 써서 아래와 같이 구현될 수 있다. 물론 C# 5.0 컴파일러가 await를 이렇게 변경한다는 것은 아니지만, 개념적으로 동일한 방식이라 볼 수 있다. 아래 예제에서 ContinueWith() 메서드는 첫번째 파라미터에서 task1 이 끝난 후 실행될 명령들을 람다식으로 지정하고 있다. 그리고 두번째 파라미터에는 실행블럭이 현재 쓰레드 (예제의 경우 UI Thread)에서 실행하도록 TaskScheduler.FromCurrentSynchronizationContext()를 지정하고 있다. 즉, 개념적으로 await는 특정 Task가 실행된 후 이러 이러한 실행블럭을 현재 실행 (쓰레드) 컨텍스트에서 실행하도록 하는 것이다. 예제// 예제4 private void Run2() { var task1 = Task.Run(() => LongCalc2(10)); // await task1과 동일한 효과 // task1.ContinueWith(x => { this.label1.Text = "Sum = " + task1.Result; this.button1.Enabled = true; }, TaskScheduler.FromCurrentSynchronizationContext()); } [Advanced Topic] 윈폼이나 WPF 같은 UI 프로그램은 await가 실행되기 전에 당시 실행되고 있는 쓰레드를 캡쳐해서 SynchronizationContext 에 가지고 있으며, await가 끝난 후에 이 컨텍스트로부터 원래 쓰레드 맥락에서 다음 문장들을 실행하게 한다. 하지만, 콘솔 프로그램이나 윈도우즈 서비스 프로그램의 경우에는 SynchronizationContext가 디폴트로 null 이 되어, await 이후의 문장들을 실행할 때 Thread Pool에서 작업 쓰레드를 가져와 실행하게 된다. 아래 예제는 awiat 실행 이전에 SynchronizationContext.Current 가 null 임을 체크하고 있으며, 또한 await 이후에 쓰레드가 작업 쓰레드임을 ManagedThreadId 속성을 통해 확인해 보는 것이다.
< / > 목차비동기 처리 방식자바스크립트는 싱글 스레드 프로그래밍언어기 때문에 비동기처리가 필수적이다. 비동기 처리는 그 결과가 언제 반환될지 알수 없기 때문에 동기식으로 처리하는 기법들이 사용되어야 하는데, 대표적으로 setTimeout이 있고 callback과 promise가 있다. 세 가지 모두 비동기 코드를 동기식으로 작성하는데 훌륭한 기법들이지만, 모두 약간의 문제점을 가지고 있다. async 와 await 는 이런 문제들을 해결함과 동시에 그 사용법에 있어서도 훨씬 단순해졌다.
Promise 문법
async 문법
한눈에 봐도 매우 직관적인 코드로 변했다고 느낄수 있다. 함수에 async만 붙이면 자동 Promise객체로 인식되고 return값은 resolve()값과 똑같다. return을 문자열로 했다고해서 일반 함수처럼 정말 문자열이 리턴되는 것은 아니다. 앞서 Promise를 리턴하게 해주는 지정자가 async라고 했다. 그래서 리턴값은 Promise{<resolved>: "hello2"}
너무 햇깔리면 이렇게 직관적으로 Promise메서드를 줘도 된다.
async & awaitasync 와 await 는 절차적 언어에서 작성하는 코드와 같이 사용법도 간단하고 이해하기도 쉽다. function 키워드 앞에 async만 붙여주면 되고 비동기로 처리되는 부분 앞에 await만 붙여주면 된다.
그리고 프라미스가 처리가 완료되어 resolve(값) 되면 값만 따로 추출해서 리턴한다. await는 promise.then보다 좀 더 세련되게 프라미스의 result 값을 얻을 수 있도록 해주는 문법이다. promise.then보다 가독성 좋고 쓰기도 쉽다.
async / await로 코드 최적화 예제 정통 Promise
async 코드 최적화 #1비동기 하는 함수 만들려면, 각 함수에다가 return new Promise()를 일일히 생성 해줘야 했다. 어차피 다 똑같은 프로미스인데 중복으로 작성되고 얼마나 비효율적인가? 프로미스 선언은 한번만 하고 각 함수에서 가져다 쓰는 방법은 없을까? 프로미스로 비동기 함수를 하나 만들어주고, 동기 처리를 async으로 하게 하면 된다. delay()라는 프로미스 객체 비동기 함수를 만들고, 각 async함수 getApple, getBanana에서 동기처리를 한다.
async 코드 최적화 #2하지만 여전히 getFruites()에서 콜백지옥이 일어나 코드가 어리지럽다. 생각해보니 굳이 async에서 리턴한 값(Promise.resolve(값)) 을 then으로 처리할 필요는 없다. getApple()에서 리턴한 값(resolve(값))을 받으려면, getApple().then( (value) => {}); then()을 통해 아규먼트로 받을수 있지만 저 value를 꺼내올수없어서 박스에서 처리해야한다는 단점이 있다. 반면에 await은 Promise가 끝날떄까지 기다리면서, 끝나면 resolve(값)에서 값만 추출해 반환하는 특성을 가지고 있다. let value = await getApple(); 즉 resolve의 값을 추출해 변수에 떄려박으면 매우 간단하게 내부가 아닌 외부함수내에서 처리 가능하다.
async 코드 최적화 #3하지만 문제점이 있다. 각 비동기를 순차적으로 기다려야 한다는 것이다.
apple과 banana는 서로 전혀 상관없는 관계이다. 1초만에 둘을 불러오고 처리하면 될건데, 지금 상태를 보면 총 2초가 걸리게 된다. 왜냐하면 하나씩 await을 했으니까. 이를 병렬 처리해서 한번에 되도록 해보자
async / await의 예외 처리(Promise의 reject()를 async에서 구현) 별거 없다. try.. catch문을 쓰고 그냥 throw해주면 된다. 리턴값이 Promise객체이니 잡는 방법도 Promise.catch() 방법과 똑같다. 0 1setTImeout 과 async 함께 쓰기예를 들어 1초뒤에 비동기 함수 처리 하는 기법 2
공유하기 게시글 관리 구독하기Inpa Dev 👨💻js async, js async await, js await, js 비동기, 비동기, 자바스크립트 비동기 잘못된 내용이 있으면 댓글로 피드백 부탁드립니다 :) 이 글이 좋으셨다면 구독 & 좋아요 여러분의 구독과 좋아요는 구독하기 0 이전 포스트 [JS] 📚 비동기처리 - Promise 문법 정리 다음 포스트 [JS] 📚 클로저 (Closure) 개념 완벽 정리 |