⚠️ 해당 포스팅은 인프런 공룡책 강의를 듣고 개인적으로 정리하는 글입니다. 정확하지 않은 정보가 있을 수 있으니 주의를 요합니다.
Chapter 6 CPU Synchronization Tools (2)
lock
을 획득해야 한다.lock
을 다시 반납해야 한다.lock
이 걸려있을 경우 락이 풀릴 때까지 기다리며 컨텍스트 스위칭을 실시한다.즉, 바쁜 대기(busy waiting)을 실시하지 않고 sleep 상태로 들어갔다가, wakeup 콜이 오면 다시 권한 획득을 시도한다.
acquire()
와 release()
가 있다.available
, 뮤텍스락 입장이 가능한지 불가능한지 판단하는 변수이다.acquire()
, release()
둘 중 하나만 호출하고, 모든 과정은 원자적(atomically)으로 이루어진다.앞에서도 이야기 했지만, 원자적으로 이루어지면 기능 실행 도중 컨텍스트 스위칭이 일어나지 않는다.
compare_and_swap
연산을 이용하여 구현될 수 있다.조금만 기다리면 바로 사용할 수 있는데, 굳이 컨텍스트 스위칭이 일어나야 하냐는 관점에서 바라보면 이해가 쉽다.
acquire()
를 실행하기 위해 무한 루프를 일으키는 것을 의미한다.wait()
, signal()
P()
, V()
라고 칭하는 경우도 있다.wait()
, signal()
연산을 통해 이루어지며 모두 원자적으로 실행되어야 한다.wait()
을 호출하고, count
의 수를 감소시킨다.열쇠 창고에 있는 열쇠를 가져가니, 열쇠 창고에 있는 열쇠의 수가 줄어드는 것
signal()
을 호출하고, count
의 수를 증가시킨다.열쇠를 다 사용하고 열쇠 창고에 반납하니 열쇠 창고에 있는 열쇠의 수가 증가되는 것
count
가 0이 되었을 때는 모든 자원이 사용되고 있는 것을 의미하므로, 프로세스는 블락(busy waiting)된다.sync
라는 세마포어 변수를 공유하게 하고, 0으로 초기화하면 자연스럽게 해결할 수 있다.wait()
연산을 실행할 때 세마포어가 양수가 아니면, 무조건 대기해야 한다.waiting queue
로 향하게 만드는 것이다.signal()
연산을 실행하면, 프로세스는 ready queue
로 들어가 재실행을 준비한다. (뮤텍스락과 비슷한 느낌이다.)
void *counter(void *param)
{
int k;
for (k = 0; k < 10000; k++) {
/* entry section */
sem_wait(&sem);
/* critical section */
sum++;
/* exit section */
sem_post(&sem);
/* remainder section */
}
pthread_exit(0);
}
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
int sum = 0; // a shared variable
sem_t sem;
int main()
{
pthread_t tid1, tid2;
sem_init(&sem, 0, 1); // binary semaphore
pthread_create(&tid1, NULL, counter, NULL);
pthread_create(&tid2, NULL, counter, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
printf("sum = %d\n", sum);
}
/* result : sum = 50000 */
n = 1
일 때의 예시 코드이다.
void *counter(void *param)
{
int k;
for (k = 0; k < 10000; k++) {
/* entry section */
sem_wait(&sem);
/* critical section */
sum++;
/* exit section */
sem_post(&sem);
/* remainder section */
}
pthread_exit(0);
}
/* 위의 코드와 동일하므로 중략 */
int main()
{
pthread_t tid1, tid2;
sem_init(&sem, 0, 5);
for (i = 0; i < 5; i++)
pthread_create(&tid1, NULL, counter, NULL);
for (i = 0; i < 5; i++)
pthread_join(tid[i], NULL);
printf("sum = %d\n", sum);
/* 결과가 50000이 나올까? */
}
50000
이 나올 것이라고 생각하지만, 실상은 그렇지 않다.sem_init(&sem, 0, 1)
로 코드를 변경하여 열쇠함에 있는 열쇠를 하나로 고정시켜버린다면, 동기화가 이뤄질 수 있을 것이다.wait
, signal
연산을 이용해 상호 배제를 제공한다.lock
기능을 지원하여 동기화를 수행한다.wait()
혹은 signal()
연산에 의해서만 움직인다.즉, 모니터는 lock 오브젝트와 조건 변수들로 구성되어 있다는 의미이다.
synchronized (object) {
// critical section
}
public synchronized void add() {
// critical section
}
synchronized
wait
, notify()
wait()
메소드 호출시, 해당 객체의 모니터락을 얻기 위해 대기 상태로 진입한다.notify()
메소드 호출시, 해당 객체 모니터에 대기중인 스레드 하나를 깨운다.notifyAll()
메소드 호출시 해당 객체 모니터에 대기중인 스레드를 전부 깨운다.이제, 예시를 통해 알아보도록 하자.
static class Counter {
private static Object object = new Object();
public static int count = 0;
public static void increment() {
count++;
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i ++) Counter.increment();
}
}
public static void main(String[] args) throws Exception{
Thread[] threads = new Thread[5];
for(int i = 0; i < threads.length; i ++) {
threads[i] = new Thread(new MyRunnable());
threads[i].start();
}
for(int i = 0; i < threads.length; i ++) {
threads[i].join();
}
System.out.println("counter = " + Counter.count);
}
}
public class SynchExample {
static class Counter {
public static int count = 0;
synchronized public static void increment() { // 메소드 전체 임계영역 설정
count++;
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i ++) Counter.increment();
}
}
synchronized
영역을 길게 작성하는 것은 멀티 스레드의 성능을 저하시킬 수 있으므로 지양해야 한다.public class SynchExample {
static class Counter {
private static Object object = new Object(); // object lock
public static int count = 0;
public static void increment() {
synchronized (object) {
count++; // 임계 영역을 따로 지정하는 방법이다.
}
}
}
static class MyRunnable implements Runnable{
@Override
public void run() {
for (int i = 0; i < 10000; i ++) Counter.increment();
}
}
public class SynchExample {
static class Counter {
public static int count = 0;
public void increment() { // static이 빠짐
synchronized (this) { // this로 자기(스레드) 참조
count++;
}
}
}
static class MyRunnable implements Runnable{
Counter counter; // not static, 인스턴스 변수 필요
public MyRunnable(Counter counter) {
this.counter = counter;
}
@Override
public void run() {
for (int i = 0; i < 10000; i ++)
counter.increment();
}
}
public static void main(String[] args) throws Exception{
Thread[] threads = new Thread[5];
for(int i = 0; i < threads.length; i ++) {
threads[i] = new Thread(new MyRunnable(new Counter()));
// 인스턴스를 생성하는 모습, 이 코드에선 5개 인스턴스를 따로 넘기는 것이다.
threads[i].start();
}
for(int i = 0; i < threads.length; i ++) {
threads[i].join();
}
System.out.println("counter = " + Counter.count);
}
}
public static void main(String[] args) throws Exception{
Thread[] threads = new Thread[5];
Counter counter = new Counter(); // 인스턴스 선언
for(int i = 0; i < threads.length; i ++) {
threads[i] = new Thread(new MyRunnable(counter));
// 하나의 인스턴스를 넘기는 모습이다.
threads[i].start();
}
for(int i = 0; i < threads.length; i ++) {
threads[i].join();
}
System.out.println("counter = " + Counter.count);
}
this
, 즉 같은 것을 참조하고 있기 때문에 정상적인 동기화가 진행되어 기대 결과값인 50000
이 출력되게 된다.
오늘도 잘 보고 갑니다