기본적으로 thread는 process enviorment를 공유한다.
그외에도 mmap으로 shared memory, shmem을 설정하는 경우도 있고... 뭐 일단 그렇다.
이런 shared resource는 non-deterministic result를 낳는다.
결론은 뭐냐, concurrency를 제약해야한다는 것이다.
Shared resource에 접근하는 code sectio을 critical section이라고 한다.
Mutual Exclusion
acquire()
: lock을 잡고싶어요... 잡을 때까지 기다린다.release()
: 잡았던 lock을 풀어준다.(그리고 기다리는 다른 thread를 깨우기도 한다.(wake
))int
withdraw(int account, int amount)
{
acquire(lock);
#pragma region critical_section
balance = get_balance(account);
balance = balance - amount;
put_balance(account, balance);
#pragma endregion critical_section
release(lock);
return balance;
}
acquire
와 release
로 critical section을 가둔다.struct lock { int held = 0; }
void acquire(struct lock *l)
{
while (l->held); // loop until when held = 0(busy waiting - not sleep)
l->held = 1; // shutdown held
}
void release(struct lock *l)
{
l->held = 0; // release held to accept another one
}
struct lock
for implement lockint interested[2]; // 각 task가 현재 shared resource에 접근하고자 하는가?
void acquire(int process)
{
int other = 1 – process;
interested[process] = TRUE; // 일단 내가 shared resource에 관심이 있다는 표시를 한다.
while (interested[other]); // 다른 task에서 관심이 없으면 진행한다.(아니면 무한 loop - busy waiting)
}
void release(int process)
{
interested[process] = FALSE; // 끝났으면 다른 쪽에서 진행이 가능하도록 interest를 풀어준다.
}
int volatile turn;
int interested[2];
void acquire(int process)
{
int other = 1 – process;
interested[process] = TRUE;
#pragma region added
turn = other; // 어떤 역할을 하는가?
#pragma endregion added
while (interested[other] && turn == other);
}
void release(int process)
{
interested[process] = FALSE;
}
저 turn의 목적은 무엇인가?
다만 여전히 2개의 thread가 존재하는 경우에만 적용가능
int number[N]; // 대기열 등록이라고 생각하면됨 현재 N번째 task가 뽑은 번호표 기록
int choosing[N]; // interest의 Multi process 판
#define EARLIER(a,b) \\
((number[a] < number[b]) || \\
(number[a] == number[b] && \\
(a) < (b))) // 먼저 enqueue한 task를 찾는다? 아마
int Findmax () // 제일 큰 번호 찾기(즉 새로운 번호표를 뽑는거라고 생각하자)
{
#pragma region CS
int i;
int max = number[0];
for (i = 1; i < N; i++)
if (number[i] > max)
max = number[i];
#pragma endregion CS
return max;
}
void acquire (int me)
{
int other;
choosing[me] = TRUE; // 현재 번호표 뽑는중
number[me] = Findmax() + 1; // 번호표 뽑기 실행
choosing[me] = FALSE; // 번호표 뽑았음(등록 완료)
for (other=0; other<N; other++)
{
while (choosing[other]); // 누가 번호표를 뽑지는 않는지?(왜나면 findmax는 CS를 포함하니까... number를 건드리면 안되거든요
while (number[other] && EARLIER(other, me)); // 나보다 빠른 번호표가 있으면, 무한 loop
}
}
void release (int me)
{
number[me] = 0;
}
int TestAndSet(int *v, int new) {
int old = *v;
*v = new;
return old;
}
struct lock { int held = 0; }
void acquire(struct lock *l)
{
while (TestAndSet(&l->held, 1));
}
void release(struct lock *l)
{
l->held = 0;
}
struct lock { int value = 0; }
int waiting[N];
void acquire(struct lock *l, int me)
{
int key;
waiting[me] = 1;
key = 1; // lock checking용 변수
while (waiting[me] && key) // waiting을 합니다. 계속
key = TestAndSet(&l->value);
waiting[me] = 0;
}
void release(struct lock *l, int me)
{
int next = (me + 1) % N;
while ((next != me) && !waiting[next])
next = (next + 1) % N; // 다음으로 락이 잡혀있는 찬구를 찾는다.
if (next == me)
l->value = 0; // 나만 사용중이면 그냥 락을 푼다.
else
waiting[next] = 0; // 락을 푸는게 아니라 다음 task가 락 안으로 들어오도록 한다.(일종의 bypass)
}
int CompareAndSwap(int *v, int expected, int new)
{
int old = *v;
if (old == expected) // expect 값이 아니면 store는 안한다!
*v = new;
return old;
}
void acquire(struct lock *l)
{
while (CompareAndSwap(&l->held, 0, 1)); // 0이 들어있으면 1을 쓴다...
}
void acquire(struct lock *l)
{
cli(); // disable interrupts;
}
void release(struct lock *l)
{
sti(); // enable interrupts;
}
Coarse-grained : 한개의 lock을 이용하여 여러 개의 critical section에 대해 동시에 제한을 거는 방법
Fine-grained : 각각의 shared data마다 다른 lock을 사용하여 보호하는 방법 → 당연히 이쪽이 효율이 더 좋음
Mutual Exclusion: 이건 그냥 MUST 당연히 되어야 한다. 이게 안되면 lock의 존재가치가 유명무실해진다...
Fairness: lock을 누가 잡는지를 결정할 수 있다면(구현에 따라서는 scheduler에 의해서 결정되는 경우도 있다. fair 하지는 않지만) 당연하겠지만 공평하게 부여해야한다. starvation이 일어날 수도 있으므로...
Performance: 원래는 multicore로 작동해야할 코드에 Bottle neck같은 것을 집어넣은 것이니 당연히 성능이 좋을 수가 없다. + lock을 얻고 푸는 overhead또한 고려해야 한다.