본문 바로가기
c#

[c#] lock 사용법 및 예제

by devjh 2020. 8. 3.
반응형

개요

1. Thread

2. lock

3. BeginInvoke(비동기, 스레드풀)

4. BeginInvoke2(비동기 리턴값)

5. BeginInvoke3(콜백)

6. BeginInvoke4(BeginInvoke예제)

 

 


저번 게시글에서 스레드의 사용법을 확인하였습니다.

 

이번게시글에서는 스레드를 사용할때 주의해야 할 점에 대해 정리합니다.

 

1. 예제

namespace Increase
{
    class Program
    {
        static void Main(string[] args)
        {
            Data myData = new Data();
            Thread myThread1 = new Thread(MyFunc);
            Thread myThread2 = new Thread(MyFunc);

            myThread1.Start(myData);
            myThread2.Start(myData);

            myThread1.Join();
            myThread2.Join();
        }

        private static void MyFunc(object obj)
        {
            Data targetData = obj as Data;

            for (int i = 0; i < 10; i++)
            {
                targetData.Increase();
                Console.WriteLine(targetData.num);
            }
        }
    }

    class Data
    {
        public int num = 0;

        public void Increase()
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}

스레드 두개를 생성해주고

 

두개의 스레드는 하나의 멤버변수 num에 접근해 숫자를 하나 증가해주고 0.005초 후 출력을 해주는 예제입니다.

 

결과값입니다.

 

1부터 20까지 순차적으로 출력될것이라는 예상과는 다르게

 

뒤죽박죽의 숫자가 출력되어있습니다.

 

원인을 살펴보겠습니다.

 

첫번째 스레드는 num을 0에서 1로 변경해준후 0.005초 대기 후 출력

두번째 스레드는 num을 1에서 2로 변경해준후 0.005초 대기 후 출력

 

위의 과정을 두개의 스레드가 반복하게됩니다.

 

그러나 첫번째 스레드가 num을 0에서 1로 변경후 0.005초 대기할때

 

두번째스레드가 num을 1에서 2로 변경해준것입니다.

 

그래서 1은 출력되지않고 2가 두번 출력된 상황입니다.

 

해당오류는 "공유자원에 대한 스레드 동기화가 되지 않아서 생긴 오류" 입니다.

 

해결방법은 num에 1을 더해주고 0.005초 대기하는 로직을 한개의 스레드만 접근할 수 있도록 제어해줘야합니다.

 

2. lock 사용법

class Data
{
    private object obj = new object();
    public int num = 0;

    public void Increase()
    {
    	lock (obj)
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}

 

공유자원에 접근하는 부분에 lock으로 블럭을 만들어주면 해당블럭은 하나의 스레드만 접근할 수 있게 되며

 

해당변수는 "스레드에 안전하다" 라고 합니다.

 

이제 정상적으로 원하는결과가 출력됩니다.

 

3. 자주 실수하는 예제

class Data
{ 
    public int num = 0;

    public void Increase()
    {
    	object obj = new object();
    	lock (obj)
        {
            this.num++;
            Thread.Sleep(5);
        }
    }
}

메서드 안에서 object변수를 생성해서 lock이 걸리지 않는 예제입니다.

 

위로직으로 lock을 걸어주면 

 

스레드들이 해당 메서드에 들어올때마다 object형 변수가 새롭게 생성되어 스레드동기화가 되지 않습니다.

 

object변수는 한번만 생성되게 멤버변수로 처리해주는게 일반적입니다.

 

추가로 씨샵에는 List와 Dictionary를 스레드로부터 안전하게 캡슐화시킨

 

ConcurrentBag, ConcurrentDictionary 키워드도 있으니

 

스레드로부터 안전한 컬렉션을 찾는다면 ConcurrentBag이나 ConcurrentDictionary를 사용하는것이 좋습니다.

 

불필요한 lock ConcurrentBag ConcurrentDictionary 의 사용은 성능저하의 원인이니(보통 lock에 대한 비용이 들어간다 라고 합니다.)

 

lock이 필요한 자원or 로직인지, 멤버변수 자원을 꼭 공유해야 하는지, 확인해보고 사용하는게 좋습니다.

반응형

댓글