크로스 스레드 발생 원인 및 해결하기[WinForm with C#]

안녕하세요. 언제나 휴일에 언휴예요.

이번 강의는크로스 스레드를 다룰 거예요.

Windows Forms 프로그램은 컨트롤을 생성한 스레드가 아닌 다른 스레드가 속성을 바꾸는 등의 작업을 요청하면 크로스 스레드 예외가 발생합니다.

특이하게 크로스 스레드 예외는 실행 모드에서는 발생하기 않고 디버그 실행 모드에서 발생합니다.

이번 강의에서는 왜 크로스 스레드 예외가 발생하는지를 알아보고 이를 해결하는 방법을 알아볼게요.

1.  실습 시나리오
2. 크로스 스레드 예외 발생
3. 크로스 스레드 예외 발생 원인
4. 크로스 스레드 예외 해결하기
5. 실습에서 작성한 소스 코드

1. 실습 시나리오

이번 강의에서 실습은 특정 디렉토리에 있는 문서 파일들을 다른 디렉토리에 이동하는 응용을 소재로 할 거예요.

프로그램을 시작하면 소스 디렉토리에 파일 목록을 소스 리스트 박스에 보여줍니다.

이동 버튼을 클릭하면 소스 디렉토리에서 타겟 디렉토리로 파일을 이동시킵니다.

그리고 타겟 리스트 박스에 이동한 파일 목록을 보여줍니다.

이 때 파일 하나를 이동하는 것이 느린 작업이라 가정하여 의도적으로 1초간 지연시키기로 할게요.

        public static void MoveStart()
        {
            while(source.Count>0)
            {
                MoveFile(source[0], src_dir, dst_dir);
            }            
        }

        private static void MoveFile(string s, string src_dir, string dst_dir)
        {
            string src = string.Format("{0}{1}", src_dir, s);
            string dst = string.Format("{0}{1}", dst_dir, s);
            File.Move(src, dst);
            Thread.Sleep(1000);//의도적으로 시간이 걸리는 작업처럼 지연
            if(FileMovedEventHandler !=null)
            {
                FileMovedEventHandler(null, new FileMovedEventArgs(s));
            }
            source.Remove(s);
        }

그런데 이동 버튼을 클릭하였을 때 MoveStart 메서드를 호출하면 모든 파일을 이동하기 전까지 프로그램은 블로킹 상태로 멈춰있습니다.

창을 이동하는 것도 못 하는 상태인 것이죠.

이를 해결하기 위해서는 MoveStart 메서드를 비동기로 수행하게 구현합니다.

        delegate void MoveDele();
        public static void MoveStartAsync()
        {
            MoveDele dele = MoveStart;
            dele.BeginInvoke(null, null);
        }

2. 크로스 스레드 예외 발생

앞의 내용처럼 파일 이동을 비동기 대리자를 이용하여 수행하고 디버그 모드로 실행해 보세요.

하나의 파일을 이동시킨 후 타겟 리스트 박스에 항목을 추가하고 소스 리스트 박 스 항목을 제거하는 부분에서 크로스 스레드 예외가 발생할 거예요.

크로스 스레드 예외 발생
[그림] 크로스 스레드 예외 발생

3. 크로스 스레드 예외 발생 원인

이처럼 비동기 프로그래밍 방식으로 처리할 때 크로스 스레드 예외가 발생할 수 있어요.

컨트롤을 생성한 스레드가 아닌 다른 스레드가 컨트롤의 속성을 변경하는 등의 작업을 할 때 발생하는 것이죠.

이러한 크로스 스레드 예외는 디버그 모드에서 발생하고 실행 모드에서는 발생하지 않아요.

왜 크로스 스레드 예외가 발생하는 것일까요?

비동기 프로그래밍에서 서로 다른 스레드가 같은 자원을 경쟁해서 사용하는 현상은 드물지 않은 현상입니다.

그리고 이러한 경쟁 상태를 효과적으로 대처하지 않는다면 교착 상태에 빠지거나 원하지 않는 형태로 동작할 수도 있어요.

크로스 스레드 예외는 컨트롤을 생성하지 않은 스레드가 컨트롤을 사용하는 코드를 만났을 때 개발자에게 컨트롤을 경쟁하는 상태임을 알려주는 것입니다.

개발자가 원하지 않는 상태로 동작할 수 있으니 신중하게 대처하라는 것이죠.

모든 경쟁 상태가 문제를 발생하지는 않지만 문제가 발생하는 것을 인지하는 것은 어렵거나 시간이 걸릴 수 있는 작업입니다.

일단 개발자가 크로스 스레드 예외가 발생한 것을 통해 자원 경쟁 상태가 발생한 것을 개발 단계에서 알게 된 것으로도 많은 비용을 줄이는 것입니다.

4. 크로스 스레드 예외 해결하기

크로스 스레드 예외가 발생하였을 때 여러 대처 방법 중에 자원 경쟁 상태를 없애는 것도 하나의 방법입니다.

원천적으로 자원 경쟁 문제를 없애 교착 상태 등으로 빠지는 것을 차단하는 것이죠.

Windows Forms의 모든 컨트롤에는 InvokeRequired 속성을 제공하고 있어요.

이 값이 True라면 현재 스레드가 자신을 생성한 스레드가 아닙니다.

이 때 컨트롤에 있는 BeginInvoke 메서드에 대리자 개체와 인자를 전달하면 .NET에서는 컨트롤을 생성한 스레드가 대리자에 설정 메서드를 수행하게 해 줍니다.

이제 경쟁 구도는 없어지는 것이죠.

다음은 크로스 스레드 예외를 해결하는 코드입니다.

        private void FileManager_FileMovedEventHandler(object sender, FileMovedEventArgs e)
        {
            MovedFile(e.FileName);
        }

        delegate void MovedDele(string fn);
        private void MovedFile(string fn)
        {
            if (lbox_dst.InvokeRequired)
            {
                MovedDele dele = MovedFile;
                object[] objs = new object[] { fn };

                lbox_dst.BeginInvoke(dele, objs);
            }
            else
            {
                lbox_dst.Items.Add(fn);
                lbox_src.Items.Remove(fn);
            }
        }

5. 실습에서 작성한 소스 코드

Form1.cs

using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace 크로스_스레드_문제_해결하기
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            List files = FileManager.Source;
            FileManager.FileMovedEventHandler += FileManager_FileMovedEventHandler;
            foreach(string file in files)
            {
                lbox_src.Items.Add(file);
            }
        }

        private void FileManager_FileMovedEventHandler(object sender, FileMovedEventArgs e)
        {
            MovedFile(e.FileName);
        }

        delegate void MovedDele(string fn);
        private void MovedFile(string fn)
        {
            if (lbox_dst.InvokeRequired)
            {
                MovedDele dele = MovedFile;
                object[] objs = new object[] { fn };

                lbox_dst.BeginInvoke(dele, objs);
            }
            else
            {
                lbox_dst.Items.Add(fn);
                lbox_src.Items.Remove(fn);
            }
        }

        private void btn_move_Click(object sender, EventArgs e)
        {
            FileManager.MoveStartAsync();
        }
    }
}

FileManager.cs

using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace 크로스_스레드_문제_해결하기
{
    public static class FileManager
    {
        public static event FileMovedEventHandler FileMovedEventHandler = null;
        static List source = new List();
        public static List Source 
        { 
            get
            {
                return source;
            }
        }
        static FileManager()
        {
            DirectoryInfo di = new DirectoryInfo(src_dir);
            FileInfo[] fis = di.GetFiles();
            foreach(FileInfo fi in fis)
            {
                if(fi.Attributes == FileAttributes.Archive)
                {
                    source.Add(fi.Name);
                }
            }
        }

        static string src_dir=@"src\";
        static string dst_dir = @"dst\";

        delegate void MoveDele();
        public static void MoveStartAsync()
        {
            MoveDele dele = MoveStart;
            dele.BeginInvoke(null, null);
        }
        public static void MoveStart()
        {
            while(source.Count>0)
            {
                MoveFile(source[0], src_dir, dst_dir);
            }            
        }

        private static void MoveFile(string s, string src_dir, string dst_dir)
        {
            string src = string.Format("{0}{1}", src_dir, s);
            string dst = string.Format("{0}{1}", dst_dir, s);
            File.Move(src, dst);
            Thread.Sleep(1000);//의도적으로 시간이 걸리는 작업처럼 지연
            if(FileMovedEventHandler !=null)
            {
                FileMovedEventHandler(null, new FileMovedEventArgs(s));
            }
            source.Remove(s);
        }
    }
}

FileMovedEventArgs.cs

using System;

namespace 크로스_스레드_문제_해결하기
{
    public delegate void FileMovedEventHandler(object sender, FileMovedEventArgs e);
    public class FileMovedEventArgs:EventArgs
    {
        public string FileName
        {
            get;
            private set;
        }
        public FileMovedEventArgs(string filename)
        {
            FileName = filename;
        }
    }
}