Programming/C#

[Invoke & BeginInvoke] 1. 다른 Thread 에서 UI 접근하기(2)

BadaGreen_Kim 2018. 8. 29. 15:27

 

 

C# 멀티쓰레드와 Invoke에 관해 정리를 잘해놓으신분이 있어서 C# Multi Thread와 Invoke에 관해 포스팅한다.

 

출처 : http://ddochea.tistory.com/11?category=568955  [또치의 삽질 보관함]

 

 지난 시간엔 Invoke를 사용하여 오래걸리는 작업에 대한 결과를 UI 멈춤 없이 표현하는 방법을 포스트했다. 이런 기능을 통해 데이터베이스에 연결, 조회, 수정과 같은 작업이나, 네트워크 통신에 대한 연결, 송신 및 수신 작업에 대해 다른 스레드로 두고, 그 상태나 결과에 대해 UI에 표현하여, 사용자의 요청에 지체없이 응답할 수 있는 프로그램을 개발하는데 응용할 수 있다. :-)

그러나 때론 UI 자체에서 큰 작업을 진행되는 경우가 있다. 가령 대용량으로 조회된 데이터를 하나의 표(Table) 컨트롤에 표현한다거나, 다량의 데이터를 차트(Chart)로 표현하는 등 표현할 데이터에 의해 수반된 Control 생성인데, 이런 처리는 데이터를 분할하여 반복문을 통해 Invoke를 반복호출하는 기교(技巧)가 필요하게 된다.

그러나 Invoke를 반복호출하여 UI의 멈춤현상은 개선할 수 있다 할지라도 Invoke 호출 이후의 연산이 있을 경우, Inovke 호출이 종료될때까지 연산을 진행할 수 없게 된다. 이를 해결하기위해 사용하는 것이 BeginInvoke 이다.

글로만 설명해선 이해하기 어려우니 직접 소스로 넘어가는게 좋겠다. 아래 이미지와 소스는 지난 포스트에서 다뤘던 2를 x만큼 더하기에서 조금 수정한 프로그램의 완성화면과 소스이다.

 


[그림 1] 수정된 프로그램. 입력 x 를 받으면 2와 3에 대한 연산작업을 수행한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading; // 추가된 namespace
using System.Threading.Tasks;
 
 
 
using System.Windows.Forms;
 
namespace MultiThread
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            long cnt = 0;
 
            if(long.TryParse(textBox1.Text,out cnt))
            {
                DateTime startCalcTime = DateTime.Now;
 
               
                Thread thread = new Thread(new ThreadStart(delegate ()
                {
                   
 
                    this.Invoke(new Action(() => button1.Enabled = false)); // 연속 클릭 방지를 위해 버튼사용 금지
                    var result = Calc(cnt); // 계산작업 (2씩 더하기)
                    this.Invoke(new Action(() =>
                    {
                        label1.Text = result.ToString();
                        Thread.Sleep(5000); // Main Thread 가 임의로 대기하도록 함.
                    }));
                    var result2 = Calc(cnt, 3); // 계산작업 (3씩 더하기)
                    this.Invoke(new Action(() => {
                        label4.Text = result2.ToString();
                        label3.Text = (DateTime.Now - startCalcTime).TotalSeconds + "초";
                        button1.Enabled = true// 작업이 완료되었으므로 버튼사용 허용
                    }));
 
 
 
                }));
                thread.Start();
            }
            else
            {
                label3.Text = "지정한 숫자가 잘못되었습니다.";
                label3.Text = "지정한 숫자가 잘못되었습니다.";
 
 
             
            }
 
 
        }
 
        private long Calc(long cnt, int addValue = 2)
        {
            long result = 0;
 
            for(long i=0; i< cnt; i++)
            {
                result += 2;
            }
 
            return result;
 
        }
 
 
    }
}
 
 
cs

완성한 프로그램에 x 값을 주면 실제 연산 결과와 함께 연산에 걸린 총 시간을 표시한다. 소스 중에 Thread.Sleep(5000); 부분이 있는데 UI 에서 5초정도 걸리는 대형 작업이 진행되는 구간이라 간주한다. UI 에서 5초의 시간이 걸리니 총 결과 시간은 5초+ α 정도 걸리게된다.
환경에 차이를 보일 수 있으나 x를 999999999 로 입력하여 계산하니 12~13초 정도 나왔다.

 


[그림 2] 프로그램 실행 결과

 

그렇다면, Invoke를 BeginInvoke로 변경하면 어떻게 될까? 확인을 위해 전체 소스중 해당 부분의 Invoke를 변경해보도록 하자

 

아까처럼 x에 999999999를 넣고 계산하면 8~9초 정도로 이전보다 4초 정도 단축이 된다.


 


[그림 3] BeginInvoke로 변경한 후 연산결과

 

이유는 Invoke 와 BeginInovke 간 작업에 대한 처리완료 대기여부의 차이가 있기 때문이다. Invoke는 자신이 위임받은 메소드 연산이 모두 완료된 뒤에 Inovke 다음 작업을 진행하도록 대기하지만, BeginInvoke는 MainThread에 위임한 작업을 위임만 시켜둔 채, 다음 작업을 수행한다. 따라서 UI에서 Thread.Sleep(5000)가 진행되는 동안 Calc 연산이 대기하지 않고 연산작업을 했기 때문에 시간이 단축될 수 있었던 것이다.

 


[그림 4] Invoke와 BeginInvoke 의 진행 흐름도

 

위 그림이 정확한 도식은 아니지만 흐름을 이해하는덴 도움이 될 것이다.

Note 1. 단순히 보면 Invoke 보단 BeginInvoke를 호출하는게 더 유리할 수 있다.