[C#] UI 스레드에서 비동기 작업을 InvalidOperationException 없이 수행하는 방법

이전까지 썼던 비동기 코드(를 가장한 쓰레기)

  멍청하게도, 저는 UI 코드에서 비동기 작업을 해야할 때 아래와 같은 식으로 작업했습니다.
// MainWindow.cs
public async void SomeElement_SomeEvent(object sender, RoutedEventArgs e)
{
    await SomeMethod();
    // SomeMethod() 내부에 비동기 구현이 되지 않았다면
    await Task.Run(() => SomeMethod1());
}

  근데 만약, SomeMethod 혹은 SomeMethod1 내부에 UI 스레드의 엘리먼트를 변경하는 코드가 있다면?
public async Task SomeMethod()
{
    _txtSomeElement.Text = "ABC";
}

  해당 부분 코드가 실행될 경우, InvalidOperationException(크로스 스레드 작업이 잘못되었습니다)라고 에러가 발생할 겁니다.

  그래서, 아주 아주 멍청하게도 아래와 같은 방식의 코드를 짜서 썼었죠.
public async void SomeElement_SomeEvent(object sender, RoutedEventArgs e)
{
    await Dispatcher.Invoke(new Action(async () => await SomeMethod()));
}

public async void SomeMethod()
{
    await Task.Run(() => _txtSomeElement.Text = "ABC";
}
세-상에... 저게 대체 무슨 가독성도 성능도 바닥에 말도 안 되는 코드란 말입니까! 설마, 그럴 리는 없겠지만, 저처럼 저게 왜 잘못됐는지 모르는 분이 계신다는 가정 하에 설명을 하도록 하겠습니다.

Invoke, BeginInvoke

  WinForms 앱에서는 Control.Invoke, Control.BeginInvoke / WPF 앱에서는 Dispatcher.Invoke, Dispatcher.BeginInvoke 라고 명명된 메소드가 존재합니다. 그리고 Invoke, BeginInvoke 메소드 둘 다 컨트롤/디스패쳐가 동작하는 UI 스레드에서 대리자를 실행시키는 점에선 같죠.

  그럼 다시 위의 코드로 가서, 무엇이 잘못됐을지 한 번 생각해봅시다. SomeElement_SomeEvent 이벤트 핸들러가 동작할 경우 SomeMethod가 실행되며, SomeMethod는 UI스레드가 아닌 새로운 스레드에서 UI엘리먼트(_txtSomeElement)의 Text 속성을 바꾸게 됩니다. 헌데, Dispatcher.Invoke 메서드를 통해 실행되므로, 아래와 같이 실행되겠죠.

  1. Dispatcher.Invoke 진입 (UI 스레드에서 실행)
  2. 새 스레드로 진입(UI 스레드가 아님)
  3. _txtSomeElement의 Text 속성을 바꿈 (UI 스레드가 아닌 곳에서 UI엘리먼트 속성 변경)
  4. ??? InvalidOperationException, 크로스 스레드 작업이 잘못되었습니다!

  네. Dispatcher.Invoke로 UI 스레드에서 작업을 하는 척 하지만, Task.Run으로 새 스레드를 만들어 해당 스레드에서 작업하므로, 결국 UI 스레드에서 작업하지 않게 됩니다. 때문에, 크로스 스레드 에러를 내뿜죠. 이유는 모르겠지만 발생 빈도는 현저히 줄긴 합니다. 발생 빈도가 줄어서 처음에 짧게 짧게 디버그할 때 문제가 없는 줄 알았다가, 실사용에서 크로스 스레드 에러가 발생해 곤욕을 치렀죠. 왜 발생 빈도가 줄어드는지 아시는 분은 댓글 남겨주시면 감사하겠습니다.

  아무튼, Invoke 내에서 Task.Run으로 비동기를 구현하면 결국 새 스레드에서 작업하기 때문에 InvalidOperationException 크로스 스레드 에러가 발생하게 됩니다. 그럼 어떻게 해야할까요?

UI 스레드에서 비동기로 작업? BeginInvoke

  답은 이미 나와있습니다. [Control|Dispatcher].BeginInvoke 메서드입니다.

  해당 메서드는 대리자를 UI 스레드에서 비동기로 실행합니다. 또한, 일반적으로 Begin~~~ 메서드가 End~~~를 요구하는 반면, [Control|Dispatcher].BeginInvoke는 공식적으로 EndInvoke를 필요로 하지 않습니다. 써놓고 잊어버리면 되는 것이죠.

  아무튼, BeginInvoke 메서드를 사용해 정상적인 코드로 바꿔보도록 하겠습니다.
public async void SomeElement_SomeEvent(object sender, RoutedEventArgs e)
{
    Dispatcher.BeginInvoke(new Action(() => SomeMethod());

    // ...
}

public void SomeMethod()
{
    _txtSomeElement.Text = "ABC";
}

  원래 코드랑 비교해보세요. 이 코드는 에러도 없고, 완벽하게 비동기로 실행됩니다. 이전 코드가... 정말 말도 못 하게 쓰레기처럼 보이게 됐습니다.

  뭐 그래도 하나 새로 알았으니 다시 열심히 배우면서 코딩하렵니다.

댓글

  1. 안타깞게도 SomeMethod에 Delay나 처리가 많은 행위를 할경우 UI가 Block 됩니다.

    답글삭제
  2. 마지막 코드처럼 개발하면 SomeMethod() 작업이 UI Thread에서 실행되는게 맞나요?

    답글삭제
    답글
    1. Dispatcher.BeginInvoke 메서드는 Dispatcher가 연관된 스레드에서 비동기로 실행하는 메서드입니다. 따라서 SomeElement_SomeEvent 메서드가 UI 스레드에서 실행됐다면 UI 스레드에서 비동기로 실행됩니다.

      https://docs.microsoft.com/en-us/dotnet/api/system.windows.threading.dispatcher.begininvoke?view=windowsdesktop-6.0

      삭제

댓글 쓰기

이 블로그의 인기 게시물

C# 남아도는 메모리에도 불구하고 OutOfMemoryException이 발생한다면?

USB를 뒤는 괜찮은데 앞에 꽂으면 인식이 힘들다?

MySQL 데이터 타입과 Java 데이터 타입 비교/매칭