본문 바로가기
[ Program ]/c#스터디

57. async / await

by 관이119 2022. 3. 29.

async 와 await 에 대해서 찾아보면 대부분의 포스터 들은 비동기를 위한 키워드라고 적어놨다.

그런데 잘생각해보면 비동기보다는 부분적으로 동기하기 위한 키워드가 더 맞는 표현이라고 생각한다.

 

[예제소스]

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#define type2
 
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 WindowsFormsApp13
{
    public partial class Form8 : Form
    {
        public Form8()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            //labelchange();
            labelchange();
 
            MessageBox.Show("Test1");
        }
 
        public async void labelchange()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label1, i.ToString());
                }
            });
 
            label_text_change(label1, new Random().Next(500).ToString());
        }
 
        public void labelchange_temp1()
        {
            for (int i = 1; i <= 500; i++)
            {
                Thread.Sleep(10);
                label_text_change(label1, i.ToString());
            }
 
            label_text_change(label1, new Random().Next(500).ToString());
        }
 
        private async void button2_Click(object sender, EventArgs e)
        {
            await label2change();
 
            MessageBox.Show("Test2");
        }
 
        public async Task label2change()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label2, i.ToString());
                }
            });
 
            label_text_change(label2, new Random().Next(500).ToString());
        }
 
        public void label_text_change(Label lb, string i)
        {
            if(lb.InvokeRequired)
            {
#if type1
                Action ac = new Action(delegate () { lb.Text = i; });
                lb.Invoke(ac);
#elif type2
                lb.Invoke(new Action(delegate () { lb.Text = i; }));
#elif type3
                lb.Invoke((Action)delegate () { lb.Text = i; });
#elif type4
                lb.Invoke((MethodInvoker)delegate () { lb.Text = i; });
#elif type5
                lb.Invoke((Action<string>)delegate (string s) { lb.Text = s; }, i);
 
#endif
            }
            else
            {
                lb.Text = i;
            }
        }
 
        private void button3_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label3, i.ToString());
                }
            });
 
            label_text_change(label3, new Random().Next(500).ToString());
        }
    }
}
 
cs

 

그림1

 

 

위의 그림1과 예제소스에 내용을 파악할 수 있는 예제를 올려놨다.

 

버튼1클릭이벤트부터 살펴보자.

그냥 소스를 확인보면 labelchange 함수를 호출하고 그뒤에 Test1 메세지박스를 팝업 시키기 때문에 labelchange 함수가 끝나고 메세지 박스를 팝업시키지 않을까 생각할수도 있다.

그런데 labelchange 함수를 들어가서 자세히 보면 task 로 쓰레드를 생성해 라벨의 텍스트를 변경 시키기 때문에 메세지박스와 상관없이 숫자가 변경될거라는걸 알수 있다.

labelchange 함수를 보면 await 라는걸 사용했다.

await 는 해당부분에서 쓰레드를 대기한다는 의미인데 async 함수 내에서만 사용 가능하다.

labelchange 함수를 찬찬히 뜯어보면 쓰레드로 1부터 500 까지 라벨값을 순서대로 변경해준 후 쓰레드가 완료되면 label_text_change 함수를 랜덤값으로 한번 호출한다.

실제 작동형태도 그런식으로 작동한다.

즉, 숫자가 1~500까지 변경될때까지 쓰레드로 작동하지만 메인쓰레드에서 완전히 별개로 분리되지않고 다음 로직을 기다릴수 있다는것이다.

 

그림2

 

그림2가 await에 대한 설명이 되겠다.

내용을 살펴보고 실제 작동내용을 확인해보면 결국 await 는 쓰레드를 동기화하는것처럼 사용할수 있는 키워드인것이다.

이렇게 보면 동기적으로 작동하는것과 뭐가 다른가 하는 의문을 가질수 있다.

 

 

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
private void button1_Click(object sender, EventArgs e)
{
    //labelchange();
    labelchange_temp1();
 
    MessageBox.Show("Test1");
}
 
public async void labelchange()
{
    await Task.Run(() =>
    {
    for (int i = 1; i <= 500; i++)
    {
        Thread.Sleep(10);
        label_text_change(label1, i.ToString());
    }
    });
 
    label_text_change(label1, new Random().Next(500).ToString());
}
 
public void labelchange_temp1()
{
    for (int i = 1; i <= 500; i++)
    {
    Thread.Sleep(10);
    label_text_change(label1, i.ToString());
    }
 
    label_text_change(label1, new Random().Next(500).ToString());
}
cs

 

 

버튼1을 클릭했을때 소스를 위와 같이 바꿔보자

그리고 작동시키면 실시간으로 라벨텍스트가 변경되지않고 이상하게 작동되는것을 알 수 있다.

정확한 내부내용은 전부 알순 없지만 컨트롤이 업데이트 되려면 다른쓰레드가 필요함을 알수 있다.

그래서 그림2처럼 단일쓰레드와 행동이 무슨차이가 있을지 모르겠는 저런 형태더라도 쓰레드가 필요하고 await 를 사용하여 해당 행동을 정상작동 하게 할 수 있다.

 

다음으로 버튼2를 클릭해보면 일반적으로 생각하는것처럼 label2change 함수가 끝나고 메세지박스가 팝업됨을 알수 있다.

저부분역시 await 로 해당 함수를 종료시까지 대기 시켜놨기에 가능한 내용이다.

 

버튼3 클릭시의 내용같은경우는 실수하기 쉬운 부분을 내용으로 만들어놓은 것이다.

실제로 실행하면 task 로 쓰레드를 만들어서 label 의 텍스트를 변경하는 도중에 label_text_change 함수가 실행된다.

즉, 쓰레드와 label_text_change  함수가 서로 충돌하면서 작동하기때문에 쓰레드의 실행이 오래걸릴경우 쓰레드에서 실행되는 내용이 마지막으로 적용될것이고 label_text_change  함수가 쓰레드 전체의 실행시간을 오버될경우 label_text_change  함수가 마지막으로 적용될것이다.

위예제의 경우는 쓰레드가 일부 실행되고 중간에 label_text_change 함수가 실행되고 그뒤에 쓰레드의 나머지 내용이 실행되고 그런형태가 될것이다.

전체 내용이 어떻게 실행될지 정확하게 예측할수 없다는 의미가 되는것이고 그런 코드는 실제로 사용하기가 힘든상태가 된다.

 

label_text_change함수의 type 들은 전부 같은 행동을 한다.

해당부분만 아래에 따로 분리했다.

여기서 다시 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void label_text_change(Label lb, string i)
{
    if(lb.InvokeRequired)
    {
#if type1
    Action ac = new Action(delegate () { lb.Text = i; });
    lb.Invoke(ac);
#elif type2
    lb.Invoke(new Action(delegate () { lb.Text = i; }));
#elif type3
    lb.Invoke((Action)delegate () { lb.Text = i; });
#elif type4
    lb.Invoke((MethodInvoker)delegate () { lb.Text = i; });
#elif type5
    lb.Invoke((Action<string>)delegate (string s) { lb.Text = s; }, i);
 
#endif
    }
    else
    {
    lb.Text = i;
    }
}
cs

이함수는 label 의 text 를 변경해주는 역할을 하는 함수인데 메인쓰레드가 아닌 다른쓰레드에서는 InvokeRequired 로 변경가능 여부를 확인후 텍스트를 변경해주는 작업을 하고 , 아래 else 부분에서는 동일 쓰레드에서 호출할때 호출될부분을 작성해 놨다.

type1 ~ type5 까지 전부 같은 행동을 하는것들인데 이전에도 설명한 내용이기 때문에 천천히 살펴보면 알수 있을거라고 생각한다.

Invoke 에서 첫번째 파라미터로 Delegate 를 받는데 실제로 해보면 액션이나 메소드인보커로 형변환을 해줘야 delegate 를 파라미터로 던질수 있다.

차이점은 아래 글을 읽어보자.

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=dontcryme&logNo=30188766487 

 

[끄적노트] Delegate와 delegate 그대들은 누구인고?

오랜만에 블로그에 쓰는 글인 것 같다.... 갑자기 생각난 김에...정리도 할겸... 여튼, C#에서 Invoke 호...

blog.naver.com

 

 

async 에 대해 좀더 살펴보자.

async 키워드는 void , Task , Task<> 를 반환값으로 사용할수 있다.

다음 코드를 보자.

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
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 WindowsFormsApp13
{
    public partial class Form9 : Form
    {
        public Form9()
        {
            InitializeComponent();
        }
 
        private async void button1_Click(object sender, EventArgs e)
        {
            Task t = rtn_task();
 
            MessageBox.Show(await rtn_taskstr());
 
            rtn_void();
 
            await t;
 
            MessageBox.Show("test end");
 
 
        }
 
        private async void rtn_void()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 2; i++)
                {
                    Thread.Sleep(1000);
                }
            });
 
            MessageBox.Show("void");
        }
 
        public async Task rtn_task()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 5; i++)
                {
                    Thread.Sleep(1000);
                }
            });
 
            MessageBox.Show("task");
        }
 
        private async Task<string> rtn_taskstr()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 7; i++)
                {
                    Thread.Sleep(1000);
                }
            });
 
            return "taskstring";
        }
    }
}
 
cs

 

rtn_void 함수와 rtn_task 함수 , rtn_taskstr 함수는 내용은 동일한데 반환값만 다르다.

버튼클릭시 해당함수들을 호출하는 부분을 하나씩 살펴보자.

 

Task t = rtn_task();
MessageBox.Show(await rtn_taskstr());
rtn_void();
await t;
MessageBox.Show("test end");

 

rtn_void 함수같은경우는 반환값이 void 이기때문에 당연히 반환값이 없다.

await 키워드를 사용할수도 없다.

이렇게 반환값이 없으면 해당 함수를 호출만 할수 있고 해당 함수에 관여할수가 없다.

그냥 일반 적으로 생각해봐도 당연한 내용이다.

아무것도 반환하지 않겠다 선언했으니 내부적으로도 함수에 외부로 통하는 어떠한 커넥션을 만들지는 않을것이다.

하지만 리턴하는 결과값이 있는 함수같은경우는 뭔가 조작하지않아도 그 함수와 커넥션을 유지하는 보이지않는 뭔가가 있어야 결과값을 받을수 있을것이라고 짐작할 수 있다.

그렇게 생각을 해보면 void 를 반환할때는 await 를 키워드를 쓸수 없다는 것이 이해가 될것이다.

await 를 쓸수 없으면 특정 지점에서 작업을 대기할 수 없게 된다.

그래서 메인쓰레드의 동작과 관련이 없는곳에서만 void 를 반환한다고 생각하면 되겠다.

결국 async 와 자주 사용될 반환값은 Task 와 Task<> 가 되겠다.

 

반환값이 있으니 외부에서 반환값을 받을수도 있다.Task 를 리턴 받는것은 쓰레드의 동작전체의 시점을 리턴 받는다고 생각하면 되겠다.즉, 쓰레드내에서 무언가 작업들을 하는것들을 설계도를 가지고 있다가 호출하는 순간 그 내용들을 통채로 시작하는것이다.생각해보면 함수 호출하는것과 같은데 동기적으로 작동하는 내용이 아닌 비동기형태의 작동시점을 리턴받는것이다.

Task t = rtn_task(); 

이와같이 Task 전체를 반환값으로 받아서 아래부분에 호출한것처럼 await t; 처럼 호출하여 원하는 위치에서 해당 태스크(쓰레드)를 실행시킬수 있다.

 

 

참고로 Thread 에서도 await 와 비슷한 형태로 만들수 있는데 아래 예제를 보자.

 

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#define type2
 
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 WindowsFormsApp13
{
    public partial class Form8 : Form
    {
        public Form8()
        {
            InitializeComponent();
        }
 
        private void button1_Click(object sender, EventArgs e)
        {
            //labelchange();
 
            CountdownEvent countdown = new CountdownEvent(2);
 
            Thread t = new Thread(()=> labelchangethread(countdown));
            t.Start();
            Thread t2 = new Thread(() => labelchangethread2(countdown));
            t2.Start();
 
            Thread tcheck = new Thread(() => threadendcheck(countdown));
            tcheck.Start();
 
            MessageBox.Show("Test1");
        }
 
        public async void labelchange()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label1, i.ToString());
                }
            });
 
            label_text_change(label1, new Random().Next(500).ToString());
        }
 
        public void labelchangethread(CountdownEvent countdown)
        {
            for (int i = 1; i <= 500; i++)
            {
                Thread.Sleep(10);
                label_text_change(label1, i.ToString());
            }
 
            label_text_change(label1, new Random().Next(500).ToString());
 
            countdown.Signal();
        }
 
        public void labelchangethread2(CountdownEvent countdown)
        {
            for (int i = 1; i <= 500; i++)
            {
                Thread.Sleep(20);
                label_text_change(label2, i.ToString());
            }
 
            label_text_change(label2, new Random().Next(500).ToString());
 
            countdown.Signal();
        }
 
        public void threadendcheck(CountdownEvent countdown)
        {
            while (true)
            {
                if (countdown.IsSet == true)
                {
                    MessageBox.Show("thread end");
                    break;
                }
                Thread.Sleep(10);
            }
        }
 
        private async void button2_Click(object sender, EventArgs e)
        {
            await label2change();
 
            MessageBox.Show("Test2");
        }
 
        public async Task label2change()
        {
            await Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label2, i.ToString());
                }
            });
 
            label_text_change(label2, new Random().Next(500).ToString());
        }
 
        public void label_text_change(Label lb, string i)
        {
            if(lb.InvokeRequired)
            {
#if type1
                Action ac = new Action(delegate () { lb.Text = i; });
                lb.Invoke(ac);
#elif type2
                lb.Invoke(new Action(delegate () { lb.Text = i; }));
#elif type3
                lb.Invoke((Action)delegate () { lb.Text = i; });
#elif type4
                lb.Invoke((MethodInvoker)delegate () { lb.Text = i; });
#elif type5
                lb.Invoke((Action<string>)delegate (string s) { lb.Text = s; }, i);
 
#endif
            }
            else
            {
                lb.Text = i;
            }
        }
 
        private void button3_Click(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                for (int i = 1; i <= 500; i++)
                {
                    Thread.Sleep(10);
                    label_text_change(label3, i.ToString());
                }
            });
 
            label_text_change(label3, new Random().Next(500).ToString());
        }
    }
}
 
cs

button1 클릭 이벤트에 있는 내용이 그것인데 CountdownEvent 라는 클래스를 사용하여 만들었다.

내용은 한번만 천천히 읽어보면 알수 있는 내용이라 설명하지않고 넘어가겠다.

추가적인 사용법은 구글에서 검색해보자.

 

***숙제 : async 와 await 를 사용하여 로딩화면 후 로또 생성을하는 프로그램을 만들어보자.

'[ Program ] > c#스터디' 카테고리의 다른 글

59. lock / Monitor / Mutex  (0) 2022.05.11
58. Threadpool / BackgroundWorker / Parallel  (0) 2022.05.11
56. 쓰레드2(Task)  (0) 2022.02.27
55. Action/Func  (0) 2022.02.22
54. 람다식  (0) 2022.02.17

댓글