C# 언어에서 동기 비동기 async sync 사용 방법 - Whitmem
C# 언어에서 동기 비동기 async sync 사용 방법
C# Language
2025-02-11 20:15 게시 e14cd94910f988a9bf38

0
0
46
이 페이지는 외부 공간에 무단 복제할 수 없으며 오직 있는 그대로 게시되며 부정확한 내용을 포함할 수 있습니다. 법률이 허용하는 한 가이드 라인에 맞춰 게시 내용을 인용하거나 출처로 표기할 수 있습니다.
This page is not to be distributed to external services; it is provided as is and may contain inaccuracies.
C#에서의 비동기 ASYNC 의 용도 및 사용 방법
C#을 접하다보면 비동기, 동기 처리를 많이 접하게 된다. 문법으로는 await, async 가 자주 눈에 보이곤 하는데 이 문법들이 무슨 역할을 하는지 알아본다. 기본적으로 작성한 코드는 절차적으로 수행되고 이전 코드의 동작이 덜 끝나면 다음 라인의 코드는 더 이상 대기 모드로 들어간다. 당연히 이전 작업이 끝나야 다음 작업을 수행하는 것은 당연하다. 단순히 하나의 작업을 처리하고 대기하는데에는 비동기 작업이 굳이 필요 없다. 오로지 동기적으로만 처리해서 모든 것을 쉽게 구현할 수 있다.
하지만 현실적으로는 그렇지 않다. 하나의 프로그램에 한 개의 로직만 탑재되지 않고 여러 로직이 유기적으로 묶어서 돌아가기 때문에 하나의 로직에서 시간이 오래 걸리는 작업을 수행하는 경우 같은 프로세스의 다른 로직또한 전체 멈춰버릴 수 있다.
예를 들어, 게임 프로그램에서는 UI를 처리하는 로직과 게임 물리를 처리하는 로직, 업데이트 로직이 존재할 수 있다. 보통 동기 프로세스로 구현을 한다면 파일을 다운로드 하는 동안 파일 다운로드 로직 안에 있는 다음 코드는 실행되지 않고 대기하는 것이 당연하다.
이 코드는 파일 다운로드가 완료된 후 복사를 수행하는 것이 의도된 것이기 때문이다.
하지만 문제는 하나의 프로그램 안에 여러 로직이 존재하기 때문에 파일 다운로드를 실행하는 동안 다른 로직 또한 현재 라인에서 멈춰버릴 수 있다는 것이다. 즉 현재 로직에서는 의도한 대로 대기하지만, 다른 로직은 여전히 실행돼야 하는 경우 다 같이 멈춰버려 프로그램이 응답 없음 상태에 빠질 수 있다.
즉 이 문제는 로직이 적어도 두 개 이상 존재하고 각 로직이 별개로 공존해야할 때 가시화된다. 한 개의 로직만 존재할 때는 해당 오직안에서 멈추더라도 상관 없다. 어차피 코드는 흐름상으로 수행되기 때문이다. 그러나 다른 로직은 각각의 코드 흐름을 갖고 있기 때문에 여기서 골치가 아파진다.
예를 들어 위 코드에서는 UI 처리 로직과 파일 다운로드 및 복사 로직이 있다고 가정한다. UI는 항상 동작이 되어야, 파일 다운로드 상태를 UI에 표시하거나 할 텐데, 컴퓨터 프로그램은 절차적이기 때문에 무조건 어디에선가는 하나의 작업을 길게 가져가는 동안 다른 작업을 수행할 수 없게 된다.
그렇기 때문에 이럴 때 사용하는 것이 비동기 로직이다. 비동기 로직은 다른 로직에 영향을 주지 않고 현재 로직에서만 대기하는 등의 멀티 작업을 가능케 한다.
즉 비동기 프로그래밍을 하면 내부적으로 적절하게 스케쥴링을 하여 두 작업이 공존할 수 있도록 절차를 조정한다고 보면 된다. 컴퓨터 CPU는 두 가지 동작을 완전 동시에 수행할 수 없다. 시간의 차가 매우 적을 뿐 절차적으로 실행되는 것이 컴퓨터이다. 이 시간을 적절히 조절하고 분배하는 것이 프로그램 개발의 과제인 것이다.
파일 다운로드 예시
예를 들어 파일 다운로드 또한 동기적으로 수행하고 그 결과를 기다릴 수 있다. 당연히 다운로드가 완료 돼야 파일을 복사하든 말든 할 터이니 다운로드가 완료될 때 까지 이 코드 공간에서는 대기되는 것이 맞다.
하지만 이렇게 비동기로 await 해주면 적어도 다른 로직 공간이 대기 모드에 빠지지 않는다. 이 async 비동기 함수만 대기 모드에 빠지게 된다.
또 다른 한 가지 예를 들어본다. 위 코드는 비동기 함수를 통해 서로 다른 로직이 실행되는 것을 나타낸 것이다. 한 개의 로직은 1초에 한 번씩 안녕 글자를 내뱉고, 다른 한 개의 로직은 10초에 한 번씩 잘가 글자를 내뱉는 코드이다.
기본적으로 메인에서 두 비동기 코드를 호출할 때 await 하지 않았고 그냥 메서드를 실행하듯이 실행했기 때문에 두 코드는 동시에 공존한다. 만약 메인 실행하는 함수가 비동기가 아닌 동기 구역에서 비동기 함수를 실행하게 되어도, 기본적으로 비동기 함수는 비동기적으로 수행된다.
위 코드에서 시작점은 async 이 아닌 일반 함수이다. 즉 일반 함수에서 비동기 함수 2개를 호출하였고, Thread.Sleep를 통해 프로그램이 종료되지 않도록 대기하고 있다.
이 같은 작업을 해준 이유는, 비동기 함수 2개를 호출하는 즉시 while문 안의 Task.Delay()가 수행되는 동안 sayHello() 및 sayBye() 의 함수 내에서는 대기 모드로 진입하지만, WorkMain()의 시작 점에서도 대기되지는 않는다. 즉 여기서는 비동기 함수를 실행하고 넘어가기 때문에 메인의 끝점에 도달하면 프로그램이 종료된다. 따라서 메인의 끝점에 도달하지 않도록 메인 쓰레드를 길게 대기시켜준다.
이외 방법은 비동기 함수가 끝날 때 까지 다음으로 넘어가지 않고 대기하도록 할 수 있다. 이 비동기 함수를 임의로 대기하기 위해서는 해당 비동기 함수를 실행한 뒤 Task 객체를 가져와 .wait() 해주면 된다.
이를 위해서 비동기 함수의 반환 값을 Task로 해야한다. 실제 반환하는 값은 없어도 된다. 만약 어떤 반환하는 값이 존재하는 경우 Task<반환값> 형태로 반환해야 한다. 물론, Task 를 안쓰고 void 와 같은 형태를 써도 async 예약어만 앞에 붙여놓으면 비동기로 동작되지만, 이런 경우 호출하는 입장에서 비동기 함수 완료 대기 등의 작업을 수행할 수 없다.
위 코드는 sayHello() 비동기 함수는 실행하고, sayBye() 비동기 함수도 실행하되, sayBye()가 끝날때 까지 대기하는 것이다. 즉 sayHello() 끝난 여부와 관련 없이 sayBye()가 끝나는 경우에만 메인에서 코드가 다음으로 넘어갈 수 있다. 두 코드는 공존하지만, 메인 쓰레드에서는 더 이상 멈춰있고, sayBye가 끝나는 경우 계속되는 것이다.
하지만, 위의 경우에는 조금 다르다. 위 코드는 이전과 달리 완전 동기적인 코드만 포함된 함수이다. 물론 함수는 비동기로 구현되었지만 안에서 동기적인 쓰레드 멈추기 등을 수행한다. 따라서 이를 메인에서 호출하게 되면 메인 쓰레드에서 비동기 함수가 호출되었으나, Thread Sleep로 인해 메인 쓰레드가 멈추어 버려 WorkMain 쓰레드가 대기 모드로 들어간다. 즉 정확히 말하자면 WorkMain() 에서 비동기 함수가 서로 동시에 공존하는 것이 아니고, 안에서 await 가 있는 경우 그 시점에 공존된다. 즉 await 가 있는 경우 다시 스케쥴링 된다.
위 코드를 보면, sayHello()가 먼저 비동기적으로 실행되었지만, 사실상 a==5 전까지는 비동기가 아니라, 동기적으로 쓰레드를 멈춘다. 즉 이를 호출한 WorkMain()이 블록킹 되고, a==5에 비로소 await 가 수행된다. 이 때 비동기 스케쥴링이 한 번실행되어 1초 뒤 예약되고, WorkMain은 다음 코드로 빠져나온다. 이 때 Main ------> Next가 출력되고, sayBye()가 실행된다. 하지만 sayBye 역시 동기 코드이기에 메인 쓰레드를 점유한다.
즉 두 코드는 결과적으로 동기적으로 1초마다 대기한다. 그럼에도 불구하고, a==5 이후에 두 코드가 공존할 수 있는 이유는 sayHello에서 a==5 이후 await 되면서 sayHello 의 내부 쓰레드는 새로 생성되기 때문이다. 실제 Thread.CurrentThread.ManagedThreadId를 출력하면 a==5 이후 sayHello 내부의 쓰레드 넘버가 바뀌는 것을 확인할 수 있다.
즉 await 를 통해 비동기를 예약함과 동시에 이후 부터 자동으로 새로운 쓰레드에서 동작을 수행하는데, 호출 처음부터 새로운 쓰레드에서 동작을 수행하기 위해서는 Task.Run(Action)을 통해 비동기 함수를 수행하면 된다.
Task.Run 으로 비동기 함수를 실행하면 해당 함수는 새로운 쓰레드 공간에서 수행되기 때문에 작업이 공존할 수 있게 된다. 즉 Thread.Sleep 는 현재 쓰레드가 대기 상태로 들어가는 것이기 때문에 동기적인 대기이다. 그렇기에 비동기 함수로 호출을 하더라도 현재 쓰레드가 멈춰버리는 위험이 존재한다.이런 경우에는 별개 쓰레드에서 실행될 수 있도록 Task.run 을 통해 별개 쓰레드 공간에서 함수를 실행해야 한다.반면 Task.Delay 는 아예 딜레이 작업 조차 비동기 함수로 구현된 것이기 때문에 단순히 Task.Run 으로 실행할 필요는 없다.
즉 결과적으로 기존에 동기적으로 작성된 코드를 비동기적으로 수행할 필요가 있는 경우 Task.Run() 을 사용할 수 있다. Task Run 의 결과는 Task 로 반환되기 때문에 여기서 다시 .Wait()를 통해 동기적으로 대기할 수 있다. 또는 Task.Run 결과는 <대기 가능>이기 때문에 다시 await를 통해 비동기적으로 대기할 수 있다.
위 코드 둘다 결과는 동일하다. 즉 어차피 동기 코드가 들어가있는 sayHello() 함수는 동기적으로 수행된다. 이 동기 함수를 Task.Run 에 감싸 실행하면 비동기로 실행할 수 있다. 비동기로 실행하면 이를 대기할지, 그냥 실행하고 다음 코드로 바로 넘어갈지 결정할 수 있다. 애초 Task.Run을 수행하는 즉시 새로운 쓰레드에서 실행되기 때문에 Wait 하지 않으면 새로운 쓰레드에서 실행한 것과 동일하다. 한편 현재 실행한 출처 함수가 비동기 함수인 경우 await 를 하면 비동기 함수대로 현재 코드 영역에서 잠시 대기할 수도 있는 것이고, 동기 공간이면 await 를 못쓰니, .Wait()로 동기적으로 대기할 수도 있고, 그냥 실행하고 넘길 수도 있는 것이다.
실행하는 현재 내 지점의 함수가 동기 함수인 경우 비동기 함수를 호출할 수는 있으나, await 할 수는 없다. 이 경우에는 동기적으로 기다리거나, 실행하고 그냥 넘기거나 둘 중 하나이다. 비동기적으로 기다리려면 호출 요청하는 측의 함수도 비동기 함수로 교체해야 한다. 호출 요청하는 측의 함수를 비동기로 바꾸면, 해당 함수와 연관된 또 다른 함수에서 비로소 이 함수를 실행하고 바로 넘어갈 수 있게 되기 때문에 구현하고자 하는 로직에 유의해야 한다.
정리
비동기에서 동기 함수를 실행하는 방법
동기 함수를 실행하기 위해서는 Task.Run 으로 감싸면 await 를 사용할 수 있게 된다. 동기 함수는 await 할 수 없으므로 비동기적으로 대기하려면 Task.Run 으로 감싸서 새로운 쓰레드에서 실행되고, 비동기 예약되게 해야한다.
동기에서 비동기 함수를 실행하는 방법
syncWork1(); //동기라서 실행 즉시 그냥 넘어가짐 syncWork1().Wait(); // 안넘어가지게 동기적으로 대기할 수 있음
위 코드는 둘다 사실상 비동기적으로 실행되지만, Wait()를 통해 임의로 대기할 수 있다.
이상하게 혼합된 경우
위 코드는 애초 비동기로 구현할 필요가 없던 코드이다. 동기로 구현해서 호출하면 어차피 그것이 동기적인데, 굳이 함수는 비동기로 구현하고, Thread.Sleep()는 다시 동기적으로 넣어두었다. 이 함수를 실행하면 비동기함수지만 쓰레드가 멈춰버린다. 따라서 Task.Run을 해서 새로운 쓰레드에서 실행되도록 하여 비동기 예약되게 하였으나 이 예약된 것을 다시 동기 Wait를 하였다...
댓글 0개
댓글은 일회용 패스워드가 발급되며 사이트 이용 약관에 동의로 간주됩니다.
확인
Whitmemit 개인 일지 블로그는 개인이 운영하는 정보 공유 공간으로 사용자의 민감한 개인 정보를 직접 요구하거나 요청하지 않습니다. 기본적인 사이트 방문시 처리되는 처리 정보에 대해서는 '사이트 처리 방침'을 참고하십시오. 추가적인 기능의 제공을 위하여 쿠키 정보를 사용하고 있습니다. Whitmemit 에서 처리하는 정보는 식별 용도로 사용되며 기타 글꼴 및 폰트 라이브러리에서 쿠키 정보를 사용할 수 있습니다.
이 자료는 모두 필수 자료로 간주되며, 사이트 이용을 하거나, 탐색하는 경우 동의로 간주합니다.