Welcome to Parallel C#(11) - 동기 부여.

C# Parallel Programming 2012. 2. 6. 09:00 Posted by 알 수 없는 사용자

우와~!!!! 증말 오랜만입니다. 이게 얼마만이죠? 하하하하하하하하-_-

기다리는 분이 아무도 없다는 건 알지만, 저 자신을 위해서 그냥 한번 호들갑을 떨어봤습니다. 허허허허 원래 제가 쓰는 포스팅이 다 그렇죠. 병렬 프로그래밍에 대해서 정리해놓은 자료가 조금 있었는데요, 매우 늦긴 했지만 그래도 일단 공부하면서 정리해놓은 자료를 그냥 썩히기는 아까워서 조금씩 옮겨 적어볼까 합니다. 그래서 다시 시작하는 거지요! 호호호호 :)

언제나 그렇듯이 제 포스팅은 공부하면서 정리하는 겁니다. 다른 분들이 적으시는 것 처럼 내공이 깊지도 않고, 그다지 실용적이지도 않지요. 의견이 있으시면 거침없이! 하지만 예의는 쪼금 지켜 주시면서! 가르침을 더 해주시면 읽으시는 분들에게 더 도움이 될 겁니다. 짧은 인생 Love & Peace! 그럼 또 초큼씩 풀어 볼까욤? ㅋㅋㅋ


- 화장실이 한 개!!!

4인 가족 기준으로 화장실이 한 개라고 생각을 하면..... 어머나!!! 끔찍해!!! 출근 시간이 되면 화장실에 응가를 때리러 러시아워가 벌어집니다. 먼저 들어간 사람은 느긋하게 다이빙하는 응가에 예술 점수를 매기면서 시간을 보낼 수 있지만, 출근 시간과 이제 그만 포기하자고 하는 장과 협상을 벌이면서 조마조마해야 하죠. 화장실을 원하는 사람은 4명인데, 화장실이 하나이다 보니, 먼저 들어간 사람이 화장실을 선점하고 문을 잠가서 락을 걸어버리면, 화장실을 원하는 다른 사람들은 그 락이 해제 될 때까지 인고의 시간을 보내야 하는 거죠. 어머나 슬픈이야기...ㅠㅠ

그러면 락을 걸지 못하게 하면 효과적인 해결책이 될까요? 일보고 있는데 갑자기 문을 열고 들어와서는 '난 얼굴 씻고 이빨만 닦으련다 변기를 쓰는 것도 아니니 너랑 내가 이 화장실을 평화롭게 공유하면서 각박한 출근 시간에 조그마한 훈훈함이라도 나눠가지지 않겠느냐' 하면 뭐 괘안을 수도 있겠습니다만.... 조금만 더 생각을 해보면 말이죠. 응가라는 건 굉장한 집중이 요구됩니다. 그리고 아무도 보는 사람이 없다는 안도감 속에서야 차분히 진행될 수 있는 일종의 비밀 의식 같은 거죠. 화장실을 공유하는 훈훈함 속에서 응가를 제대로 할 수 는 없습니다. 그리고 세면을 하는 사람 역시 은은하게 풍겨나오는 향기에 정신을 빼앗기다 보면 어서빨리 탈출하고 싶은 욕구에 사로잡히게 되어 세면을 제대로 할 수 없게 됩니다.

즉, 락을 거는 게 가장 효율적이라는 거죠. 여담이지만 그래서 저는 공중화장실에서는 응가를 안하는 편입니다. 정신 사나워서 집중을 할 수 없거든요. :)


- 그래서 필요한 동기화

그래서 프로그래밍을 하면서도 동기화에 신경을 써줘야 합니다. 컴퓨터의 자원을 한정적이고 그걸 호시탐탐 선점하려는 스레드의 욕구를 잘 제어해줘야 하기 때문이죠. 그럼 동기화 없이 스레드의 탐욕에 맡긴 결과가 어떻게 되나 한번 볼까요. 후후후후

using System;

using System.Threading.Tasks;

 

namespace Exam22

{

    class Program

    {

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                sum += 1;

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                sum -= 1;

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

두 개의 스레드를 이용해서, 주 스레드에서는 sum의 값을 1씩 감소 시키고, 추가 스레드에서는 증가 시키도록 해놓은 평범한 예제입니다. 하지만, 서로 뺏고 뺏기는 리얼 야생 탐욕이 살아 숨쉬는 정글의 법칙이 녹아들어 있습니다. 결과를 보시죠.

Result = 17104

계속하려면 아무 키나 누르십시오...

분명히 같은 횟수로 1씩 증가시키고 감소를 시켰는데 이런 결과가 나오다니!!! 매번 실행 할때 마다 다른 값이 나오긴 하지만, 이 결과는 분명 이상합니다. 오래전 포스팅이지만 원자성에 대해서 잠깐 이야기를 한적이 있었는데요, 특정 작업이 실행 될 때 그 작업 외에 다른 작업이 끼어들어서는 안된다는 원칙을 이야기 하는 것입니다. 그렇다면! 위에서 사용중인 +=, -= 연산자는 분명히 원자성을 위반하고 있는 것으로 보입니다.

단순하게 'a += 1' 을 가지고 생각해볼까욤. 이 연산은 몇 가지 단계 나누어 집니다. 우선 a의 값을 가져오고, 가져온 값을 1 증가 시키고, 증가된 값을 다시 a에 대입하는 단계로 나누어 지는 거죠. 그런데 a값을 가져온 직후에 다른 스레드가 a의 값을 변화시킨다면 어떻게 될까요? 이런 거 말로 설명하면 설명하는 사람도 읽는 사람도 불통에 빠지게 됩니다. 현재 sum이라는 변수의 값이 5라고 가정하고 간단하게 표를 통해 설명 해보죠 ㅋ

 증가 스레드(sum += 1 수행) 감소 스레드(sum -= 1 수행) 
   5를 읽어옴
 5를 읽어옴  
 1증가시킴  
 6을 저장  
  1감소시킴 
  4를 저장 

위와 같은 순서로 수행이 된다면?

분명히 1증가와 1감소가 수행되었지만, 결과는 1감소만 수행된 셈이죠. 이런식으로 서로 방해와 방해를 하다보면 이상한 결과가 나오게 되는 겁니다. 그래서 화장실에 락을 걸어서 순서를 정해서 사용하도록 하듯이, 순서를 정해줘야 하는 겁니다. 하하하하 :)

- 모니터로 락을 걸자! 락을 걸자!

자 그럼 아주 간단하게, 모니터로 락을 걸어 볼까요? 준비물은 여러분이 지금 보고 있는 모니터를 사용하면 됩니....는 훼이크고 Monitor라는 클래스를 이용해서 락을 걸어보겠습니다. Enter와 Exit라는 메서드를 이용해서 락을 걸고 빠져나가도록 통제를 하게 되는 데요. 참고로 이렇게 둘 이상의 스레드가 동시에 접근하면 안되기 때문에 락을 걸어야 하는 부분을 임계여역(Critical Section)이라고 합니다. 허허허허. 그럼 Monitor를 사용한 예제를 한번 보시죰.

using System;

using System.Threading.Tasks;

using System.Threading;

 

namespace Exam23

{

    class Program

    {

        readonly static object sync = new object();

        static readonly int count = 10000000;

        static int sum = 0;

 

        static void IncreaseByOne()

        {

            for (int i = 0; i < count; i++)

            {

                bool locked = false;

                Monitor.Enter(sync, ref locked);

                try

                {

                    sum += 1;

                }

                finally

                {

                    if (locked)

                    {

                        Monitor.Exit(sync);

                    }

                }

            }

        }

 

        static void Main(string[] args)

        {

            Task task = Task.Factory.StartNew(IncreaseByOne);

 

            for (int i = 0; i < count; i++)

            {

                bool locked = false;

                Monitor.Enter(sync, ref locked);

                try

                {

                    sum -= 1;

                }

                finally

                {

                    if (locked)

                    {

                        Monitor.Exit(sync);

                    }

                }

            }

 

            task.Wait();

            Console.WriteLine("Result = {0}", sum);

        }

    }

}

자, 코드를 보면 락이 걸려있는지를 판별하기 위해서 readonly static으로 sync라는 변수를 선언하고, 그 변수에 대해서 락을 걸로 해제하는 것으로 특정영역에 대한 락을 걸도록 했습니다. 그리고 Monitor를 통해서 들어가고 나오는 순서를 정해주고 있죠. 만약에 주 스레드에서 sync에 대해서 락을 획득하고 sum을 감소시키는 중이라면, 다른 스레드는 sync에 대한 락이 해제 될 때까지 임계영역에 들어가지 못하고 대기하게 됩니다. 그렇다면? 각자가 원하는 작업을 하는 동안 다른 스레드가 방해를 하지 못하겠죵?

참고로 락을 걸고 해제하는 대상이 되는 sync라는 객체를 보면, readonly이므로 객체의 값을 변경시킬 수 없는 immutable한 객체이고, static이므로 유일하게 존재하게 됩니다. 이렇게 복사본이 없고, 값을 변경시킬 수 없는 객체에 대해서 락을 거는 것이 안전한 방법이지요. 위 코드를 실행하면 다음과 같은 결과가 나오게 됩니다.

Result = 0

계속하려면 아무 키나 누르십시오...

이거시 바로 우리가 기대했던 결과지요. 하하하하하하하하. 아... 좀 불편한 자세로 포스팅을 하고 있으려니 허리가..... 일단 오늘은 여기까지 하겠슴니다 하하하하 :)