전통적 동기화 예제(생산자-소비자문제, RW문제, 식사하는 철학자 문제)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


전통적 동기화 예제 (Classical Synchronization Problem)

1. Producer and Consumer Problem (생산자-소비자 문제)

다른말로 유한버퍼 문제(Bounded Buffer Problem)라고도 한다.
생산자가 데이터를 생산하면 소비자는 그것을 소비하는데, 그 생산자와 소비자 사이에는 저장공간인 buffer가 있고 이 buffer의 크기는 유한하다.

예) 컴파일러 -> 어셈블러, 파일서버 > 클라이언트, 웹서버 > 웹 클라이언트

Bounded Buffer?

Bounded: 한계가 있다.
buffer: 데이터를 모아둘 수 있는 메모리/디스크 공간

  • 생산된 데이터는 버퍼에 일단 저장(속도차이 등)
  • 현실 시스템에서 버퍼 크기는 유한
  • 생산자는 버퍼가 가득 차면 더 넣을 수 없다 (size = count)
  • 소비자는 버퍼가 비면 뺄 수 없다.
class Buffer {
  int[] buf;
  int size; //버퍼 갯수(용량), 크기
  int count; //버퍼안의 생산되어 저장되어있는 갯수
  int in; //처음 in 인덱스 값부터 생산자의 데이터를 넣겠다.
  int out; // 빼내는 위치
}

Buffer(int size){
  buf = new int(size);
  this.size = size;
  count = in = out = 0
}
// check if buf is full
void insert(int item) { // 생산자가 버퍼에 데이터를 집어넣는 함수
  while (count == size) // 같다면 무한루프를 돌며 여기서 멈춰있다.

// buf is not full
  buf(in) = item; //소비자가 하나를 가져가 1의 공간이 생긴다면 item을 in에 넣고
  in = (in + 1) % size; //나머지가 0
  count ++;
}

int remove() {
  // check if buf is empty
  while (count == 0) //버퍼가 비어져있으면 무한루프 돌면서 소비자는 기다리고 있음

// buf is not empty
  int item = buf(out); //buf에 out한 값을 item에 넣는다.
  out = (out + 1)%size; //나머지가 0
  count --;
  return item
}
class Test {
  public static void main(String[] args) {
    Buffer b = new Buffer(100);
    Producer p = new Producer(b, 10000);
    Consumer c = new Consumer(b, 10000);
    p.start()
    c.start()
    try {
      p.join()
      c.join()
    } catch (InterruptedException e) {
      System.out.println("Number of items in the buf", b.count);
    }
  }
}

정상적으로 실행시켜보면 count=0이 나올것이다(동기화를 하는 이유)

그러나 잘못된 결과가 실행되는 경우가 있다.

  1. 실행불가
  2. count ≠ 0

이런 결과가 나타나는 이유는 결국 아래와 같다.

  • 공통변수 count(), buf[]에 대한 동시 업데이트 진행
  • 공통변수 업데이트 구간(임계구역)에 대한 동시 진입이 진행

따라서 이를 해결하기 위한 방법은 아래와 같다.

  • 임계구역에 대한 동시 접근 방지(상호배타)
  • 세마포를 사용한 상호배타 (mutual exclusion)
  • 세마포: mutex.value = 1(# of permit)

Busy-Wait

바쁘게 기다린다는 의미로 cpu에서 생산자는 버퍼가 가득차있으면 더이상 생산을 해낼수가 없고 버퍼가 비어있으면 소비자는 버퍼가 찰때까지 기다려야 한다. 이 기다리는 코드는 아래와 같다.

  • 생산자: 버퍼가 가득차면 기다려야 = 빈공간이 있어야 한다.
  • 소바자: 버퍼가 비면 기다려야 = 찬 공간이 있어야 한다.
Buffer(int size){
  buf = new int(size);
  this.size = size; //버퍼 크기
  count = in = out = 0 // 생산된 항목 갯수
}

size == count라면 무한루프 돌면서 기다려라라고 함으로써 cpu는 이 시간에 계속해서 size == count한 것을 계속 세면서 다른일을 하지못하고 있다. (비효율적) 이런 상황을 busy wait이라고 한다.

os는 성능을 높여줘야 하는데, cpu는 아무일도 못하고 무한루프 돌고 있으니 낭비가 굉장히 심하다.

세마포를 사용한 busy-wait 회피

  • mutex.value = 1
  • 생산자: empty.acquire() # of permit = BUF SIZE
  • 소비자: full.acquire() # of permit = 0
[생산자] [소비자]
empty.acquire(); full.acquire();
PRODUCE; CONSUME;
full.release(); empty.release();

즉, 만약 버퍼가 가득차있다면 생산자쪽에 semaphore가 block 처리를 시켜줌으로써 더이상 cpu가 여기서 무한루프를 돌지않도록 해준다. 이를 깨워주는것은 소비자가 이 버퍼에서 빼내어주어서 빈공간이 생기면 다시 생산자가 넣을 수 있도록 해준다. 따라서 block은 자고있다는 뜻으로 서로가 서로를 가두고 풀어줌으로써 효율을 높여준다.

따라서 semaphore를 통해서 busy-wait이 일어나지 않도록, 즉 운영체에의 성능을 높여주도록 한다.

2. Reader-Writer Problem

주로 공통된 데이터베이스를 접근할 때 발생하는 문제이다. 데이터베이스는 공통이니까, 임계구역이 반드시 존재해야한다. 즉 어느 경우에도 한사람만 들어올 수 있도록 해야하는데 그렇게 하면 상당히 비효율적이다. 더 나아가 reader, writer의 경우는 조금 다르다.

  • 공유 데이터베이스 접근
    • Reader: read data. never modify it
    • Writer: read data and modify it
    • 상호배타: 한번에 한개의 포로세스만 접근 > 비효율적
  • 효율성 제고
    • Each read or write of the shared data must happen within a critical action
    • Guarantee mutual exclusion for Writer
    • Allow multiple readers to execute in the critical section at once

서로 공유하는 데이터에 한해서, 읽기쓰기는 임계구역으로 구현해야하는데 writer 두명이 들어오면 임계구역 해야하지만 reader는 여러명이 들어오기를 혀용한다. (내용을 바꾸는것이 아니니까) 그러다가 reader가 들어와서 database를 조회하고 있는데 writer가 온다면 writer는 기다려야 한다.

  • 변종
    • The first R/W problem(readers-preference): 항상 reader 에게 우선권을 준다.
    • The second R/W problem(writer-preference): writer에게 우선권을 준다.
    • The third R/W problem: 우선권을 아무에게도 주지 않는것

즉, reader가 들어왔는데 writer는 block되어야 하고 반대의 상황도 같다. 그리고 reader가 들어와있는데 다른 reader가 들어오고 싶다면 그건 효율성 측면에서 허용이 된다!

3. Dining Philosopher Problem (식사하는 철학자 문제)

  • 식사하는 철학자 문제
    • 5명의 철학자, 5개의 젓가락, 생각->식사->생각->식사…
    • 식사하려면 2개의 젓가락이 필요
  • 프로그래밍
    • 젓가락: 세마포(# of permit =1) // 세마포의 초기값을 1로 두고
    • 젓가락과 세마포에 일련번호: 0 - 4 //
    • 왼쪽 젓가락 -> 오른쪽 젓가락
public void run() {
  try {
    while (true) {
      lstick.acquire();
      rstick.acquire();
      eating();

      lstick.release();
      rstick.release();
      thinking();
    }
  } catch (InterruptedException e)
}

void eating() {
  System.out.println('I' + id + "eating");
}

void thinking() {
  System.out.println('I' + id I "thinking");
}
class Test {
  static final int num = 3

  public static void main(Sting[] args) {

    // Chopsticks
    Semaphore[] stick = new Semaphore[num];
    for (i=0l i=num; i++) {
      stick[i] = new Semaphore(1)
    }
    // Philosopher
    Philosopher[] phil =new Philosopher[num];
    for (i=0l i=num; i++;){
      phil[i] = new Philosopher(i, stick[i], stick[(i+1)%num]);
    }
    // let Philosopher eat and think
    for (i=0; i<num; i++;){
      phil[i].start();
    }
  }
}

무한루프임에도 실행되다가 중지가 되는데, 그 이유가 뭘까?

  • starvation: 모든 철학자가 식사를 하지 못해 굶어죽는 상황 -> 이유: 교착상태(deadlock) > 모든 사람들이 다 왼쪽 젓가락을 든 상태

동기화를 위한 도구 - Semaphore(Mutual exclusive, Ordering)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


동기화를 위한 도구

  • Semaphores
  • Monitors

1. Semaphores: 전통적인

  • 철도의 신호기(깃발) 등의 사전적 의미를 가짐
  • 동기화 문제 해결을 위한 소프트웨어 도구
  • 네덜란드 사람이 만들었음
  • 구조: 정수형 변수 + 두개의 동작 (P:Proberen(test)->acquire(), V:Verhogen(increment)->release())
    • 정수형 변수를 테스트하고 값을 증가시키는 동작

acquire()

void acquire() {
  value --; // number of permit(무언가를 할 수 있는 권한)
  if (value < 0) {
    add this process/thread to list
    block;
  }
}
  • acquire()가 실행되면 value값은 1만큼 감소된다.
  • value < 0이 되면 acquire를 호출했던 프로세스나 쓰레드를 큐(list)안에 넣는다.
    • 즉, acquire내부를 살펴보면 value, P, V, 큐(list)가 존재
  • 누가 꺼내주기 전까지는 block한다. (멈춰있다)

release()

void release() {
  value ++;
  if (value <= 0) {
    remove a process P from list
    wakeup P;
  }
}
  • release()가 실행되면 value 값은 1만큼 증가한다.
  • value <= 0이 되면 release를 갖혀있던 한 쓰레드를 꺼내서 깨워준다
  • 즉 큐에서 해방시켜준다.

이러한 Semaphores는 어떠한 때에 사용할까?

상황1. Mutual exclusion: 상호 배타 목적으로 사용

  • value = 1을 둔다.(권한을 1로 두었다는 것을 의미)
  • acquire()를 호출하면 아직 value<0이 되지 않음으로 바로 critical section 으로 진입
  • context switching이 실행되어 다른 프로세스가 돈다고 해보자
  • 다른 프로세스가 acquire()하여 value=-1이 되었으니 큐에 갇히게 됨
  • 즉, critical section으로 진입이 되지 못함
import java.util.concurrent;

class BankAccount {
  int balance;
  Semaphore sem;
  BankAccount() {
    sem = new Semaphore(1);
  }
  void deposit(int n) {
    try{
    sem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("+");
    balance = temp;
    //
    sem.release();
  }
  void withdraw(int n ){
    try{
    sem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("-");
    balance = temp;
    //
    sem.release();
  }
  int getBalance() {
    return balance;
  }
}

상황2. Ordering

앞에서 이야기 했듯이 프로세스/쓰레드 동기화에서는 임계구역 문제를 해결하는 것도 중요하지만, 프로세스의 실행 순서를 제어하는 것 또한 중요하다.

  • sem.value = 0
P1 P2
  sem.acquire();
S1; s2;
sem.release();  
  • CPU가 P1을 먼저 가리킨다면 자연스럽게 S1이 실행될 것이다.
  • 공교롭게 CPU가 P2를 가리키게 된다면 sem.acquire()를 실행하게 되어 value = -1로 만든다.
  • 그러면 P2는 sem.acquire()에 block 될 것이고, S1이 실행될 것이다.
  • S1을 끝내고 sem.release()를 실행시키면 value = 0 이 되고
  • block 된 P2는 다음 코드로 진행이 가능할 것이다.

문제: 원하는 방식으로 입금/출금 프로그램 만들기

import java.util.concurrent;

class BankAccount {
  int balance;
  Semaphore sem; dsem; wsem;
  BankAccount() {
    sem = new Semaphore(1);
    dsem = new Semaphore(0);
    wsem = new Semaphore(0);
  }
  void deposit(int n) {
    try{
    sem.acquire();
    // critical section
    int temp = balance + n;
    System.out.println("+");
    balance = temp;
    sem.release();
    //
    wsem.release();
    dsem.acquire();
    } catch (InterruptedException e) ()
  }
  void withdraw(int n ){
    try{
    wsem.acquire();
    } catch (InterruptedException e) ()
    // critical section
    int temp = balance + n;
    System.out.println("-");
    balance = temp;
    sem.release();
    //
    dsem.release();
  }
  int getBalance() {
    return balance;
  }
}

정적 페이지와 동적 페이지

|

개인적인 연습 내용을 정리한 글입니다.
잘못된 내용이 있다면 편하게 댓글 남겨주세요!


정적 웹 페이지(Static Web Page)

  • 서버에 미리 저장된 파일이 그대로 전달되는 웹 페이지
  • 서버는 사용자의 요청(request)에 해당하는 저장된 웹 페이지를 보냄
  • 사용자는 서버에 저장된 데이터가 변경되지 않는 한 고정된 웹 페이지를 보게됨

동적 웹 페이지(Dynamic Web Page)

  • 서버에 있는 데이터들을 스크립트에 의해 가공처리한 후 생성되어 전달되는 웹 페이지
  • 서버는 사용자의 요청(request)을 해석하여 데이터를 가공 후 생성된 웹 페이지를 보냄
  • 사용자는 상황, 시간, 요청 등에 따라 달라지는 웹 페이지를 보게 됨

인터넷을 사용하다보면, 크게 웹페이지는 정적 웹페이지와 동적 웹페이지로 나뉜다. 정적 웹페이지는 컴퓨터에서 저장된 텍스트 파일을 메모장을 통해 열어보듯 저장된 그대로를 보는것이며, 동적 웹페이지는 그런 내용들이 다른 변수들에 의해 변경되어 보여진다. 가장 큰 차이는 사용자가 받아보는 웹 페이지가 동적으로 변하는 가 아닌가 에 있다.

우리가 보는 대부분의 웹페이지는 동적 웹페이지라고 할 수 있다. 뉴스사이트의 뉴스를 보거나, 포털에서 웹툰을 보거나 하는 행위는 사용자의 요청에 따라 원하는 페이지를 동적으로 생성하여 보내주는 것이다. 만약 정적인 웹 페이지로만 구성하여 홈페이지를 만들게 되면, 새로 업데이트를 하거나 제거를 할때마다 정적 웹 페이지를 제작해줘야하는데 이를 동적 웹페이지로 구축하게 되면 해당하는 스크립트만 작성하고 자동으로 페이지가 생성되게만 해주면 사이트의 관리 비용이 절감할 수 있게 된다.

사용자 입장에서는 사실 서버에서 처리된 웹 페이지를 전달받기 때문에 정적이든 동적이든 크게 상관이 있지않다. 결국 웹페이지는 HTML로 이루어진 웹 페이지 이기 때문이다.

정적 웹 페이지의 장/단점

정적 웹 페이지

  • 장점
  1. 빠르다: 요청에 대한 파일만 전송하면 되기 때문에 추가적인 작업이 없다.
  2. 비용이 적다: 마찬가지로 웹 서버만 구축하면 된다.
  • 단점
  1. 서비스가 한정적이다: 저장된 정보만 보여줄 수 있다.
  2. 관리가 힘들다: 추가/수정/삭제의 작업 모두 수동으로 이루어져야 한다.

동적 웹 페이지

  • 장점
  1. 서비스가 다양하다: 다양한 정보를 조합하여 동적으로 생성하여 제공이 가능하다
  2. 관리가 쉽다: 웹 사이트 구조에 따라서 추가/수정/삭제 등의 작업이 용이하다.
  • 단점
  1. 상대적으로 느리다: 사용자에게 웹 페이지를 전달하기 전에 처리하는 작업이 필요하다.
  2. 추가 비용이 든다: 웹 서버외에 추가적으로 처리를 위한 어플리케이션 서버가 필요하다.

동적 웹 사이트는 정적 웹 사이트와는 달리 서버 내부에서 추가적인 작업이 필요하기 때문에 서버 내부에 그런 작업들을 처리하기 위한 모듈같은 것이 필요하다. 따라서 서버를 구성할 때 필요한 모듈들을 설치해야하고, 웹 호스팅을 이용할 때에도 해당 모듈을 제공하는 서비스를 선택해야한다.

이런 모듈들은 서버의 자원(CPU, 메모리 등)을 이용하기 때문에 비용이 들기도하며, 이런 모듈들을 이용해 동적 웹페이지를 생성한다는 것 자체가 바로 전송이 가능한 정적인 웹 페이지에 비해 느린것도 사실이다. 그러나 웹 사이트 구축과 운영/관리 측면에서 봤을때, 구조화된 웹사이트 구축과 관리의 용이성 때문에 전체적인 비용이 절감되는것도 사실이다.

정적인 웹 페이지는 각각 독립되어 있기 때문에 같은 코드가 포함된다하더라도 각각 따로 저장되어야 하지만, 동적 웹 페이지는 중복 코드를 하나의 파일로 만들어 스크립트 불러오기가 가능하다. 따라서 구조화된 웹 사이트를 구축할 수 있는 장점이 있다.

이러한 특징으로 정적 웹페이지보다 동적 웹 페이지를 더 많이 사용하긴 하지만, 정적 웹사이트가 아예 사용되지 않는 것도 아니다. 정적 웹 페이지는 자주 변경되지 않는 페이지(about 등)에 대부분 사용되어 만들어진다. ㄸ라서 웹 사이트의 성격에 맞게 각각 페이지 특성에 맞게 적절히 섞어 사용하는것이 가장 좋다.

컴파일과 고레벨, 저레벨 언어

|

개인적인 연습 내용을 정리한 글입니다.
잘못된 내용이 있다면 편하게 댓글 남겨주세요!


컴파일?

고급프로그래밍 언어에 쓰여진 프로그램으로 소스코드에서의 오브젝트로 변환되는 것

  • 소스코드: 프로그래밍을 위한 일련의 텍스트들
  • 오브젝트 코드: 컴파일에 의해 생성된코드

프로그래머는 소스코드를 작성하는데, 이를 프로그래밍이라고도 한다. 즉, 소스코드는 개발자가 사용하는 언어에 따른 명령어들의 조합이라고 볼 수 있지만 명령어 실행을 위해서는 기계어 즉, 저레벨 언어로 쓰여져야만 하드웨어 제어가 가능하다.

저레벨 언어(low-level language)

  • 기계어, 어셈블리어를 의미
  • 고레벨언어보다 하드웨어와 더 밀접한 언어이다.

고레벨 언어(high-level language)

  • 프로그래밍에 좀 더 특화된 언어이다.
  • 기계어보다 좀 더 인간의 언어에 가깝다.
  • 저레벨 언어보다 읽기 쉬우며, 읽기 뿐만 아니라 쓰기, 유지보수에도 용이하다.
  • 즉, 프로그램을 생산하기 수월하다
  • 그러나 고레벨 언어는 기계어로 변환하기 위한 인터프리터나 컴파일러가 필수적으로 요구된다.

즉, 컴파일러는 고레벨 언어를 저레벨 언어로 변경하기 위해 필요한 장치 또는 도구라고 볼 수 있다.

프로그래밍 자체가 하드웨어를 제어하기 위해 탄생한 것으로 볼 수 있고 이를 조작하기 위해서는 기계어로 작성해야하지만, 기계어로는 프로그래밍을 작성하기 매우 까다롭기 때문에 인간에게 더 친숙한 고레벨 언어가 탄생하였고 이 고레벨 언어로 작성한 프로그램이 작동하기 위해서는 컴파일러가 필요한것이다.

쓰레드(Threads)와 동기화(Synchronization)

|

개인공부 후 자료를 남기기 위한 목적임으로 내용 상에 오류가 있을 수 있습니다.
경성대학교 양희재 교수님 수업 영상을 듣고 정리하였습니다.


쓰레드 (Thread)

프로그램 내부의 흐름, 맥

class Test {
  public static void main(Sting[] args) {
    int n = 0;
    int m = 6;

    System.out.println(n+m);

    while (n < m)
    n++;

    System.out.println("Bye");
  }
}

하나의 프로그램은 하나의 맥이 있고 이러한 맥을 쓰레드라고 한다.

Multithreads

  • 다중 쓰레드(Multithreads)
    • 한 프로그램에 2개 이상의 맥
    • 맥이 빠른 시간 간격으로 스위칭 된다 => 여러 맥이 동시에 실행되는 것처럼 보인다. (concurrent vs simultaneous)
    • 현재의 모든 프로그램들은 다 다중쓰레드를 지원한다.

동시에 돌 수 있는 이유는 맥이 빠른시간으로 스위칭 되기 때문이다.
그래서 실제로 같이 하고 있는 것처럼 보이지만 사실 CPU는 하나이기 때문에 우리는 하나만 사용하고 있다. 우리는 일반적으로 concurrent!

  • 예: Web breoser
    • 화면 출력하는 쓰데르 + 데이터 읽어오는 쓰레드
  • 예: Word Processor
    • 화면 출력하는 쓰레드 + 키보드 입력 받는 쓰레드 + 철자/문법 오류 확인 쓰레드
  • 예: 음악 연주기, 동영상 플레이어, Eclipse IDE…

실제 context switching 되는 단위는 process단위가 아니라 thread단위이다!

Thread vs Process

  • 한 프로세스에는 기본 1개의 쓰레드: 단일 쓰레드(single thread) 프로그램
  • 한 프로세스에 여러개의 쓰레드: 다중 쓰레드 (multi-thread) 프로그램

  • 쓰레드 구조
    • 하나의 프로그램에서 맥이 여러개다.
    • 프로세스의 메모리 공간을 공유한다 (code, data를 쓰레드가 서로 공유)
    • 프로세스의 자원공유(한 쓰레드 안에서 file, io 등을 공유)
    • 비공유: 개별적인 PC(program counter), SP(stack pointer), Registers, stack
      • CPU가 p1에 있는 각각의 쓰레드가 가지고 있는 PC값은 다를 것이다. 그래서 스위칭 될때마다 각자가 가리키고 있는 값이다르니 이 값들은 공유하지 않는다.
      • 스택은 리턴값 혹은 파라미터 값을 저장하기 위해 사용되는데, 서로간에 호출하는 메소드가 다 다를수가 있다.

하나의 프로그램은 코드, 데이터, 스택으로 구성되는데 쓰레드가 코드, 데이터는 공유를 하지만 이때 스택은 따로 가지고 있다.

  • 프로세스의 스위칭 vs 쓰레드의 스위칭
    현대의 모든 운영체제는 context switching 단위가 쓰레드간의 단위이다.

예: 자바 쓰레드

  • 맥 만들기
    • java.lang.Thread
      • 자바에서는 쓰레드가 객체로 이루어져있기 때문에, 쓰레드라는 객체를 만들어줘야함
    • 쓰레드가 가지고 있는 주요 메소드
      • public void(run): 새로운 맥이 흐르는 곳
      • void(start): 쓰레드 시작 요청
      • void(join): 쓰레드가 마치기를 기다림
      • static void(sleep): 쓰레드 잠자기 / 멈추도록

java.lang.Thread

  • Thread.run()

쓰레드가 시작되면 run()메소드가 실행된다(run()메소드를 치환한다)
run()안에서 안에서 새로운 맥이 실행되기 때문에 run()을 치환해야 하고, 치환하려면 쓰레드 클래스에 하위 클래스를 만들어서 치환한다.

class MyThread extends Thread{
  public void run() { // 치환(override)
    // 코드
    while (True) {
      System.out.println("B");
      try {
      Thread.sleep(100);
      } catch (InterruptedException e) ()
    }
  }
}
class Test {
  public static void main(String[] args) {
    MyThread th = new MyThread()
    th.start();
    while (True) {
      System.out.println("A");
      try {
      Thread.sleep(100);
      } catch (InterruptedException e) ()
    }
  }
}
  • 모든 프로그램은 처음부터 1개의 쓰레드는 갖고 있다(main)
  • 2개의 쓰레드: main + MyThread

해당 프로그램이 시작되면 main함수가 먼저 시작할것이다. main함수의 MyThread가 실행되면서 while문의 무한루프가 돌면서 B가 계속해서 찍혀나갈 것이고 그와 동시에 main함수의 while문이 무한루프를 돌면서 A를 계속해서 찍어낼 것이다. 컴파일해보면 A와 B가 섞여서 찍힐 것이다.

프로세스 동기화(Process Synchronization)

더 정확한 표현은 Thread Synchronization이다.
서로간 영향을 주고받는 데이터들간에 데이터의 일관성이 유지될 수 있도록 해주는 것이 동기화이다.

보통 컴퓨터 메모리 안의 프로세스들은 독립적이지 않고 협조하는 관계이다. 즉, 다른 프로세스에게 영향을 주거나 영향을 받는다. 대부분 공통된 자원(메인메모리)을 서로 접근하려고 하다보니 그런것이다. 하나의 메인메모리에 프로세스들이 많으니 어떤 방식으로든 영향을 주고받으니 그럴수록 프로세스 동기화의 개념이 중요해지고 있다.

  • Processes
    • Independent vs Cooperating
    • Cooperating process: one that can affect or be affected by other processes executed in the system(그 시스템 내에서 실행되고 있는 다른 프로세스에 대해서 프로세스 간 영향을 주든지 영향을 받든지하는 프로세스)
    • 프로세스 간 통신: 전자우편, 파일 전송(서로 공유하며 데이터 통신을 함)
    • 프로세스간 자원 공유: 메모리 상의 자료들, 데이터베이스 등(데이터를 공유하기에 서로에게 영향을 줌)
    • 명절 기자표 예약, 대학 온라인 수강신청, 실시간 주식거래
  • Process Synchronization
    • Concurrent access to shared data may result in data inconsistency
    • Orderly execution of cooperating processes so that data consistency is maintained -> 공유 데이터에 동시에 접근하면 데이터의 비일관성을 초래한다.
      -> 서로간 영향을 주고받는 데이터들간에 데이터의 일관성이 유지될 수 있도록 해주는 것이 동기화이다.
  • ex: BankAccount Problem(은행계좌문제)
    • 부모님은 은행계좌에 입금, 자녀는 출금
    • 입금(deposit)과 출금(withdraw)은 독립적으로 일어난다.
class BankAccount {
  int balance;
  void deposit(int n) {
    balance = balance + n;
  }
  void withdraw(int n ){
    balance = balance + n;
  }
  int getBalance() {
    return balance;
  }
}
class Parent extends Thread {
  BankAccount b;
  Parent(BankAccount b) {
    this.b = b;
  }
  public void run() {
    for (int i=0; i<1000;, i++)
      b.deposit(1000);
      System.out.println("+");
  }
}
class Child extends Thread {
  BankAccount b;
  Child(BankAccount b) {
    this.b = b;
  }
  public void run() {
    for (int i=0; i<1000;, i++)
      b.withdraw(1000);
      System.out.println("-");
  }
}
class Test {
  public static void main(String[] args)
  throws InterruptedException {
    BankAccount b = new BankAccount();
    Parent p = new Parent(b);
    Child c = new Child(b);

    p.start();
    c.start();
    p.join();
    c.join();

    System.out.println("balance = " + b.getBalance());
  }
}

잔액은 0원이 나올 것이다.

프로세스 동기화가 안되는 경우의 문제

시간지연에 따라 잘못된 결과를 초래한다.

class BankAccount {
  int balance;
  void deposit(int n) {
    balance = balance + n;
    System.out.println("+");
    balance = temp;
  }
  void withdraw(int n ){
    balance = balance + n;
    System.out.println("-");
    balance = temp;
  }
  int getBalance() {
    return balance;
  }
}
class Parent extends Thread {
  BankAccount b;
  Parent(BankAccount b) {
    this.b = b;
  }
  public void run() {
    for (int i=0; i<1000;, i++)
      b.deposit(1000);
  }
}
class Child extends Thread {
  BankAccount b;
  Child(BankAccount b) {
    this.b = b;
  }
  public void run() {
    for (int i=0; i<1000;, i++)
      b.withdraw(1000);
  }
}
class Test {
  public static void main(String[] args)
  throws InterruptedException {
    BankAccount b = new BankAccount();
    Parent p = new Parent(b);
    Child c = new Child(b);

    p.start();
    c.start();
    p.join();
    c.join();

    System.out.println("balance = " + b.getBalance());
  }
}

시간지연에도 불구하고 결과는 바르게 나와야하는데, 이를 해결해야하는 것을 동기화라고 한다.
-> context switching이 중간에 일어나게 되면 이상한 결과를 초래할 수 있다.
-> 공통변수(common variable, balance)에 대한 동시 업데이트(concurrent update)로 인해 발생!
(즉, 하이레벨 랭귀지는 한줄이지만 로우레벨은 여러줄인데 업데이트 도중에 스위칭이 발생)
-> 해결: 공통변수에 대해 한번에 한 쓰레드만 업데이트 하도록한다! 임계구역 문제 = atomic하게!

임계구역 문제(Critical-Section Problem)

Critical section(임계구간, 치명적인 구간): 이 구간에서 아주 치명적인 오류가 발생할 수 있다.

A system consisting of multiple threads. Each thread has a segment of code, called critical section, in which the thread may be changing common variables, updating a table, writing a file, and so on

여러개의 쓰레드로 구성된 시스템에서 각각의 쓰레드는 어떤 코드의 영역을 가지고 있는데, 그 코드의 영역을 임계구역이라고 한다. 이 임계구역이 되기 위해서는 여러 쓰레드들이 공통의 변수, 테이블, 파일들을 바꾸는 부분 코드를 임계구역이라고 한다.

  • Solution (3개가 모두다 만족되어야 한다.)
    • Mutual exclusive(상호배타): 오류가 일어나지 않으려면 공통 변수에 대한 업데이트는 오직 한 쓰레드만 들어가 있을 때 진행되어야 한다. 즉 Parent가 critical section에 들어가면 그 순간에는 Child가 critical section에 들어가면 안된다.

    • Progress(진행): 진입 결정은 유한 시간 내에 일어나야 한다. 즉 critical section에 누가 들어갈 것인지를 결정하는 것은 유한 시간 내에 일어나야 한다.

    • Bounded waiting(유한대기): 어느 쓰레드라도 유한 시간 내에 일어나야 한다. 기다리는 시간의 한계가 들어가 있다는 것으로 내가 기다리고 있다면 유한 시간 내에 critical section 안으로 들어갈 수 있다.

프로세스/쓰레드 동기화

  • 임계구역 문제 해결
  • 프로세스 실행 순서 제어(원하는대로)
  • Busy wait 등 비효율성 제거

프로세스 관리에서 중요한 것은 결국 프로세스/쓰레드의 동기화이다. 틀린답이 나오지 않도록 os에서 임계구역 문제를 해결해줘야 한다. 그리고 프로세스/쓰레드의 순서를 임의로 제어할 수 있어야 한다.