'멀티 스레드'에 해당되는 글 2건

  1. 2018.07.17 C# 멀티스레드 공유 리소스 동기화
  2. 2018.07.15 C# 멀티 태스킹 구현, 비동기 태스크

멀티스레드에서 하나의 자원에 접근할 때 동기화 이슈가 발생할 수 있습니다. 

멀티스레드에 관한것은 아래 링크를 참고하자.

2018/07/15 - [닷넷,C#,.NET] - C# 멀티 태스킹 구현, 비동기 태스크


그래서 여러 스레드가 하나에 자원에 접근할 때 하나가 해당 자원을 점유하고 있으면 다른 스레드에서는 접근을 못하고 대기하게 해야한다.


 아래 예제는 변수 num 에 두개으 스레드가 접근하여 하나의 스레드(MethodA)는 num값을 하나 증가시키고 다른 스레드(MethodB)는 num 값을 감소시킨다.

그래서 5만번씩 증가와 감소를 하면 num의 최종값은 0이되야 한다. 하지만 실제 값은 0이 아닌 다른 값이 나올때가 많다. 매번 값은 다르다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static int num = 0;
        static void MethodA()
        {
            for (int i = 0; i < 50000; i++)
            {                
                    num++;                
            }
        }
        static void MethodB()
        {
            for (int i = 0; i < 50000; i++)
            {
                num--;
            }
        }
 
        static void Main(string[] args)
        {
            Stopwatch watch = Stopwatch.StartNew(); //시간 측정 시작
            Task aTask = Task.Factory.StartNew(MethodA);
            Task bTask = Task.Factory.StartNew(MethodB);
            Task.WaitAll(new Task[] { aTask, bTask });
            watch.Stop(); //시간 측정 완료
            Console.WriteLine("소요시간 : " + 
                            watch.ElapsedMilliseconds + "s");
            Console.WriteLine("num : " + num);
        }
    }
}
cs



 그래서 이와 같은 동기화 문제를 해결하기 위해서는 lock으로 num을 접근하는 코드에 넣어서 해결할 수 있다.

 아래 예제는 동기화 이슈를 해결한 코드이다.

 물론 소요시간은 위에 예제보다 더 많이 걸리게 된다. lock이 걸리면 다른 스레드는 대기하기 때문에 시간이 오래 걸리는 것이다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static int num = 0;
        static object conch = new object();
        static void MethodA()
        {
            for (int i = 0; i < 50000; i++)
            {
                lock (conch)     //잠금
                {
                    num++;
                }    //풀림
            }
        }
        static void MethodB()
        {
            for (int i = 0; i < 50000; i++)
            {
                lock (conch)     //잠금
                {    
                    num--;
                }    //풀림
            }
        }
 
        static void Main(string[] args)
        {
            Stopwatch watch = Stopwatch.StartNew(); //시간 측정 시작
            Task aTask = Task.Factory.StartNew(MethodA);
            Task bTask = Task.Factory.StartNew(MethodB);
            Task.WaitAll(new Task[] { aTask, bTask });
            watch.Stop(); //시간 측정 완료
            Console.WriteLine("소요시간 : " + watch.ElapsedMilliseconds + "s");
            Console.WriteLine("num : " + num);
        }
    }
}
 
cs


 이런 동기화 이슈가 발생하는 이유는 A스레드에서 변수 num값을 읽어와서 증가시킨 다음에 다시 num변수에 값을 쓴다. A스레드가 num값을 읽어간 동안 스레드 B가 num값을 다시 읽어가면 원하지 않는 값을 쓰게 된다.

 C# 코드로는 num++ 한줄이지만 어셈블리 코드로는 여러줄이 되게 된다. 그래서 이러한 문제를 lock 하는 방법 말고 Interlocked 타입을 이용해서 값을 증가하거나 감소하는 코드를 대신해서 사용하면 이런 문제를 해결할 수 있다.

 아래 예제를 보자. 위의 예제와 같은 결과를 얻을 수 있다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static int num = 0;
        static object conch = new object();
        static void MethodA()
        {
            for (int i = 0; i < 50000; i++)
            {
                Interlocked.Increment(ref num);
            }
        }
        static void MethodB()
        {
            for (int i = 0; i < 50000; i++)
            {
                Interlocked.Decrement(ref num);
            }            
        }
 
        static void Main(string[] args)
        {
            Stopwatch watch = Stopwatch.StartNew(); //시간 측정 시작
            Task aTask = Task.Factory.StartNew(MethodA);
            Task bTask = Task.Factory.StartNew(MethodB);
            Task.WaitAll(new Task[] { aTask, bTask });
            watch.Stop(); //시간 측정 완료
            Console.WriteLine("소요시간 : " + watch.ElapsedMilliseconds + "s");
            Console.WriteLine("num : " + num);
        }
    }
}
cs

Posted by 꿈만은공돌
,

C#에서 비동기로 병렬 처리하는 방법에 대해서 알아보겠다.


1. 일반적인 함수호출(단일 스레드)

아래 코드는 3개의 함수(FuncA, FuncB, FuncC)를 순서대로 호출하는 코드이다.

세가지의 함수가 단일스레드로 호출하는데 총 걸리는 시간을 측정하여 출력한다. 


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace T
{
    class Program
    {
        static void FuncA()
        {
            Console.WriteLine("A함수 시작");
            Thread.Sleep(1000);
            Console.WriteLine("A함수 끝");
        }
        static void FuncB()
        {
            Console.WriteLine("B함수 시작");
            Thread.Sleep(2000);
            Console.WriteLine("B함수 끝");
        }
        static void FuncC()
        {
            Console.WriteLine("C함수 시작");
            Thread.Sleep(3000);
            Console.WriteLine("C함수 끝");
        }
        static void Main(string[] args)
        {
            var timer = Stopwatch.StartNew(); //시간측정 시작
            FuncA();           //A함수 호출
            FuncB();           //B함수 호출
            FuncC();           //C함수 호출
            timer.Stop();      //시간 측정 끝
            Console.WriteLine(timer.ElapsedMilliseconds + "s");
        }
    }
}
 
 
cs

- 출력 결과 -

A함수 시작

A함수 끝

B함수 시작

B함수 끝

C함수 시작

C함수 끝

6005s



2. 비동기 멀티 테스크 만들기

Thread 클르스를 이용하면 스레드를 만들고 관리할 수 있다. 하지만 이 Thread 클레스는 직접 사용하기가 까다로운 점이 많다.

그래서 C# 4.0부터 추가된 Task 클래스를 사용하면 스레드를 쉽게 생성하고 관리 할 수 있다.

아래 예제는 비동기로 3가지 함수를 각각 실행한다.

Task 클래스를 사용하여 객체를 선언하고 Start함수로 태스크를 시작한다. 그러면 스레드가 특정한 순서 없이 동시에 실행이 된다. 그래서 실행순서는 보장하지 않고 끝나는 시점은 딜레이가 가장 짧은 FuncA부터 종료하게 된다. Wait 함수로 해당 태스크가 종료될때까지 해당 코드에서 대기한다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static void FuncA()
        {
            Console.WriteLine("A함수 시작");
            Thread.Sleep(1000);
            Console.WriteLine("A함수 끝");
        }
        static void FuncB()
        {
            Console.WriteLine("B함수 시작");
            Thread.Sleep(2000);
            Console.WriteLine("B함수 끝");
        }
        static void FuncC()
        {
            Console.WriteLine("C함수 시작");
            Thread.Sleep(3000);
            Console.WriteLine("C함수 끝");
        }
        static void Main(string[] args)
        {
            Task taskA = new Task(FuncA); //태스크 객체
            Task taskB = new Task(FuncB);
            Task taskC = new Task(FuncC);
            var timer = Stopwatch.StartNew(); //시간측정 시작
            taskA.Start();         //태스크 A시작
            taskB.Start();         //태스크 B시작
            taskC.Start();         //태스크 C시작
            taskA.Wait();         //태스크 A 끝날때까지 대기
            taskB.Wait();         //태스크 B 끝날때까지 대기
            taskC.Wait();         //태스크 C 끝날때까지 대기
            timer.Stop();         //시간 측정 끝
            Console.WriteLine(timer.ElapsedMilliseconds + "s");
        }
    }
}
cs


- 출력 결과 -

C함수 시작

B함수 시작

A함수 시작

A함수 끝

B함수 끝

C함수 끝

3008s



3. 다양한 방법으로 태스크 만들기

위의 예제에서는 Task객체를 각각 생성해서 Start함수를 호출하였지만 아래 예제에서는 Task.Factory.StartNew(FuncA) 와 같은 방식이나 Task.Run(new Action(FuncB))와 같은 방식으로 객체를 생성과 동시에 Task를 시작시킨다.

그리고 각각의 Task들에 Wait함수를 호출하여 종료를 대기했지만 아래 예제에서는 Task 배열을 만들고 Task.WaitAll() 함수에 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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static void FuncA()
        {
            Console.WriteLine("A함수 시작");
            Thread.Sleep(1000);
            Console.WriteLine("A함수 끝");
        }
        static void FuncB()
        {
            Console.WriteLine("B함수 시작");
            Thread.Sleep(2000);
            Console.WriteLine("B함수 끝");
        }
        static void FuncC()
        {
            Console.WriteLine("C함수 시작");
            Thread.Sleep(3000);
            Console.WriteLine("C함수 끝");
        }
        static void Main(string[] args)
        {
            var timer = Stopwatch.StartNew(); //시간측정 시작
            Task taskA = Task.Factory.StartNew(FuncA);  //쓰레드 A 생성 후 시작
            Task taskB = Task.Run(new Action(FuncB));   //쓰레드 B 생성 후 시작
            Task taskC = Task.Factory.StartNew(FuncC);  //쓰레드 C 생성 후 시작
            Task[] tasks = {taskA, taskB, taskC};       //쓰레드 종료까지 대기
            Task.WaitAll(tasks);
            timer.Stop(); //시간 측정 끝
            Console.WriteLine(timer.ElapsedMilliseconds + "s");
        }
    }
}

cs


- 출력 결과 - 

C함수 시작

A함수 시작

B함수 시작

A함수 끝

B함수 끝

C함수 끝

3008s



4. A스래의 결과를 B스래드에 넘겨주기

A스레드를 실행하여 결과를 받아서 B스레드를 실행하면서 값을 넘겨주는 예제이다.

ContinueWith 함수를 사용한다.

TestA 함수를 StartNew 함수로 태스크를 만들어서 스래드를 실행하고 그 결과 값을 ContinueWith 함수로 TestB에 Result로 결과 값을 넘겨준다. 값을 받아 새로운 스레드를 만들어 실행한다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
 
namespace T
{
    class Program
    {
        static int TestA()
        {
            Console.WriteLine("TestA Start");
            Thread.Sleep(1000);
            Console.WriteLine("TestA End");
            return 10;
        }
        static int TestB(int i)
        {
            Console.WriteLine("TestB Start");
            Thread.Sleep(1000);
            Console.WriteLine("TestB End");
            return i + 10;
        }
        static void Main(string[] args)
        {
            var test = Task.Factory.StartNew(TestA)
                                .ContinueWith(task=>TestB(task.Result));
            Console.WriteLine("result : " + test.Result);
        }
    }
}
 
cs


- 출력 결과 -

TestA Start

TestA End

TestB Start

TestB End

result : 20


5. 중첩 태스크(Task)

아래 예제는 2개의 Task가 중첩되서 실행하는 코드이다.

Inner 태스크가 3초 딜레이가 있어 Outer 태스크가 먼저 종료되고 Inner 태스크가 종료된다. 

Inner 태스크가 완료되기를 기다리지 않는다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static void Main(string[] args)
        {
            
            var outer = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Outer task 시작");
                var inner = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Inner task 시작.");
                    Thread.Sleep(2000);
                    Console.WriteLine("Inner task 종료.");
                });            
            });
            outer.Wait();
            Console.WriteLine("Outer task 종료.");
            Console.ReadLine();
        }
    }
}
cs


- 출력 결과 -

Outer task 시작

Outer task 종료.

Inner task 시작.

Inner task 종료.


Inner 태스크가 완료되기를 기달리고 Outer 태스크가 완료되게 하려면 Inner 태스크에 TaskCrationOption의 AttachedToParent 값을 추가해준다.

아래는 그 예제이다. Inner 클래스가 먼저 종료되는 것을 볼 수 있다.


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
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
 
namespace T
{
    class Program
    {
        static void Main(string[] args)
        {
            
            var outer = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Outer task 시작");
                var inner = Task.Factory.StartNew(() =>
                {
                    Console.WriteLine("Inner task 시작.");
                    Thread.Sleep(2000);
                    Console.WriteLine("Inner task 종료.");
                }, TaskCreationOptions.AttachedToParent);            
            });
            outer.Wait();
            Console.WriteLine("Outer task 종료.");
            Console.ReadLine();
        }
    }
}
 
cs


- 출력 결과 -

Outer task 시작

Inner task 시작.

Inner task 종료.

Outer task 종료.


멀테스레드에서 동기화하는 방법은 아래 링크를 참고하자

2018/07/17 - [닷넷,C#,.NET] - C# 멀티스레드 공유 리소스 동기화



참고 자료 : 크로스 플랫폼 개발을 위한 C# 7과 닷넷 코어 2.0 책

Posted by 꿈만은공돌
,