/////
Search

7장: 동기화 예제

7.1 고전적인 동기화 문제들

Bounded Buffer

Producer와 Consumer가 있다.
Semantics
1.
Producer가 넣을 때, Consumer가 읽으면 안 된다. (Mutual Exclusive)
2.
Producer는 Consumer가 읽을 때까지 기다리면 안된다. → 너무 느려진다. 이러면 하나 넣고 하나 빼고 이렇게 되기 때문에
3.
Producer는 다 차면, Consumer가 room을 만들 때까지 기다려야한다. Consumer도 마찬가지.
→ 그럼 어떻게 해야함?, 3가지 constraint가 있기 때문에 3가지 세마포어가 있어야한다.
1.
nFullSlots → consumer’s constraint : 이게 0이면 버퍼가 empty이고, consumer가 기다려야한다.
2.
nEmptySlots → producer’s constraint: 이게 0이면 버퍼가 full이고, produce가 기다려야한다.
3.
mutex → 0이거나 1이다. mutex임
Semaphore nFullSlots = 0; -> 지금 있는 것들의 개수 Semaphore nEmptySlots = N; -> 가능한 room의 개수 Semaphore mutex = 1; Producer(item){ nEmptySlots.wait(); // wait until space mutex.wait(); //mutex Enqueue(item); mutex.signal(); nFullSlots.signal(); // tell consumer there is more data } Consumer(){ nFullSlots.wait(); // check if there's data mutex.wait(); item = Dequeue(); mutex.signal(); nEmptySlots.signal(); // tell producer need more return item; }
C
복사
→ asymmetry 코드를 가지고 있다.
어떤 실수들이 나올 수 있는지 보자.
is Order of wait’s important? → 만약에 wait의 순서를 바꾸면 어떻게 되냐
Producer(item){ mutex.wait(); nEmptySlots.wait(); Enqueue(item); mutex.signal(); nFullSlots.signal(); // tell consumer there is more data } Consumer(){ nFullSlots.wait(); // check if there's data mutex.wait(); item = Dequeue(); mutex.signal(); nEmptySlots.signal(); // tell producer need more return item; }
C
복사
→ 데드락이 걸릴 수 있다.
만약에 producer가 락을 획득해서 코드를 진행했는데 n 만큼 다 찼다면, consumer 무조건 mutex.wait()에서 걸린다.
그렇다면 signal의 순서는??
wait의 경우에는 실행되면 sleep할 수 있다.
근데 signal의 경우에는 sleep하러 가는게 아니라, 그 세마포어에 걸려있는 다른 애들을 wakeup 시키는 것이기 때문에, 행동 자체에는 영향을 끼치지 않는다.
다만 scheduling efficiency에는 영향을 끼칠 수 있다.
그렇다면 2개의 producer와 2개의 consumer가 있다면?
→ 코드가 바뀌어야할까?
바뀔 게 없다. 생산자, 소비자가 하나씩 더 늘어난다고, constraint의 개수가 늘어나진 않기 때문이다.

application example

producer : 키보드 → asynchronously하게 엄청많이 들어갈 수 있고
consumer : 데이터를 읽는 소프트웨어 → periodically하게 읽어갈 수 있다.

Readers-Writers Problem

Single Writer → Data → 2 Reader
Reader : do not perform any updates
writers: can both read and write

Problem

1.
오직 하나의 라이터만 한 시간에 데이터에 접근할 수 있도록 한다. → Mutex between writers
2.
writer는 최대한 빨리 처리한다.
3.
라이터가 쓰고 있을 때, 리더는 읽으면 안되고 리더가 읽을 때, 라이터는 쓰면 안된다. → Mutex with Writer and Reader
4.
리더가 읽고 있을 때, 다른 리더가 있다면, 리더들은 접근해도 된다
4번 문제 → writer의 기아 상태를 낳을 수 있다.
2번 문제 → reader의 기아 상태를 낳을 수 있다.

예시

인터넷 웹 사이트가 딱 이런 예시
라이터는 웹 사이트를 업데이트 할 수 있고
리더는 전 세계에서 웹 사이트를 읽어 갈 수 있다.

Shared data

Data
Semaphore mutex initialized to 1
Semaphore wrt initialized to 1 → mutex to writers
Integer nReaders initialized to 0

solution

기아 상태의 고려가 없는 해결책
semaphore rw_mutex = 1; // reader와 writer 간의 mutex, writer들 간의 mutex semaphore mutex = 1; // read_count를 갱신할 때 상호 배제를 보장하기 위함. int read_count = 0; //Writer while(true){ wait(rw_mutex); signal(rw_mutex); } //Reader while(true){ wait(mutex) read_count ++; if(read_count == 1){ wait(rw_mutex); } signal(mutex); //reading wait(mutex); read_count --; if(read_count == 0){ signal(rw_mutex); } signal(mutex); }
C
복사

Synchronization

1.
Mutual Exclusion
a.
상호 배제
2.
Progress
a.
if Critical Section이 비었다면, Critical Section에 진입하기를 원하는 다른 프로세스가 즉시 진입해야한다. 즉시가 안 된다면, 영원히 기다리는게 아니라 좀 있다가 하든, 결국 진입하긴 해야한다.
b.
이게 데드락 문제네
3.
Bounded Waiting
a.
쓰레드가 Critical Section에 진입했다면, 다른 쓰레드가 그것을 기다리는 시간은 Bounded 되야한다
b.
이게 기아 상태 문제구나

식사하는 철학자들 문제

철학자들은 식사하거나 생각해야한다.
생각할 때는 밥을 안 먹는다.
식사해야할 때는 젓가락을 집어서 먹는다.
5개의 젓가락과 5명이 있다. 따라서 양 옆의 젓가락을 집어서 먹을거다. → 없으면 식사를 못 하겠지?
젓가락이 동시에 있을 순 없겠지?

Constraint

1.
젓가락 두 개를 집으려면 하나씩 집어야겠지? → atomic operation을 의미한다.
2.
각 젓가락은 오직 한 사람에게만 속해야한다 → mutal exclusion
Semaphore chopstick[5] initialized to 1

Solution

semaphore chopstick[5]; while(true){ //think wait(chopstick[i]); wait(chopstick[i+1] % 5); // eat signal(chopstick[i]); signal(chopstick[i+1%5]); //think }
C
복사
이건 어떤 걸 지키고 있느냐?
1.
Mutual exclusion
a.
잘 지켜진다.
2.
Progress
a.
no, 데드락 상태가 있다.
b.
모든 철학자들이 왼쪽 젓가락만 기다리면 어떻게 되냐? 오른쪽을 다 같이 기다리겠지?
c.
모두가 서로를 기다리는 상황
3.
Bounded waiting time

데드락

서로가 끝나기만을 서로 기다리는 상황
intersection 에서 서로 빡빡하게 기다리는 상황을 얘기
어떤 차가 뒤로 가준다면, 길이 뚫리겠지?

Starvation vs Deadlock

Starvation: 쓰레드가 영원히 기다리는 것이다.
낮은 우선순위가 높은 애들을 많이 기다리는 상황
Deadlock: Circular waiting for resources
Thread A가 Res1을 가지고 Res2를 기다리고
Thread B가 Res2를 가지고 Res1을 기다리고

식사하는 철학자들 문제에서 데드락을 해결하는 방법?

철학자 4명만이 앉도록 한다.
한 철학자가 두 개를 모두 집을 수 있을 때만 젓가락을 집도록 허용(철학자는 임계구역 안에서만 젓가락을 집어야함) → 예시 해결 방법. 모니터를 사용해서, 원자적으로 만든다.
비대칭 해결안을 사용.
홀수 번호의 철학자는 왼쪽 젓가락을 집고, 다음에 오른쪽
짝수는 오른쪽을 집고 왼쪽을 집음

모니터를 사용하자!

여러 개의 Instruction 그 자체로 critical section이 될 수 있기에, 여러 개의 instruction 앞 뒤로 mutex를 걸어야한다. 근데 이거는 tricky하고 버그가 날 위험이 있기에, 모니터를 쓴다.
모니터 안에서는 mutual exclusion을 보장하기 때문이다.
monitor DiningPhilosophers { enums{THINKING, HUNGRY, EATING} state[5]; condition self[5]; void pickup(int i){ state[i] = HUNGRY; test(i); if(state[i]!=EATING) self[i].wait(); //자기 자신을 대기 시킨다. } void putdown(int i){ state[i] = THINKING; test((i+4)%5); test((i+1)%5); } void test(int i){ if((state[i+4]%5 != EATING) && (state[i+1]%5 != EATING) && state[i]==HUNGRY){ state[i] = EATING; self[i].signal(); // 남이 풀어준다. } } initialization_code(){ for(int i=0; i<5; i++) state[i] = THINKING; } }
C
복사

7.2 커널 안에서의 동기화

Windows의 동기화

Dispatcher 객체는 signaled 상태에 있을 수도 있고 nonsignaled 상태에 있을 수도 있다.
Signaled 상태는 객체가 사용 가능하고 그 객체를 얻을 때 그 스레드가 봉쇄되지 않음을 뜻한다.
Nonsignaled 상태는 객체가 사용할 수 없고 그 객체를 얻으려고 시도하면 그 스레드가 봉쇄됨을 뜻함.
커널이 대기 큐로부터 선택하는 스레드의 개수는 dispatcher 객체의 유형에 달려있다.
Mutex 객체는 오직 하나의 스레드만 소유할 수 있으므로, mutex의 대기 큐에서는 오직 하나의 스레드만 선택
Event객체의 경우에는 이 이벤트를 기다리고 있는 모든 스레드를 선택하게 됨.

Linux의 동기화

원자적 정수를 사용하는 모든 수학 연산은 중단됨 없이 수행됨.
atomic_st(&counter,5)
atom_add(10, &counter);
atomic_sub(4, &counter);
atomic_inc(&counter);
value = atomic_read(&counter)
원자적 연산은 오버헤드가 없다.
Linux커널에스 스핀락과 mutex락은 재귀적이지 않다. 스레드가 락 중 하나를 획득했다면 획득한 락을 해제하지 않고는 같은 락을 다시 획득할 수 없다.
해제하지 않으면 락을 획득하려는 시도는 봉쇄된다.

7.3 POSIX 동기화

Posix Mutex 락

#include <pthread.h> pthrad_mutex_t mutex; pthread_mutex_init(&mutex, NULL); pthrad_mutex_lock(&mutex); //critical section pthread_mutex_unlock(&mutex);
C
복사

POSIX 세마포

#include<semaphore.h> sem_t *sem //create semaphore and initialize to 1 sem = sem_open("SEM"., O_CREAT, 0666, 1);//SEM이라는 이름, O_CREAT: 존재하지 않을 경우 생성, 0666을 통해 다른 프로세스에 읽기 및 쓰기 접근 권한, 1로 초기화 sem_wait(sem); //critical section sem_post(sem); //무명 세마포 sem_init(&sem, 0, 1) //플래그로 0을 전달하면 세마포를 만든 프로세스에 속하는 스레드만 이 세마포를 공유할 수 있다.
C
복사

POSIX 조건 변수

pthread_mutex_t mutex; pthread_cond_t cond_var; pthread_mutex_init(&mutex, NULL); pthread_cond_init(&cond_var, NULL); pthread_mutex_lock(&mutex); while(a!=b) pthread_cond_wait(&cond_var, &mutex); pthread_mutex_unlock(&mutex);
C
복사
이 락이 획득되면 스레드가 조건을 확인할 수 있다. 조건이 true가 아닌 경우 pthread_cond_wait를 호출함. mutex락이 해제되어 다른 스레드가 공유 데이터에 접근하고 해당 값을 갱신하여 조건 절이 true가 될 수 있다.
pthread_mutex_lock(&mutex); a = b; pthread_cond_signal(&cond_var); pthread_mutex_unlock(&mutex);
C
복사

JAVA에서의 동기화

JAVA 모니터

Java의 모든 객체는 하나의 락과 연결되어 있다. 메소드가 synchronized로 선언된 경우 메소드를 호출하려면 그 객체와 연결된 락을 획득해야한다. BoundedBuffer 클래스의 insert() 및 remove()메소드와 같은 메소드를 정의할 때 synchronized를 선언하면 syncrhonized 메소드가 된다.
synchronized 메소드를 호출하려면 boundedBuffer 객체 인스턴스와 연결된 락을 소유해야한다.
public class BoundedBuffer<E>{ private static final inti BUFFER_SIZE = 5; private int count, in, out; private E[] buffer; public BoundedBuffeR(){ count = 0; in = 0; out = 0; buffer = (E[]) new Object[BUFFER_SIZE]; } public synchronized void insert(E item){ while(count == BUFFER_SIZE){ try { wait(); } catch(InterruptedException ie){} } buffer[in] = item; in = (in + 1) % BUFFER_SIZE; count ++; notify(); } public synchronized E remove(){ E item; while(count == 0){ try{ wait(); } catch (InterruptedException ie){} } item = buffer[out]; out = (out + 1) % BUFFER_SIZE; count --; notify(); return item; } }
Java
복사
synchronized 메솓를 호출할 때, 락이 가용한 경우 호출 스레드는 객체 락의 소유자가 되어 메소드로 진입.
메소드를 종료하면 락이 해제된다.
조건이 맞지 않으면 wait함수를 호출한다.
스레드가 객체의 락을 햊
스레드 상태가 봉쇄 됨
스레드는 그 객체의 대기 집합에 넣어진다.
notify()메소드를 호출한다.
대기 집합의 스레드 리스트에서 임의의 스레드 T를 선택
스레드 T를 대기 집합에서 진입 집합으로 이동
T의 상태를 봉쇄됨에서 실행 가능으로 설정

재진입락

Lock key = new ReentrantLock(); key.lock(); try{ //critical section }finally { key.unlock(); }
JavaScript
복사
여러 쓰레드가 공유 데이터를 읽기만 하고 쓰지 않을 때에는 너무 보수적인 전략일 수 있다.
Java API는 ReentrantReadWriteLock을 제공함.
reader는 여러개 일 수 있지만, writer는 반드시 하나여야 한다.

세마포

Semaphore sem = new Semaphore(1); try{ sem.acquire(); } catch(InterruptedException ie){} finally{ sem.release(); }
JavaScript
복사

조건 변수

Lock key = new ReentrantLock(); Condition condVar = key.newCondition(); Lock lock = new ReentrantLock(); Condition[] condVars = new Condition[5]; for(int i=0; i<5; i++) condVars[i] = lock.newcondition(); public void doWork(int threadNumber){ lock.lock(); try{ if(threadNumber != turn) condVars[threadNumber].await(); turn = (turn + 1) % 5; condVars[turn].signal(); }catch(InterruptedException ie){ } finally { lock.unlock(); } }
Java
복사