본문 바로가기

프로그래밍&IT/C#

스레드 동기화를 위한 동기화 객체들

알고리즘 매매를 개발하다보니

비동기 그리고 스레드 관련된 일이 좀 많은 듯하다.

 

이에 스레드 동기화를 위한 객체들에 대해

복습차 다시 한번 살펴봤다.

 

스레드동기화를 위한 동기화 객체들

 

1. Monitor

Monitor는 객체의 동기화에 사용되며, 스레드가 임계 구역에 진입할 때 락을 설정합니다. 생성자가 따로 없으며

Monitor.Enter와 Monitor.Exit을 사용하거나

lock 키워드를 사용합니다.

- 사용 예시

lock (obj) { // 임계 구역 }
Monitor.Enter(obj); try { // 임계 구역 } finally { Monitor.Exit(obj); }

2. Mutex (뮤텍스)

Mutex는 프로세스 간 동기화를 지원하는 동기화 객체로, 하나의 스레드만이 리소스에 접근하도록 보장합니다.

- 생성자

public Mutex(bool initiallyOwned)
public Mutex(bool initiallyOwned, string name)
public Mutex(bool initiallyOwned, string name, out bool createdNew);

- 사용 예시

Mutex mutex = new Mutex(false, "MyMutex")
mutex.WaitOne(); try { // 임계 구역 } finally { mutex.ReleaseMutex(); }

3. Semaphore (세마포어)

Semaphore는 여러 스레드가 동시에 접근할 수 있도록 허용하되, 접근할 수 있는 스레드의 수를 제한합니다.

- 생성자

public Semaphore(int initialCount, int maximumCount)
public Semaphore(int initialCount, int maximumCount, string name)
public Semaphore(int initialCount, int maximumCount, string name, out bool createdNew);

- 사용 예시

Semaphore semaphore = new Semaphore(0, 3);
semaphore.WaitOne(); try { // 임계 구역 } finally { semaphore.Release(); }

4. SemaphoreSlim

SemaphoreSlim은 Semaphore의 경량화 버전으로, 프로세스 간 동기화를 지원하지 않지만 더 빠릅니다.

- 생성자:

public SemaphoreSlim(int initialCount); public SemaphoreSlim(int initialCount, int maxCount);

-  사용 예시:

SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1); semaphoreSlim.Wait(); try { // 임계 구역 } finally { semaphoreSlim.Release(); }

5. AutoResetEvent

AutoResetEvent는 신호를 받으면 자동으로 리셋되는 방식으로 스레드가 대기하고 있다가 특정 시점에 하나의 스레드만을 깨웁니다.

-  생성자:

public AutoResetEvent(bool initialState);

-  사용 예시:

AutoResetEvent autoResetEvent = new AutoResetEvent(false); autoResetEvent.WaitOne(); // 임계 구역 autoResetEvent.Set(); // 다른 스레드에게 신호를 보냄

6. ManualResetEvent

ManualResetEvent는 신호를 수동으로 리셋해야 하며, 신호를 설정하면 모든 대기 중인 스레드가 계속 실행됩니다.

-  생성자:

public ManualResetEvent(bool initialState);

-  사용 예시:

ManualResetEvent manualResetEvent = new ManualResetEvent(false); manualResetEvent.WaitOne(); // 임계 구역 manualResetEvent.Set(); // 신호를 설정하여 모든 대기 중인 스레드를 계속 실행 manualResetEvent.Reset(); // 수동으로 리셋

7. Barrier

Barrier는 다수의 스레드가 특정 지점에 도달할 때까지 대기시키며, 모든 스레드가 그 지점에 도달하면 동시에 다음 작업을 진행합니다.

-  생성자:

public Barrier(int participantCount); public Barrier(int participantCount, Action<Barrier> postPhaseAction);

-  사용 예시:

Barrier barrier = new Barrier(3); // 각 스레드에서 barrier.SignalAndWait() 호출로 대기

8. CountdownEvent

CountdownEvent는 설정된 수만큼 신호가 발생하면 진행을 허용하는 동기화 객체입니다.

-  생성자:

public CountdownEvent(int initialCount);

-  사용 예시:

CountdownEvent countdown = new CountdownEvent(3); countdown.Signal(); // 카운트 감소 countdown.Wait(); // 카운트가 0이 될 때까지 대기

9. SpinWait

SpinWait는 아주 짧은 대기 상태에서 스레드를 차단하지 않고 회전하며 기다리는 방식으로, 짧은 잠깐의 대기를 필요로 할 때 사용합니다. CPU를 차단하지 않고 바쁘게 대기하기 때문에 짧은 대기에서 오버헤드가 적습니다.

-  사용 예시:

SpinWait spinWait = new SpinWait(); while (!condition) { spinWait.SpinOnce(); }

SpinWait는 주로 자원을 매우 짧은 시간 동안 기다릴 때 유용하며, 스레드를 차단하지 않기 때문에 컨텍스트 스위칭 오버헤드를 줄일 수 있습니다. 그러나 장기간 사용할 경우 CPU 소모가 커질 수 있습니다.

10. SpinLock

SpinLock은 SpinWait과 유사하게 락을 사용할 때, 스레드를 차단하지 않고 계속 반복해서 락을 얻으려 시도하는 동기화 객체

생성자:

public SpinLock(bool enableThreadOwnerTracking);

사용 예시:

SpinLock spinLock = new SpinLock(); bool lockTaken = false; try { spinLock.Enter(ref lockTaken); // 임계 구역 } finally { if (lockTaken) { spinLock.Exit(); } }
 

위의 동기화 객체들은 C#에서 멀티스레딩 환경에서 동시성 문제를 해결하는 데 매우 유용합니다. 스레드의 동작 특성에 따라 적합한 동기화 객체를 선택하는 것이 중요합니다.

SpinWait과 같은 객체는 짧은 대기 시간을 필요로 할 때,

Mutex, Semaphore는 보다 긴 시간 동안 대기해야 할 때 적합합니다.

 

※ 컨텍스트 스위칭 오버헤드 (Context Switching Overhead)?

컴퓨터 시스템에서 CPU가 하나의 프로세스 또는 스레드에서 다른 프로세스나 스레드로 전환될 때 발생하는 성능 저하.

이 과정에서 운영체제는 현재 실행 중인 프로세스의 상태(레지스터, 프로세스 상태, 메모리 상태 등)를 저장하고, 새로 실행될 프로세스의 상태를 복원해야 합니다.

컨텍스트 스위칭은 필수적이지만, 이 과정이 지나치게 자주 발생하면 CPU의 실제 작업 처리 시간이 줄어들고, 전체적인 성능 저하로 이어질 수 있습니다.

 

주요 오버헤드 원인은 다음과 같습니다

  1. 상태 저장 및 복원 시간: 프로세스의 레지스터 및 프로그램 카운터 등 현재 상태를 저장하고 복원하는 데 시간이 소요됩니다.
  2. 캐시 무효화: 새로운 프로세스가 실행될 때 이전 프로세스의 캐시가 무효화되므로, 새 프로세스의 데이터나 명령을 다시 캐시에 로드해야 합니다.
  3. 메모리 관리: 페이지 테이블이나 메모리 매핑 정보를 업데이트하는 과정에서도 시간이 소요될 수 있습니다.

* 컨텍스트 스위칭 오버헤드를 줄이는 방법에는 다음과 같은 것들이 있습니다

  • 스레드 경량화: 프로세스 대신 스레드를 사용하여 컨텍스트 스위칭의 복잡도를 줄이는 방식.
  • 적절한 스케줄링 알고리즘: 너무 잦은 스위칭을 피하는 방식으로 스케줄링을 최적화하는 것이 중요합니다.