본문 바로가기
CS/OS

프로세스 동기화 [혼공컴구]

by 블로블로글 2024. 1. 18.

프로세스 동기화

  - 프로세스들 사이의 수행시기를 맞추는 것

    - 동기화 방법

      - 실행 순서 제어

        - 프로세스를 올바른 순서대로 실행하기

      - 상호 배제

        - 동시에 접근해서는 안 되는 자원에 하나의 프로세스만 접근하게 하기

 

  - 실행 순서 제어를 위한 동기화

    - 동시에 실행되는 프로세스를 올바른 순서대로 실행하는 것

      - 예시)

        - Read 프로세스, Write 프로세스와 같이 읽고 쓰기 순서가 중요한 프로세스는 순서 동기화가 잘 되어야함

        - Read 프로세스는 특정 위치에 값이 존재한다는 조건 하에 실행을 이어나갈 수 있으므로 동시에 실행되는 Write 프로세스의 순서가 잘 제어되야 함

  - 상호 배제

    - 공유가 불가능한 자원의 동시 사용을 피하기 위해 사용하는 알고리즘

    - 동시에 접근해서는 안되는 자원에 동시에 접근하지 못하게 막음

 

  - 은행 계좌 문제

[혼공컴운]

 

  - 생산자와 소비자 문제

#include <stdio.h>
#include <pthread.h>

void *produce(void *arg);
void *consume(void *arg);

int sum = 0;

int main() {

    printf("초기 합계: %d\n", sum);
    pthread_t producer, consumer;

    pthread_create(&producer, NULL, produce, NULL);
    pthread_create(&consumer, NULL, consume, NULL);

    pthread_join(producer, NULL);
    pthread_join(consumer, NULL);

    printf("producer, consumer 스레드 실행 이후 합계: %d\n", sum);

    return 0;
}

void *produce(void *arg) {
    for(int i = 0; i < 100000; i++) {
        sum++;
    }
    return NULL;
}

void *consume(void *arg) {
    for(int i = 0; i < 100000; i++) {
        sum--;
    }
    return NULL;
}

    - 생산자와 소비자는 'sum'이라는 변수를 공유

    - 생산자는 sum에 1을 더하고, 소비자는 sum에 1을 뺌

    - 실제 실행 시에 예상치 못한 값을 얻게 되는데, 이는 생산자와 소비자 프로세스가 동기화되지 않아 동시에 접근해서는 안되는 자원에 접근했기에 생기는 문제

 

  - 공유 자원

    - 위 코드의 sum과 같은 공동의 자원을 공유 자원이라고 함

 

  - 임계 구역

    - 동시에 접근하면 문제가 발생하는 자원에 접근하는 "코드 영역"

 

  - 경쟁 상태 ( race condition )

    - 잘못된 실행으로 인해 여러 프로세스가 동시 다발적으로 임계 구역의 코드를 실행하여 문제가 발생하는 경우

    - 위의 계좌 잔액 문제, 생산자-소비자 문제는 레이스 컨디션의 사례

 

  - 임계 구역 문제를 해결을 위한 원칙

    - 상호 배제

      - 한 프로세스가 임계 구역에 진입했다면 임계 구역에 진입하고자 하는 프로세스는 들어올 수 없다

    - 진행

      - 임계 구역에 어떤 프로세스도 진입하지 않았다면 임계 구역에 진입하고자 하는 프로세스는 들어갈 수 있어야한다

    - 유한 대기

      - 한 프로세스가 임계 구역에 진입하고 싶다면 그 프로세스는 언제가는 임계 구역에 들어올 수 있어야한다

 

뮤텍스 락

  - 동시에 접근해서는 안 되는 자원에 동시에 접근하지 않도록 만드는 도구

  - 상호 배제를 위한 동기화 도구

//mutexlock

#include <stdio.h>
#include <pthread.h>

void *produce(void *arg);
void *consume(void *arg);

int sum = 0;
pthread_mutex_t mutex;

int main()
{
  pthread_mutex_init (&mutex, NULL);

  printf ("초기 합계: %d\n", sum);
  pthread_t producer, consumer;

  pthread_create (&producer, NULL, produce, NULL);
  pthread_create (&consumer, NULL, consume, NULL);

  pthread_join (producer, NULL);
  pthread_join (consumer, NULL);

  printf ("producer, consumer 스레드 실행 이후 합계: %d\n", sum);

  return 0;
}

void *produce(void *arg)
{
  for (int i = 0; i < 100000; i++)
	{
	  pthread_mutex_lock (&mutex);
	  sum++;
	  pthread_mutex_unlock (&mutex);
	}
  return NULL;
}

void *consume(void *arg)
{
  for (int i = 0; i < 100000; i++)
	{
	  pthread_mutex_lock (&mutex);
	  sum--;
	  pthread_mutex_unlock (&mutex);
	}
  return NULL;
}

  - aquire()

    - pthread_mutex_lock (&mutex);

    - 임계 구역에 진입하기 전에 호출하는 함수

    - 임계 구역이 잠겨 있다면 임계 구역이 열릴 때까지 임계 구역을 반복적으로 확인하고, 임계 구역이 열려 있다면 임계 구역을 잠그는 함수

 

  - release()

    - pthread_mutex_unlock (&mutex);

    - 임계 구역에서의 작업이 끝나고 호출하는 함수

 

  - lock이 잠겨 있는지 쉴 새 없이 반복하며 확인해 봄

  - 이러한 대기 방식을 바쁜 대기라 부름

 

 

세마포

  - 뮤텍스 락과 비슷하지만, 조금 더 일반화된 방식의 동기화 도구

    - 뮤텍스 락은 하나의 공유 자원에 접근하는 프로세스를 상정한 방식

    - 공유 자원이 여러 개 있을 경우 여러 개의 프로세스가 각각 공유 자원에 접근이 가능해야 한다는 필요에서 나온 것이 세마포

//semaphore

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

void *produce(void *arg);
void *consume(void *arg);

int sum = 0;
sem_t sem;

int main() {

	sem_init(&sem, 0, 1);

	printf("초기 합계: %d\n", sum);
	pthread_t producer, consumer;

	pthread_create(&producer, NULL, produce, NULL);
	pthread_create(&consumer, NULL, consume, NULL);

	pthread_join(producer, NULL);
	pthread_join(consumer, NULL);

	printf("producer, consumer 스레드 실행 이후 합계: %d\n", sum);

	return 0;
}

void *produce(void *arg) {
	for(int i = 0; i < 100000; i++) {
		sem_wait(&sem);
		sum++;
		sem_post(&sem);
	}
	return NULL;
}

void *consume(void *arg) {
	for(int i = 0; i < 100000; i++) {
		sem_wait(&sem);
		sum--;
		sem_post(&sem);
	}
	return NULL;
}

- MacOS에서는 일반 sem관련 함수 지원 x

//semaphore_MacOS

#include <stdio.h>
#include <pthread.h>
#include <dispatch/dispatch.h>

void *produce(void *arg);
void *consume(void *arg);

int sum = 0;
dispatch_semaphore_t sem;

int main() {

	sem = dispatch_semaphore_create(1);

	printf("초기 합계: %d\n", sum);
	pthread_t producer, consumer;

	pthread_create(&producer, NULL, produce, NULL);
	pthread_create(&consumer, NULL, consume, NULL);

	pthread_join(producer, NULL);
	pthread_join(consumer, NULL);

	printf("producer, consumer 스레드 실행 이후 합계: %d\n", sum);

	return 0;
}

void *produce(void *arg) {
	for(int i = 0; i < 100000; i++) {
		dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
		sum++;
		dispatch_semaphore_signal(sem);
	}
	return NULL;
}

void *consume(void *arg) {
	for(int i = 0; i < 100000; i++) {
		dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
		sum--;
		dispatch_semaphore_signal(sem);
	}
	return NULL;
}

  - 전역 변수 sem

    - 임계 구역에 진입할 수 있는 프로세스의 개수

  - wait 함수

    - 임계 구역에 들어가도 좋은지, 기다려야 할지를 알려줌

  - signal 함수

    - 임계 구역 앞에서 기다리는 프로세스에 들어다고 된다고 신호를 주는 함수

 

 

모니터

  - 세마포의 단점을 보완하기 위해 모니터 등장

    - 세마포는 wiat와 signal을 일일히 명시해야되기 때문에 어려운 점이 존재

    - 코드가 방대해지거나 복잡해지면 잘못된 사용이 일어날 확률이 높아짐

  - 모니터는 공유 자원과 공유 자원에 접근하기 위한 인터페이스를 묶어 관리

    - 프로세스는 반드시 인터페이스를 통해서만 공유 자원에 접근 가능

//monitor.cpp

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>

std::mutex mtx;
std::condition_variable cv;
bool access = true;

int sum = 0;

void produce() {
    for(int i = 0; i < 100000; i++) {
        std::unique_lock<std::mutex> lck(mtx);
        cv.wait(lck, []{return access;});
        sum++;
        access = false;
        cv.notify_one();
    }
}

void consume() {
    for(int i = 0; i < 100000; i++) {
        std::unique_lock<std::mutex> lck(mtx);
        cv.wait(lck, []{return !access;});
        sum--;
        access = true;
        cv.notify_one();
    }
}

int main() {
    std::cout << "초기 합계: " << sum << '\n';

    std::thread producer(produce);
    std::thread consumer(consume);

    producer.join();
    consumer.join();

    std::cout << "producer, consumer 스레드 실행 이후 합계: " << sum << '\n';

    return 0;
}

 

  - 조건 변수 (cv)

    - 특정 조건을 바탕으로 프로세스를 실행하고 일시 중단하기 위해 사용

    - wait와 signal(norify_one) 연산 수행

  - 동작

    - 특정 프로세스가 아직 실행될 조건이 되지 않았을 때에는 wait를 통해 실행을 중단

    - 특정 프로세스가 실행될 조건이 충족되었을 때에는 signal을 통해 실행을 제어