NGMsoftware

NGMsoftware
로그인 회원가입
  • 매뉴얼
  • 학습
  • 매뉴얼

    학습


    C# 크로스 스레드 작업이 잘못되었습니다. (Cross-thread operation not valid)

    페이지 정보

    본문

    안녕하세요. 소심비형입니다. Windows Forms 또는 WPF에서 멀티 스레딩으로 작업할 때 자주 접하게 되는(?) 크로스 스레드라는 에러가 있습니다. 우선, 이 문제를 해결하기 전에 상황 재연을 위한 간단한 윈폼을 만들도록 하겠습니다. 아래와 같이 폼을 디자인하고 Start와 Stop버튼을 추가합니다. 그리고 아래 위치한 TextBox의 Multiline속성을 True로 변경하세요.

    3Tc3q6o.png

     

     

    Start, Stop버튼을 더블 클릭해서 이벤트 핸들러를 추가하세요. 아래는 크로스 스레드 에러를 발생 시키기 위한 전체 소스입니다.

    Form1.cs

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows.Forms;
     
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            bool isThreadLive = false; Thread thread; public Form1()
            {
                InitializeComponent();
            }
     
            private void btnStart_Click(object sender, EventArgs e)
            {
                isThreadLive = true;
                thread = new Thread(new ParameterizedThreadStart(ThreadWorker));
                thread.Start("Start Thread...");
            }
     
            private void btnStop_Click(object sender, EventArgs e)
            {
                isThreadLive = false;
                thread.Abort();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
     
            private void ThreadWorker(object p)
            {
                txtTrace.Text += p.ToString();
                txtTrace.Text += Environment.NewLine;
     
                while (isThreadLive)
                {
                    txtTrace.Text += "Running...";
                    txtTrace.Text += Environment.NewLine;
                    Thread.Sleep(1000);
                }
            }
        }
    }

     

     

    이 프로그램을 실행(F5)한 후 Start버튼을 클릭 해보세요. 아래와 같은 에러를 만나게 됩니다.

    2lI7HWy.png

     

     

    이런 문제가 발생되는 이유는 윈도우 프로그램의 경우 기본적으로 1 Process에 1 Thread를 사용하기 때문입니다. 그래서 모든 작업은 동기화되고, 다중 작업이 불가능하죠. 좀 더 쉽게 말하면 윈도우 응용 프로그램은 한번에 하나의 작업만 할 수 있다는 것입니다-_-; 그렇기에 하나의 응용 프로그램에서 동시에 여러 작업을 할 때 이와같은 문제가 발생되는 거죠. 하지만, UI 스레드를 사용하지 않는 콘솔의 경우에는 이런 문제를 만날일이 거의 없습니다. 아무튼, WinForm이나 WPF의 경우에는 Main Thread에서 관리되는데 이 때 사용자가 만든 다른 스레드가 매인 스레드에서 관리하는 컨트롤에 접근할 때 크로스 스레드가 발생하게 됩니다.

     

    WinForm 컨트롤에 대한 액세스는 기본적으로 스레드로부터 안전하지 않습니다. 둘 이상의 스레드가 컨트롤 상태를 조작하는 경우 컨트롤이 불일치하는 상태로 강제 지정될 수 있기 때문입니다. 또한 경합 상태와 교착 상태 등의 기타 스레드 관련 버그도 발생할 수 있죠. 컨트롤에 액세스할 때는 스레드로부터 안전한 방식을 사용해야 합니다. Invoke 메서드를 사용하지 않고 컨트롤을 만든 스레드가 아닌 다른 스레드에서 컨트롤을 호출하는 것은 안전하지 않습니다.

     

    만약, .NET Framework 1.1에서 이와같은 테스트를 진행한다면 Cross Thread 에러를 볼 수 없을겁니다. NET Framework 2.0부터 안전하지 않은 스레드 작업에 사용자에게 알리도록 추가되었기 때문입니다.

     

    이제 정상적으로 처리될 수 있게 스레드에 안전한 방식으로 컨트롤에 접근하는 코드로 변경합니다.

    Form1.cs

    using System;
    using System.Threading;
    using System.Windows.Forms;
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            bool isThreadLive = false;
            Thread thread;
            delegate void SetText(string text);
     
            public Form1()
            {
                InitializeComponent();
            }
     
            private void button1_Click(object sender, EventArgs e)
            {
                isThreadLive = true;
                thread = new Thread(new ParameterizedThreadStart(ThreadWorker));
                thread.Start("Start Thread...");
            }
     
            private void button2_Click(object sender, EventArgs e)
            {
                isThreadLive = false;
                thread.Abort();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
     
            private void ThreadWorker(object p)
            {
                Run(p);
                while (isThreadLive)
                {
                    Run("Running...");
                    Thread.Sleep(1000);
                }
            }
     
            private void Run(object p)
            {
                if (txtTrace.InvokeRequired)
                {
                    this.Invoke(new SetText(Run), new object[] { p });
                }
                else
                {
                    txtTrace.Text += p.ToString();
                    txtTrace.Text += Environment.NewLine;
                }
            }
        }
    }

     

     

    라인 12의 대리자(delegate)는 TextBox의 속성을 변경하기 위한 비동기 호출을 할 수 있습니다. 보통은 대리자를 만들 때 Callback을 뒤에 붙여서 비동기 호출용 대리자임을 알리게 됩니다. 여기에서는 SetTextCallback과 같이 사용하는게 좋겠죠^^; 그리고 이벤트에 사용할 때는 보통 Handler를 뒤에 붙여줍니다. IndexChangedHandler처럼 말이죠. 하지만 여기에서는 단순히 SetText로 정의했습니다. 예제니까요.

     

    라인 35는 라인 46에 있는 Run 메서드를 호출하도록 변경합니다. While문과 같은 반복기에서 동기화 할 경우 UI 스레드가 업데이트 할 시간을 가질 수 없게됩니다. 이런 경우에는 응용 프로그램이 메시지 큐에 있는 모든 윈도우 메시지를 처리할 수 있도록 강제해야 합니다. 만약, 반복기 안에서 처리하려면 아래와 같이 코드를 수정할 수 있습니다.

    Form1.cs

    using System;
    using System.Threading;
    using System.Windows.Forms;
     
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            bool isThreadLive = false;
            Thread thread;
            delegate void SetText(string text);
     
            public Form1()
            {
                InitializeComponent();
            }
     
            private void button1_Click(object sender, EventArgs e)
            {
                isThreadLive = true;
                thread = new Thread(new ParameterizedThreadStart(ThreadWorker));
                txtTrace.Text += "Start Thread...";
                txtTrace.Text += Environment.NewLine;
                thread.Start();
            }
     
            private void button2_Click(object sender, EventArgs e)
            {
                isThreadLive = false;
                thread.Abort();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
     
            private void ThreadWorker(object p)
            {
                while (isThreadLive)
                {
                    if (txtTrace.InvokeRequired)
                    {
                        this.Invoke(new SetText(ThreadWorker), new object[] { "Running..." });
                    }
                    else
                    {
                        txtTrace.Text += p.ToString();
                        txtTrace.Text += Environment.NewLine;
                    }
     
                    Application.DoEvents();
                    Thread.Sleep(1000);
                }
            }
        }
    }
    

     

     

    53라인의 Application.DoEvents 메서드를 호출하면 윈도우 메시지 큐에 있는 모든 이벤트를 강제로 처리하게 됩니다. 이렇게 처리해서 Run 메서드를 삭제하고 좀 더 간단한 소스가 되었습니다. 예전에는 Thread.Sleep을 호출하게 되면 자동으로 Application.DoEvents도 실행되었지만, 지금은 사용자가 직접 실행해야 합니다. 기억이 정확하지는 않지만, 아마도 .NET 2.0이전과 이후로 나뉘는듯한데... 정확한 내용은 찾아봐야겠네요.

     

    이제 무명 메서드를 이용해서 호출하는 방식으로 변경합니다.

    Form1.cs

    using System;
    using System.Threading;
    using System.Windows.Forms;
     
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            bool isThreadLive = false;
            Thread thread;
     
            public Form1()
            {
                InitializeComponent();
            }
     
            private void button1_Click(object sender, EventArgs e)
            {
                isThreadLive = true;
                thread = new Thread(new ParameterizedThreadStart(ThreadWorker));
                txtTrace.Text += "Start Thread...";
                txtTrace.Text += Environment.NewLine;
                thread.Start();
            }
     
            private void button2_Click(object sender, EventArgs e)
            {
                isThreadLive = false;
                thread.Abort();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
     
            private void ThreadWorker(object p)
            {
                while (isThreadLive)
                {
                    this.Invoke(new MethodInvoker(delegate
                    {
                        txtTrace.Text += "Running..."; txtTrace.Text += Environment.NewLine;
                    }));
     
                    Thread.Sleep(1000);
                }
            }
        }
    }
    

     

     

    위의 41부터 45라인에서 무명 메서드를 넘겨주고 있습니다. Invoke는 파라메터로 대리자를 받아야 하기에 반환값과 파라메터가 없는 모든 메서드를 대리하는 MethodInvoker를 넘겨줍니다. 그렇다면, 대리자를 두번 사용할 필요가 있을까요? 그렇지는 않습니다. 타입을 가지지 않는 무명 메서드도 대리자이기 때문에 Invoke에게 자신도 대리자임을 알려주기만 하면 됩니다. 그래서 아래와 같이 좀 더 간단하게 표현할 수 있습니다.

    Form1.cs

    using System;
    using System.Threading;
    using System.Windows.Forms;
     
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            bool isThreadLive = false;
            Thread thread;
     
            public Form1()
            {
                InitializeComponent();
            }
     
            private void btnStart_Click(object sender, EventArgs e)
            {
                isThreadLive = true;
                thread = new Thread(new ParameterizedThreadStart(ThreadWorker));
                txtTrace.Text += "Start Thread...";
                txtTrace.Text += Environment.NewLine;
                thread.Start();
            }
     
            private void btnStop_Click(object sender, EventArgs e)
            {
                isThreadLive = false;
                thread.Abort();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
     
            private void ThreadWorker(object p)
            {
                while (isThreadLive)
                {
                    this.Invoke((MethodInvoker)delegate 
                    {
                        txtTrace.Text += "Running..."; txtTrace.Text += Environment.NewLine;
                    });
     
                    Thread.Sleep(1000);
                }
            }
        }
    }

     

     

    실행해서 결과를 확인 해볼까요? 아래와 같이 별도의 스레드를 이용하여 TextBox에 메시지를 출력하고 있습니다.

    4vv9sGN.png

     

     

    아래는 BackgroundWorker를 사용하여 윈폼 컨트롤을 업데이트하는 방법입니다. 예제 용도로 만든것이므로 실제 현업에서는 이렇게 사용하지 않는다는것을 미리 알려드립니다-_-; BackgroundWorker의 용도는 UI에서 처리되지 않는 어떤 작업을 수행한 후 이 작업이 완료되면 UI를 업데이트 하는 방식입니다. 쉬운 예로 데이타베이스에서 데이타를 가져오거나 파일을 읽거나 쓸 때 시간이 오래 걸린다면 BackgroundWorker에서 작업하고 UI에서는 다른 작업을 진행할 수 있도록 구현할 때 사용됩니다. 이외에도 Task를 이용하여 비동기 호출을 구현할수도 있습니다.

    Form1.cs

    using System;
    using System.ComponentModel;
    using System.Threading;
    using System.Windows.Forms;
     
    namespace WindowsFormsApp1
    {
        public partial class Form1 : Form
        {
            BackgroundWorker worker; public Form1()
            {
                InitializeComponent();
            }
     
            private void btnStart_Click(object sender, EventArgs e)
            {
                txtTrace.Text += "Start Thread...";
                txtTrace.Text += Environment.NewLine;
                worker = new BackgroundWorker();
                worker.WorkerSupportsCancellation = true;
                worker.DoWork += (object s1, DoWorkEventArgs e1) =>
                {
                    for (int i = 0; i < 10; i++)
                    {
                        this.Invoke((MethodInvoker)delegate
                        {
                            txtTrace.Text += "Running...";
                            txtTrace.Text += Environment.NewLine;
                        });
                        if (worker.CancellationPending)
                        {
                            e1.Cancel = true;
                            break;
                        }
                        Thread.Sleep(1000);
                    }
                };
                worker.RunWorkerCompleted += (object s1, RunWorkerCompletedEventArgs e1) =>
                {
                    txtTrace.Text += "It was completed work!";
                    txtTrace.Text += Environment.NewLine;
                };
                worker.RunWorkerAsync();
            }
     
            private void btnStop_Click(object sender, EventArgs e)
            {
                worker.CancelAsync();
                txtTrace.Text += "Stop Thread...";
                txtTrace.Text += Environment.NewLine;
            }
        }
    }

     

     

    멀티 스레딩에 대한 자세한 내용은 C# 강좌에서 다루도록 하겠습니다. 적다보니 문제 해결에 대한 내용 외에 여러가지가 포함되어 버렸네요.

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천0 비추천0

    댓글목록

    등록된 댓글이 없습니다.