본문 바로가기

Programming/Thread

Threading in C# 3.동기화 처리의 기초

본 글은 Threading in C# - Getting Started 의 내용을 번역한 내용입니다.

3. 동기화 처리의 기초


동기처리

지금까지 Task를 사용하여 쓰레드를 기동하는 방법과 메인 쓰레드와 백그라운드 쓰레드 간에 데이터를 주고 받는 방법을 설명했었습니다. 또한 지역 변수가 어떻게 쓰레드 마다 분리되어 있는지, 또한 참조형이 어떻게 해서 쓰레드 간에 공유되는 지도 설명했으며, 공유된 변수에 대해서의 처리도 설명했었습니다.


다음은 동기에 대해서 설명합니다. 동기를 사용하면 복수의 쓰레드로부터의 조작을 조절하여 예측 가능한 결과를 도출하는 것이 가능합니다. 동기 처리는 공유된 같은 데이터에 관해 복수의 쓰레드로부터 접근할 경우에 특히 중요합니다. 동기를 사용한다고 한다면 이러한 문제에 대해서 상당히 간단히 처리할 수 있습니다.



동기에는 4가지 종류가 있습니다.


단순한 블럭용의 메서드


다른 쓰레드의 완료를 기다릴지, 지정한 시간이 경과할 때까지 기다릴지를 선택하기 위한 메서드입니다. Sleep, Join, Task.Wait 메서드등이 이렇게 해당합니다.



락(Lock)


몇가지 쓰레드가 동시에 무언가의 처리를 실행하고 있다고 가정합시다. 독점 락(Exclusive Lock)은 상당히 알려져 있는 락입니다. 독점 락을 사용하면 한 번에 1개의 쓰레드만이 처리를 실행할 수 있어, 다른 쓰레드로 부터 간섭을 막을 수 있습니다. 가장 표준적인 독점 락은 lock(Monitor.Enter, Monitor.Exit), Mutex, SpinLock 입니다. 독점 락이 아닌 락으로서는 Semaphore, SemaphoreSlim, Reader/Writer locks가 있습니다.



시그널(신호)


시그널을 사용하면 다른 쓰레드로부터 통지를 받을 때 까지 쓰레드를 정지할 수 있습니다. 시그널을 사용하는 것으로 필요없는 비효율적인 폴링을 회피할 수 있습니다. 시그널에는 두 가지 종류의 시그널이 자주 사용됩니다. Event wait handleMonitorWait/Pulse 메서드입니다. .NET 4.0에서는 CountdownEventBarrier 클래스가 새롭게 추가되었습니다.



블럭을 하지 않는 동기처리


위의 기능은 프로세서가 갖고 있는 독점 처리를 사용하여 공유 필드로의 접근을 블럭합니다. CLR C#에는 다음과 같이 프로세서 단계에서 블럭하지 않는 동기 처리를 제공하고 있습니다. Thread.MemoryBarrier, Thread.VolatileRead, Thread.VolatileWrite, volatile 키워드, interlocked 클래스가 제공되고 있습니다. 블럭킹은 절대 불가결한 기능입니다. 마지막 장에서 다시 설명하겠습니다. 간단하게 다음의 장에서 설명하겠습니다.



Blocking

쓰레드는 그 실행이 무언가의 이유로 정지되고 있는 경우 블럭되었다고 보게 됩니다. 예를 들면 Sleep 메서드와 다른 쓰레드의 완료를 Join과 EndInvoke로 대기하고 있는 경우입니다. 블럭된 쓰레드는 타임 슬라이스를 고쳐 OS에 반환하여 블럭이 해제되기 까지의 CPU 시간을 일정 소비하지 않습니다. 쓰레드가 블럭되어 있는지 아닌지는 ThreadState 프로퍼티에서 취득할 수 있습니다.


bool blocked = (someThread.ThreadState & ThreadState.WaitSleepJoin) != 0;


쓰레드의 상태는 저 값을 읽어 들인 타이밍과 다음 타이밍에서는 달라질 가능성이 있습니다. 이 프로퍼티는 뭔가 에러를 진단하거나 하는 등으로 사용할때만 유용합니다.

쓰레드가 블럭되어 있다고 하지 않으려고 OS는 항상 컨텍스의 교체를 진행합니다. 아 교체 처리는 약 수 마이크로의 시간이 걸립니다.

블럭 해제는 아래의 4가지 방법 중 1개에 의해 발생합니다.


  • 블럭 해제의 조건이 만족할 때

  • 조작이 타임 아웃이 될 때

  • Thread.Interrupt가 호출될 때

  • Thread.Abort가 호출 될 때


Suspend 메서드(폐지예정)을 사용하여 정지된 쓰레드는 블럭되어 있다고 보지 않습니다.



Blocking vs Spinning

가끔 어떤 특정의 조건을 만족할 때 까지 쓰레드를 정지하고 싶을 때가 있습니다. 시그널 또는 락을 사용하면 상당히 효율 좋은 방법으로 이러한 용도를 만족하는 것이 가능합니다. 하지만 이것들의 대비책으로서 좀 더 간단한 방법이 있습니다. 정기적으로 폴링을 수행하는 루프를 작성하여 그 루프 내에서 조건이 만족되지 않는 다는 것을 체크하는 형태입니다.


while (!proceed);

or:

while (DateTime.Now < nextStartTime);


위와 같이 작성하면 가능합니다. 일반적으로 이 방법은 CPU 시간을 상당히 쓸모없이 사용합니다. CPUCLR의 관점에서 본다면 쓰레드는 무언가 의미있는 계산을 하고 있다고 보여지며, 결과로서 쓰레드에는 다양한 리소스를 할당하게 됩니다. 

하지만 아래와 같이 블럭킹과 스피닝의 양쪽을 사용하는 기호가 사용되는 것도 있습니다.


while (!proceed) Thread.Sleep (10);

굉장히 우아한 방법은 아니겠지만, 이 방법은 위의 스피닝 보다는 훨씬 쓸모가 있는  방법이 됩니다. 

하지만 proceed 변수의 변경에 관해서는 병렬처리에 의한 다른 문제가 발생합니다. 이것을 회피하기 위해 블럭킹과 시그널을 적절히 사용할 필요가 있습니다.



스피닝은 조건이 상당히 짧은 시간에 (몇 마이크로 초 정도) 총족된다는 것을 미리 알고 있는 경우에 유용합니다. 왜냐하면 쓰레드의 컨텍스트 스위치에 의한 지연을 회피하는 것이 가능하기 때문입니다. .NET은 스피닝을 실현하기 위해 몇가지 특별한 메서드를 준비했습니다. 이것에 관해서는 고도의 쓰레드 처리에서 계속 해설 합니다.




ThreadState

쓰레드의 실행 상태를 ThreadState 프로퍼티에서 취득할 수 있습니다. 이 프로퍼티는 ThreadState라고 하는 Enum 타입으로 대부분의 값은 중복된다거나, 사용하지 않는 다거나, 폐지 예정이었다거나 합니다.




아래의 코드는 ThreadState의 안에 가장 유용한 4개의 것을 가리킵니다. Unstarted, Running, WaitSleepJoin, Stopped


public static ThreadState SimpleThreadState (ThreadState ts)
{
  return ts & (ThreadState.Unstarted |
               ThreadState.WaitSleepJoin |
               ThreadState.Stopped);
}


ThreadState 프로퍼티는 쓰레드의 상태를 판단할 경우에는 유효하지만, 동기 처리에 사용하기에는 부적절합니다. 왜냐하면 쓰레드의 상태를 읽어 들인 후에 즉시 다른 상태로 변해버릴 가능성이 있기 때문입니다.


다음에 계속 하겠습니다.


'Programming > Thread' 카테고리의 다른 글

Threading in C# 4.락 (Lock)  (0) 2016.10.30
Threading in C# 2.쓰레드의 개념(2)  (0) 2016.10.23
Threading in C# 1.쓰레드의 개념(1)  (0) 2016.10.21