Q1. Thread.Sleep(1); Thread.Sleep(0); Thread.Yield(); 의 차이점에 대해 말해보세요.
Q2. ContextSwitching 할 때 일어나는 일에 대해 말해보세요.
지난 시간에 스핀락에 대해 알아 봤어.
자리로 돌아와서 기다렸다 몇 분 후 다시 화장실이 비었는지 확인하는 그런 개념
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
}
}
이 부분 자체가 스핀락이라 할 수 없고, 얘를 시도 했을 때 실패를 했을 때 어떤 행동을 할 것인가가 락을 구현하는 방법을 가르게 된다 .
실패하자마자 다시 한번 루프를 실행해서 재차 시도를 하는 건 스핀락이 될 거고, 그게 아니라 쉬다 오면 두번재 방법이 될 것이다.
쉬다 올게~ 라는 걸 해줘야 하는데
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
**// 쉬다 올게 ~
Thread.Sleep(1);
Thread.Sleep(0);
Thread.Yield();**
}
}
이렇게 3가지 방법이 있다.
각가의 미묘한 차이가 있다.
milliseconds 만큼 대기 하겠다는 의미
무조건 휴식을 의미한다.
Thread.Sleep(1); // 무조건 휴식 => 무조건 1ms 정도 쉬고 싶어요
어디까지나 희망사항이고 실제로 몇 초 쉴지는 운영체제가 결정을 해줄 것이야. 운영체제가 스케쥴러라고 누가 몇초 동안 실행될 것인지 관리하는 관리자가 있는데 걔가 정해줄거야. 최대한 우리가 요청한거랑 비슷하게 해줄거야.
Thread.Sleep(0); // 조건부 양보 => 나보다 우선순위가 낮은 애들한테는 양보 불가
// => 우선순위가 나보다 같거나 높은 쓰레드가 없으면 다시 본인한테
여기서 우선순위란?
직원마다 모두 공평하게 우선순위가 같을 수도 있지만 아닐 수도 있다. 특정직원이 더 중요하다고 해서 우선순위를 높여야 한다면 나중에 설정할 때 우선순위 높여서 우선적으로 실행되게 할 수 있다. 그럼 상대적으로 우선순위가 낮은 스레드들은 기아현상을 겪에 된다.
양보긴 한데 갑질하는 양보다. 나보다 우선순위가 같거나 높은 애가 없으면 본인한테 불러 온다는 특징이 있다. 장점이라고 볼수도 단점이라고 볼 수도 있다.
Thread.Yield(); // 관대한 양보
// => 관대하게 양보할테니, 지금 실행이 가능한 쓰레드가 있으면 실행하세요
// => 실행 가능한 애가 없으면 남은 시간 소진
이렇게 3가지 버전이 있는데 이중에서 골라서 사용하면 된다.
어느게 좋다고 말하기 애매하고 만드는 프로그램에 따라 테스트 해볼 수 있다. 어떤 식으로 프로그램이 돌아가냐에 따라, 어떤 환경에 있느냐에 따라 조금씩 달라질 수 있기 때문에
일단 테스트는 Yield로 해본다.
이렇게 동작을 한다.
여기서 별 차이는 못느끼겠지만 여기서 이렇게 쉬다 오면 장점은
무한정으로 뺑뺑이 도는 상황을 예방해줄 수 있다.
static void Thread_1()
{
for(int i = 0; i<1000000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
만약 여기 안에서 무거운 작업을 하고 있었다고 가정을 해보면
public void Acquire()
{
while (true)
{
int expected = 0;
int desired = 1;
**if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;**
}
}
기다리는 애가 무한정 뺑뺑이를 돌면서 계속 실행을 하면 부담되는 쓰잘대기 없는 작업을 하는 사태가 발생하기 떄문에
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
// 쉬다 올게 ~
//Thread.Sleep(1); // 무조건 휴식 => 무조건 1ms 정도 쉬고 싶어요
//Thread.Sleep(0); // 조건부 양보 => 나보다 우선순위가 낮은 애들한테는 양보 불가 => 우선순위가 나보다 같거나 높은 쓰레드가 없으면 다시 본인한테
Thread.Yield(); // 관대한 양보 => 관대하게 양보할테니, 지금 실행이 가능한 쓰레드가 있으면 실행하세요 => 실행 가능한 애가 없으면 남은 시간 소진
}
}
이런 식으로 적절하게 한번씩 쉬다오는 것도 경우에 따라 괜찮을 방법이 된다.
이렇게 양보하는 방법이 장점만 있는지는 애매하다. 문제가 되는 건 컨텍스트 스위칭이라는 비용이야. 빙의를 할 때 옮겨 가는 비용을 말한다.
식당 관리자도 빙의 대상이긴 하다.
관리자가 어떤 직원을 다음으로 할지 골라준다. 아래 부분도 cpu가 실행을 시켜야 하는 코드야. 관리자도 일종의 직원.
이렇게 되면 관리자 모드로 어떤 직원을 실행시킬까를 골라주는 작업
운영체제 윈도우즈 커널의 역할을 관리자가 하는 거라 생각하면 된다.
왼쪽 직원에서 오른쪽 직원으로 옮겨 가는 건 관리자 모드로 갔다가 다시 올라 가야해. 생각보다 단계가 늘어난다.
위에 부분이 유저 모드, 아래가 커널모드
직원에 관련된 정보는 무엇인지 궁금해.
관리자는 cpu 의 코어, ALU, 캐시 장치가 있어.
레지스터는 임시로 메모하기 위한 공간이라고 간략하게 요약했는데 사실 여러개가 있고 각각의 용도가 굉장히 다르다. 연산하고 있는 걸 기록하는 용도로 사용하는 레지스터가 있고 어디까지 무엇을 실행하고 있었는지 코드 실행 자체를 어디까지 했는지 추적하는 용도로 사용되는 레지스터도 있고, 메모리 주소에 접근하기 위하 사용하는 레지스터도 있다. 다양한 용도로 사용된다.
영혼이 직원에 빙의를 했다 가정을 해보자. 빙의하면 이상한 행동하고 영혼이 떠나가면 빙의 됐을 때의 기억을 잊게 된다. 이것도 그런 느낌. 직원은 껍데기, 직원이랑 레스토랑 정보는 RAM에 어딘가에 저장하고 있다. 코어가 직원을 빙의할 때는 컨텍스트라는 모든 정보를 복원시켜 줘야 한다는게 된다. 단순하게 영혼을 이용해서 빙의하겠다 가정을 하면 RAM에 있던 정보들 중에서 직원과 식당에 대한 정보를 뽑아 와야 한다. 일부는 레지스터에 복원을 해야 한다. 식당이나 주방 테이블 위치 같은 정보도 복원해야 하는데 가상 메모리와 연관이 있다. 운좋게 빙의한 직원이랑 이전 직원이랑 같은 식당에서 일하고 있었으면 다행히 가상 메모리는 안바꿔줘도 되는데 일식집에서 일하는 직원에 빙의하다 한식집에서 일하는 직원에 빙의 하면 식당 구조가 달라지니까 가상 테이블도 바꿔치기 해서 결국에는 식당에 대한 정보도 새로 기억 하게끔 추출을 해야 한다.
레지스터는 온갖 정보를 들고 있는데 빙의하고 옮겨 다닐 때 마다 레지스터의 정보는 싸그리 날리고 복원을 해야 한다. 이전에 들고 있던 레지스터 정보는 메모리(Ram)에 고이고이 저장을 해서 다시 빙의 하면 다시 꺼내쓸 수 있도록 만들어 줘야 한다. 온갖 정보를 저장하고 복원하는 단계가 핵심적인 단계가 된다.
어려운 작업을 하고 있었어.
결론은 우리가 이전 프로그램에서 thread sleep을 하건 yield를 하건 소유권을 포기하고 남한테 준다고 꼭 좋다는 건 아냐.
한번씩 영혼을 바꿔치기 할 때 마다 어마어마한 부담이 되기 때문에 경우에 따라서는 스핀락처럼 계속 유저모드에서 계속 돌면서 트라이 하는게 효율적일 수 있다는 얘기가 된다.
using System;
using System.Threading;
using System.Threading.Tasks;
namespace ServerCore
{
class SpinLock
{
volatile int _locked = 0;
public void Acquire()
{
while (true)
{
//int original = Interlocked.Exchange(ref _locked, 1);
//if (original == 0)
// break;
// CAS Compare-And-Swap
int expected = 0;
int desired = 1;
if (Interlocked.CompareExchange(ref _locked, desired, expected) == expected)
break;
// 쉬다 올게 ~
//Thread.Sleep(1); // 무조건 휴식 => 무조건 1ms 정도 쉬고 싶어요
//Thread.Sleep(0); // 조건부 양보 => 나보다 우선순위가 낮은 애들한테는 양보 불가 => 우선순위가 나보다 같거나 높은 쓰레드가 없으면 다시 본인한테
Thread.Yield(); // 관대한 양보 => 관대하게 양보할테니, 지금 실행이 가능한 쓰레드가 있으면 실행하세요 => 실행 가능한 애가 없으면 남은 시간 소진
}
}
public void Release()
{
_locked = 0;
}
}
internal class Program
{
static int _num = 0;
static SpinLock _lock = new SpinLock();
static void Thread_1()
{
for(int i = 0; i<1000000; i++)
{
_lock.Acquire();
_num++;
_lock.Release();
}
}
static void Thread_2()
{
for (int i = 0; i < 1000000; i++)
{
_lock.Acquire();
_num--;
_lock.Release();
}
}
static void Main(string[] args)
{
Task t1 = new Task(Thread_1);
Task t2 = new Task(Thread_2);
t1.Start();
t2.Start();
Task.WaitAll(t1, t2);
Console.WriteLine(_num);
}
}
}
'Server programming' 카테고리의 다른 글
01_13_멀티쓰레드_ReaderWriteLock (0) | 2023.03.21 |
---|---|
01_12_멀티쓰레드_AutoResetEvent (0) | 2023.03.16 |
01_10_멀티쓰레드_SpinLock (0) | 2023.03.15 |
01_09_멀티쓰레드_Lock 구현 이론 (0) | 2023.03.14 |
01_08_멀티쓰레드_DeadLock (0) | 2023.03.14 |
댓글