내용 보기

작성자

관리자 (IP : 172.17.0.1)

날짜

2020-08-06 06:31

제목

[C#] SynchronizationContext 와 await


C#에서 비동기(Async) 동작을 하기 위해선 보통 async - await 을 사용한다.

그리고 WPF에서 메인 쓰레드인 UI 쓰레드의 어떤 UI Component를 다른 쓰레드에서 변경하려고 하면 InvalidOperationException 오류가 발생한다.

그래서 UI 쓰레드가 아닌 다른 쓰레드에서 UIComponent를 변경하려고 하면 Dispatch.BeginInvoke() 함수를 사용한다.

SynchronizationContext는 현재 수행중인 코드가 속한 쓰레드의 현재 환경을 나타낸다. 다시 말해, 우리가 비동기 프로그램에서 다른 쓰레드를 실행시킬 때 현재의 쓰레드 환경을 SynchronizationContext에 저장함을 의미한다.    

그렇다면 SynchronizationContext는 왜 쓰는가?
A 쓰레드에서 B 쓰레드에게 W라는 일을 시킨다고 가정해보자.

그리고 "A 쓰레드에서" B 쓰레드에게 맡긴 일이 끝난다면 X라는 일을 수행한다고 치자.
즉, B가 W 작업을 다 끝내면, A 쓰레드는 X라는 일을 마저 하는 것이다.

자, 이제 B가 W 작업을 열심히 끝마친 순간으로 가보자.
B는 자신의 작업이 끝났으므로 A에게 X 작업을 마저 하라고 알려줘야 한다.
그런데 자신을 부른 쓰레드가 A라는 걸 어떻게 아는가? 쓰레드 풀에 존재하는 C 쓰레드나 D 쓰레드일수도 있지 않은가?

이 때 A를 알 수 있는 객체가 SynchronizationContext에 저장된 Current 이다.
이 Current 값을 확인하여 자신을 부른 쓰레드가 A임을 확인하고 A에게 X 작업을 시킬 수 있다.

이 일련의 과정을 코드로 어떻게 짤 수 있는가?

A 쓰레드에서 다음과 같은 개념으로 작성하면 된다.

public void DoW()
{
    // On A Thread
    var sc = SynchronizationContext.Current;
 
    ThreadPool.QueueUserWorkItem(delegate{
        // Do work W on B Thread
        sc.Post(delegate{
            // Do work X on A thread
        }, null);
    })
}
cs

하지만 우리는 보통 C#에서 비동기 작업을 할 때 await 키워드를 자주 사용한다.
그리고 알고보니, await은 내부적으로 SynchronizationContext와 Task.ContinueWith() 함수의 조합으로 구성되어 있다.
그리고 SynchronizationContext.Current 객체의 Post() 함수를 사용하는데,
이는 어떤 작업을 SynchronizationContext에 저장된 쓰레드로 전달하는 데 사용되는 함수이다.

await DoWAsync();
DoX();
cs

위의 코드는 내부적으로

var task = DoWAsync();
var cs = SynchronziationContext.Current;
task.ContinueWith(delegate{
    if(cs == null) DoX(); // Do X on B ThreadPool
    else cs.Post(delegate { RestOfMethod(); }, null);
}, TaskScheduler.Current);
cs

이와 같이 동작한다.

출처1

https://korsa.tistory.com/94

출처2