C#

스레드 동기화(lock,AutoResetEvent)

윤태영(Coding) 2023. 6. 13. 19:21

스레드를 사용하다 보면, 변수를 스레드끼리 공유해서 사용하는 경우가 있다.

그럴 때, 스레드 동기화를 해주면 둘 이상의 스레드가 서로의 작업을 덮어쓰지 않고 공유 자원에 안전하게 엑세스 할 수 있는 방법이다.

출처 : C#.NET 0.5년차~3년차(파트1)

처음 a값에 변수 5를 가지고 있는데 Thread2에서 a 값을 4로 변경하고 getValue(a)를 통해서 4값을 동기화를 해주면, 4값을 얻을 수 있는데, 만약 동기화가 안되어 있다면 getValue(a)에서 4를 기대했으나, Thread1에 의해서 3으로 변경한 것이 영향을 받게 되어, 3의 값을 얻게 되버린다.

 

Thread2가 a=4;로 선언했으면 Thread2에서 getValue(a)를 했을 시, 4를 얻을 수 있는 영역을 임계영역(Critical section)이라고 한다. 임계 영역은 공유 자원에 Thread Safety하게 접근하는 영역이다.

 

a=5;를 하고

Thread2 에서 a=4;로 변경을 요청한다.

그런데 Thread2을 임계영역으로 두게 된다면 Thread1의 a=3;요청은 뒤로 미뤄지게 되고, Thread2가 독점적으로 임계영역으로 사용하게 되고, Thread2에서 getValue(a)를 하게 되면 4가 출력된다. 그리고 임계 영역이 끝나면, 공유자원에 대한 독점을 해제 함으로써, Thread1이 a를 3으로 변경하게 되는 작업을 하게 된다.

 

 

< 스레드 동기화 문법 >

[lock 키워드]

C#에서는 lock 키워드를 통해 공유 자원에 대해서 서로 작업을 스레드끼리 충돌이 안나게끔 해줄 수가 있게 된다. 

 

먼저 동기화에 사용할 객체를 정의해줘야 한다.ex) object lockObj = new object(); 

 

그 다음에 lock 키워드를 적고 나서 생성한 객체를 소괄호 안에 넣어주면 만든 object에 대해서 상호 베재 잠금을 획득하게 된다.  

< 실습 > 

아래와 같이 코드를 작성하게 되면, Thread2에서는 Thread1의 영향을 받아, Message Box에서는 3이 출력되는 상황이 벌어진다. 

이런 상황에서 서로간에 영향을 안받게 할려면 lock() 키워드를 사용하게 된다는 것이다.

 

lock 키워드 안에는 잠금에 사용할 객체를 만들어 줘야 한다.

잠금에 사용할 객체 생성
상호배제 잠금하기

여기서 While문을 통해서 이런 작업이 반복적으로 일어나도록 할 수 있다.

위와 같이 코드를 작성하면, 의도한 대로 Thread1에 영향을 받지 않고 4가 출력이 된다는 것을 확인할 수 있다. 

Form1 실행 후 4초 뒤 4가 출력되고 확인을 누르면 또 4초뒤에 반복적으로 해당 메세지가 출력된다.

< 코드의 실행 순서 >

Message.show에서 3초 후에 나타나는 줄 알았는데 Thread1의 Thread.Sleep의 시간도 같이 합해지는 이유를 몰랐었다.

이에 대해 친구에게 매우 상세하게 답변을 얻었다.

 

우선 상상을 해야한다.

 

두 명의 사람이 한 대의 컴퓨터를 쓰려고 하는 상황이다.

컴퓨터는 한 번에 한 사람만 사용할 수 있고, 각자 쓰고자 하는 프로그램이 있다고 가정해보자.

사람 1은 컴퓨터를 쓰려고 자리에 앉는다 그리고선 컴퓨터를 사용하기 시작하는데, 갑자기 2분 동안 쉬기로 결정한다.

하지만 사람1은 컴퓨터를 점유한 상태다.
사람 2는 컴퓨터를 사용하고 싶지만, 사람 1이 여전히 컴퓨터 앞에 앉아 있으므로 기다려야 하는 상황이다.
사람 1이 쉬는 것을 끝내고 컴퓨터를 떠나면, 사람 2는 컴퓨터를 사용하기 시작한다. 그러나 이제 사람 2가 컴퓨터를 사용하면서 3분 동안 쉬기로 결정한다.


이 예제에서, 사람 1은 WorkThread 메서드를, 사람 2는 Work2 메서드를, 컴퓨터는 공유 자원인 변수 a를 상징한다.

사람들이 쉬는 시간은 Thread.Sleep 메서드에 해당한다.

MessageBox.Show가 나타나는 시점은 사람 2가 컴퓨터를 사용하기 시작한 이후다.

그러나 사람 2는 사람 1이 쉬는 시간 동안 기다려야 했기 때문에, 실제로는 사람 1의 쉬는 시간 + 사람 2의 쉬는 시간 만큼 기다린 후에 MessageBox.Show가 나타난다.

즉, Work2 스레드는 WorkThread 스레드가 lock을 해제할 때까지 기다려야 하고, WorkThread는 2초 동안 쉰다.

그 후 Work2가 실행되고, 추가로 3초를 기다린 후에 MessageBox가 표시된다. 따라서 총 대기 시간은 2초 + 3초인 5초가 되는 것이다.

 

이런 lock 키워드는 파일 읽고쓰기,네트워크 전송,수신,DB작업 등과 같은 곳에 사용할 수 있다.

 

※< lock keyword 사용할 때 주의점 >

lock(obj)
{
…
  lock(obj)
  {
    …  
  }
…
}//상호배제잠금을해제

위와 같이 lock안에 동일한 객체에 잠금을 요청하게 된다면, lock을 해제 하기 전에 lock을 또 시도하기 때문에

교착상태(DeadLock) 발생할 수 있다.

(위와 같은 코드를 사용한 프로그램 멈출 가능성이 높다. try catch에

잡히지 않기에 원인도 파악하기 힘들다.)


< AutoResetEvent > 

스레드를 동기화 하는 방법 중 두 번째는 AutoResetEvent가 있다. 

 

AutoResetEvent를 사용하게 된다면 Thread간의 실행 순서를 제어할 수 있게 된다.

예를 들어 Thread1이 작업이 끝나야지만 Thread2가 동작이 되게 하게 끔을 할 수 있는 것이다.

 

Thread간의 순서를 둬야할 경우, 위의 그림과 같이 DB에 데이터를 먼저 쓰고 나서

데이터를 읽는 작업을 Thread2에서 한다거나 할 때 AutoResetEvent를 사용하면 효과적이다.

 

또 DB데이터 쓰는 작업이 계속해서 반복 될 때 오래걸리게 되면, Thread2가 오래걸리는 작업을 영향을 안받고 데이터를 읽을 수 있다. 이렇듯 서로의 작업에 영향을 받지 않고 작업을 하는데 단지 먼저 데이터가 쓰여야지만, 읽을 수 있게끔 방법을 만들 수 있다.

 

이 코드를 실행하면 autoResetEvent.WaitOne();이 신호를 받을 때 까지 기다리고 있다가, Thread.Sleep(5000);에 의해 5초를 기다리고 autoResetEvent.set();을 통해 신호가 되었다고 신호를 보내주면, 이제서야 WaitOne();이 신호를 받고, 

MessageBox.Show(""+a);를 통해 a메세지가 출력되는 것이다. 즉, Set()을 보내기 전까지 Thread2의 Set()의 윗코드가 실행된다.

 

또한

이 AutoResetEvent를 true로 바꾸게 되면, 첫번째 신호는 받은 상태로 변경된다. 즉 실행하자마자 변수가 나타나고, 이후 Sleep()등을 통해, 작동 된다.

using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;


namespace WinFormsApp1
{
    public partial class Form1 : Form
    {

        int a = 5; //공유 자원
        object lockObj = new object();

        AutoResetEvent autoResetEvent = new AutoResetEvent(true);


        Thread thread = null;
        Thread thread2 = null;
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            thread = new Thread(new ThreadStart(WorkThread));
            thread.IsBackground = true;
            thread.Priority = ThreadPriority.Normal;
            thread.Start();

            thread2 = new Thread(new ThreadStart(Work2));
            thread2.IsBackground=true;
            thread2.Priority = ThreadPriority.Normal;
            thread2.Start();
            
        }
        private void WorkThread()
        {
            while (true) //반복문 시작
            {
                autoResetEvent.WaitOne(); //신호를 받을 때 까지 대기하게 된다.

                MessageBox.Show("" + a);
            
            }
        }
        private void Work2()
        {
            while (true)
            {
                a++;
                Thread.Sleep(5000);
                autoResetEvent.Set(); //신호를 보내서, WaitOne()을 실행할 수 있게 된다.
               
            }
        }
    }
}

위와 같이 코드를 작성하게 되면, 코드를 빌드하자마자 바로 5가 출력된다. 이후, a값이 1씩 증가할 수 있는 것을 확인할 수 있다. 

 

 

 

Reference : C#.NET 0.5년차~3년차(파트1)

이 블로그의 모든 내용은 원작자와 출판사로부터 허락을 받아 작성되었습니다.

https://www.inflearn.com/course/lecture?courseSlug=%EB%8B%B7%EB%84%B7-%EC%9C%88%ED%8F%BC-1&unitId=77888&tab=curriculum 

 

학습 페이지

 

www.inflearn.com