개발/C#

Thread를 이용한 윈폼 프로그래밍 (크로스 스레딩)

FA1976 2017. 12. 7. 14:56

Thread의 개념적인 부분은 넘어가기로 하겠습니다

지금 부터 설명할 것은 C# 윈폼에서의 다중 쓰레드를 이용하여 컨트롤들을 조작하는 내용입니다.

평소에 가장 보기 쉬운 윈폼 다중 쓰레드 프로그램은, 프로그램이 업데이트 하는 업데이트 폼이 아닐까 싶습니다.

업데이트 폼은 보통 프로그래스바를 이용하여 작업 진행상황을 보여주고 밑에 취소 버튼등이 있죠.!

만약 이걸 단일 쓰레드로 한다면, 업데이트 작업이 완료될동안  즉 프로그래스바가 완료 될때 까지  취소버튼을 누를수 없는 

먹통이 될것으로 예상합니다. 단일 쓰레드는 업데이트를 받으면서 프로그래스바를 값을 계속 변경해줘야 하기
 
때문에 취소 버튼의 이벤트를 받을 수 없을테니까요
.  예로써 간단하게 실험을 해보았습니다.

일단 간단하게 폼에 프로그래스바 1개와

버튼을 하나 올려 놓았습니다

그리곤 시작 버튼에 클릭이벤트를 걸어서

프로그래스바의 값을 변경하도록 했습니다

//코드1
//시작 버튼 클릭 이벤트
  private void button1_Click(object sender, EventArgs e)
  {
            progressBar1.Minimum = 0;  
            progressBar1.Maximum = 1000000;  //프로그래스의 최대값을 설정

            for (int i = 0; i < 1000000; i++) // for문을 이용해 프로그래스바의 값을 변경해 줍니다
            {
                progressBar1.Value = i;
            }
}

이대로 프로그램을 실행한다면 에러는 나지 않지만 프로그래스바가 값이 변경되는 동안은 프로그램이 먹통이 되는것을 
확인 할 수 있으실겁니다.

이런 상황이 발생 하지 않도록 하기 위해 다중 쓰레드를 사용합니다

그래서 똑같은 폼에 쓰레드를 이용해 보았습니다.

//코드2  
private void button1_Click(object sender, EventArgs e)
        {
            // 쓰레드는 생성자로 ThreadStart라는 델리게이트를 파라미터를 받습니다
            // ThreadStart델리게이트는 void ()형의 메소드를 설정할 수 있습니다
            // 파라미터가 있는 메소드를 지정하고 싶을 때는 ParameterizedThreadStart 델리게이틀 사용합니다
            Thread thread1 = new Thread(new ThreadStart(ProgValueSetting));
            thread1.Start();
        }
        private void ProgValueSetting()
        {
            progressBar1.Minimum = 0;
            progressBar1.Maximum = 1000000;  //프로그래스의 최대값을 설정

            for (int i = 0; i < 1000000; i++) // for문을 이용해 프로그래스바의 값을 변경해 줍니다
            {
                progressBar1.Value = i;
            }
        }


위 코드대로 하면 이론상 문제 없을거 같습니다. 컴파일 역시 에러가 나지 않구요!!!

하지만 버튼을 클릭하면 예외가 발생합니다!!

"크로스 스레드 작업이 잘못되었습니다. 'progressBar1'컨트롤이 자신이 만들어진 스레드가 아닌 스레드에서 액세스 되었습니다."

이것이 무슨 예외인가 하면 윈폼 컨트롤들은 스레드에 안전합니다. 안전하다는건 다시말해 서로다른 두개의 쓰레드가 

컨트롤들을 동시에 접근할 수 없다는 얘기 입니다.  

그럼 여기서 의문!! 아니 나는 코드상에서 스레드로 호출한 메소드 말고는 progressBar에 접근하는 코드를 작성한

적이 없는데 무슨말이냐!!. 
라고 생각이 들수도 있습니다. 

 하지만 우리는 직접 작성은 안했지만 이미 프로그래스바를 접근하는 코드를 작성했죠! 

바로 프로그래스바를 생성해주는 코드 입니다.

Visual Studio(이하 VS)는 우리가 컨트롤을 폼위에 올려놓으면 자동으로 디자인 코드를 생성해주죠  그리고 폼이 

실행될때 그 디자인 코드를 메인쓰레드가 실행하게 됩니다.  그리고 제 생각입니다만(추측) 일반 클래스 객체들과는 

달리 윈폼 컨트롤 들은 이벤트들이 발생하고 이벤트를 수신해야 하기에 자신을 생성한 쓰레드만 접근이 가능하도록 

한것인것 같습니다.(실제로 다른 컨트롤이 아닌 클래스들은 여러 쓰레드가 접근해도 문제없습니다. 자원공유문제만 해결하면요 ㅎ)

그럼 이제 이것을 해결해야 되는데 해결 방법은 2가지가 있습니다

첫번쨰 CheckForIllegalCrossThreadCalls 속성을 사용해서 크로스스레드 예외가 발생하지 않도록 하고 컨트롤에 

메인쓰레드 말고 다른 스레드도 접근할 수 있도록 한다! 하지만 이방법은 별로 추천되지 않는 방법입니다. 컨트롤들을 여러 

쓰레드가  접근해서 값을 변경한다면 프로그램이 꼬일수도 있고 뭐.. 여러가지 문제가 발생할 가능성이 있기 때문이죠
(자세히는 저도 잘...)

두번째 방법은 Contorl.Invoke 메소를 이용하는 것입니다.  Invoke메소드는 Delegate를 파라미터로 받는데요.

이 델리게이트가 가리키는 메소드를 Control를 생성한 쓰레드가 호출하게 됩니다. 다시 말해 델리게이트가 가르키는

메소드에 컨트롤을 접근하는 코드를 넣으면, 그 메소드를 컨트롤을 생성한 쓰레드가 실행하기에 크로스 스레딩이 발생

하지 않는 것입니다. 

//코드3
 private void button1_Click(object sender, EventArgs e)
 {
            Thread thread1 = new Thread(new ThreadStart(ThreadGOGO));
            thread1.Start();  //ThreadGOGO메소드를 새로운 쓰레드가 실행합니다
 }
 
private void ThreadGOGO()
 {
            // MethodInvoker 델리게이트는 기본적으로 Void () 메소드 형식의 델리게이트 입니다
            // Void () 형식의 메소드를 쓰려면 따로 만들 필요 없이 그냥 사용하는게 좋겠죠?
            progressBar1.Invoke(new MethodInvoker(ProgValueSetting));
  }

  private void ProgValueSetting()
 {
            progressBar1.Minimum = 0;
            progressBar1.Maximum = 1000000;  

            for (int i = 0; i < 1000000; i++) 
            {
                progressBar1.Value = i;
            }
 }


그래서 바로 Invoke를 써서 코딩해 봤습니다. 그리고 실행해 보았습니다!!!

역시 예외는 발생하지 않습니다!!! 하지만 이게 모야 코드1과  같이 프로그래스바 증가하는 동안 프로그램이 먹통이 됩니다.

이유가 뭘까요? Invoke메소드가 무엇을 하는지 생각해 봅시다 progressBar를 만든 쓰레드로 ProgValueSetting메소드를 

호출 하였습니다. 감이 오셨나요? 눈치 채셨나요?  이코드에 대한 오류는 ProgValueSetting메소드에 있습니다. 이메소드에 

반복문 바로 For문을 사용하는게 문제 였습니다. 결과적으로 메인쓰레드가 코드1과 같이 반복문을 실행하게 되어 다른 

이벤트들을 처리하지 못하게 된것입니다. 

그래서 바로 수정해보았습니다.

//코드4 
private void button1_Click(object sender, EventArgs e)
 {
            progressBar1.Minimum = 0; //크로스 쓰레딩을 피하기 위해 설정은 메인쓰레드가 실행하는 곳에 넣었습니다
            progressBar1.Maximum = 1000000;  
            
            Thread thread1 = new Thread(new ThreadStart(ThreadGOGO));
            thread1.Start();  //ThreadGOGO메소드를 새로운 쓰레드가 실행합니다
 }

//이번엔 파라미터를 하나 넘겨줘야 하기에 델리게이트를 직접 정의 했습니다
delegate void ProgvarCall(int var);

private void ThreadGOGO()
{
            for (int i = 0; i < 1000000; i++) 
            {    //Control.Invoke는 델리게이트와 param Object[]형식을 받는 오버로드된 메소드가 있습니다.
     // param Object[]파라미터는 델리게이트의 파라미터를 받아서 넘겨주죠 여기선 i값을 Object[]형식으로
                 // 만들어서 넘겨주었습니다
                progressBar1.Invoke(new ProgvarCall(ProgValueSetting) ,new object[]{i} );
            }
 }

private void ProgValueSetting(int var)
{
            progressBar1.Value = var;
 }


좋아 이대로 실행해 봅시다!! 예! 먹통이 되지 않고 잘 실행됩니다~ 코드3과 틀린점은 바로 연산을 하는 부분은

새로 생성한 쓰레드가 실행하고 컨트롤의 값이 바뀔때만 Invoke를 사용해서 바꿔준다는 거죠
.  결과적으로 

메인쓰레드는 본연의 일을 하며 대기상태에 있고 새로만든 쓰레드가 포문을 돌며 메인 쓰레드를 잠깐 잠깐 호출해 주는 

것입니다. 그리고 한가지 더 말하자면 progressBar1.Invoke를 하지 않고 this.Invoke를 해도 문제가 없습니다 생각해보면

this, 즉 폼을 만든 쓰레드나 progressBar1를 만든 쓰레드나 똑같은 메인쓰레드 이기 때문이죠!!. 한마디로 자신이 어떤

쓰레드를 호출해야 되는지 생각하고 쓰는것이 중요하다고 생각됩니다.~!!

그런데 이렇게 해보니 프로그램이 먹통이 되진 않으나 조금 버벅이는게 느껴지시죠? 그건 새로운 쓰레드가 

For문을 돌며 Invoke를 마구 호출해줘서 입니다. 메인쓰레드가 다른 이벤트를 대기 하긴 하지만 엄청난 속도를 자꾸 자신을 

부르니 정신이 없어 버벅거리는 거죠 그럴땐 progressBar1.Invoke 밑에 Thread.Sleep(값)구문을 넣어주면 메인쓰레드가 

한숨 돌려 버벅이는게 줄어듭니다. 대신 프로그래스바 증가 속도가 느려지겠죠? 대신 프로그래스바 맥스값을 낮춰주면 

또 문제 없죠!! 이런 수치를 맞추는 것은  각자의 판단에 맡기도록 하겠습니다 

그리고 하나 더 생각해 볼것이 프로그래스바가 증가하는 가운데 버튼을 한번더 클릭해 보면 위의 코드대로 라면

쓰레드가 하나 더 생겨 또 Invoke를 마구 호출해서 프로그래스바 값은 정신못차릴 걸로 예상됩니다~ 

윈폼 다중 쓰레드를 이용할때는 이런것도 염두해 두셔야 합니다~ 모두 쉽게 해결하실수 있을걸로 예상됩니다.

이상 글을 마치도록 하겠습니다. 

잘못된 내용이나 궁금한점이 있으시면 코멘트 남겨주시면 꼭 답변 해 드리도록 하겠습니다

좋은 하루 되세요~


출처: http://chanun.tistory.com/entry/Thread를-이용한-윈폼-프로그래밍-크로스-스레딩?category=280938 [차넌's]

'개발 > C#' 카테고리의 다른 글

C#클래스 분리  (3) 2017.12.26
C#에서 dll import 하기  (0) 2017.12.14
C# 폼간에 전역변수 사용하기  (0) 2017.12.14
디버깅 오류 - System.BadImageFormatException  (0) 2017.12.13
모달리스 다이얼로그  (0) 2017.12.13
모달 다이얼로그  (0) 2017.12.13
c# 쓰레드2  (0) 2017.12.07
c# 쓰레드  (0) 2017.12.07