Synchronization(13-1, 13-2)

msung99·2022년 11월 24일
0
post-thumbnail

본 포스트팅은 인하대학교 컴퓨터공학과 시스템 프로그래밍 수업자료에 기반하고 있습니다. 개인적인 학습을 위한 정리여서 설명이 다소 미흡할 수 있으니, 간단히 참고하실 분들만 포스팅을 참고해주세요 😎


Shared Variables in Threaded C Program

여러 쓰레드들이 메모리에 있는 한 데이터를 보는경우, shared 되었다고 한다.


Shared Memory Model

Conceptual Model

  • 멀티 쓰레드의 모든 쓰레드들은 하나의 process (single process) 에서 돌아야한다.

  • 각 쓰레드는 본인만의 thread context 를 가지고있다.

    • thread context => ex. thread Id, stack, stack pointer, PC, condition codes, ... 등

    => 이떄 한 쓰레드에 대해 스택이 있을텐데, 이 스택은 다른 쓰레드에 있는 스택 공간이라 남의 스택이 보인다. 즉, 같은 프로세스에 존재하는 모든 쓰레드는 스택의 같은 메모리를 공유한다(shared memory).
    <=> process 는 쓰레드와 달리 남의 스택을 볼수 없었다.

  • 모든 쓰레드들은 존재하는 process context 들을 다 같이 공유한다.

    • process context => code, data, heap 등을 함께공유

for문을 돌면서 thread 라는 이름을 가진 쓰래드를 2개 만들고,
그리고 thread 함수에서 printf 문으로 쓰레드의 id 값을 출력한다.

ptr 은 전역변수인데 main 의 지역변수에 대한 주소값을 건내준다. 쓰레드들이 ptr 을 사용해서 ptr[myid] 로 값을 access 할수있다.

ptr 은 전역변수라서 공유가 되는데, thread 함수의 myid 는 각 쓰레드의 id 로써 공유가되선 안된다.

여기서 문제점은 main 에서 쓰레드를 for문으로 create를 호출한다. 이때 pthread 는 커널이 만들어줄텐데, pthread 가 만들어질때 myid 가 출력되는지, 아니면 for문에서 Pthread_create 로 생성시에 넘겨준 인자값 myid 가 먼저 출력되는지가 문제이다.


Mapping Variable Instances to Memory

  • 전역변수 : 모든 쓰레드가 함께공유
  • 지역변수 : 각 쓰레드가 자신만의 지역변수들을 가지고있다.
  • static 변수 : 모든 쓰레드가 함께공유

  • myid : 각 쓰레드가 자신만의 지역변수를 가지고 있어서 printf 문으로 각 쓰레드의 myid 값이 출력된다. 즉, 공유되지 않느 변수이다.

  • ptr, msg, cnt : 모든 쓰레드들이 공유 => 모든 쓰레드들이 access 하므로

  • i, myid : 지역변수로써 공유x

  • myid 는 쓰레드가 각자 가지고있어야함. 즉 myid 는 2개 생성되어야한다.
  • i,msg : main만 가지고 있는 지역변수.
  • cnt : static 변수라서 쓰레드 2개가 같이 공유한다.
    • 그래서 thread 함수에서 printf 문으로 cnt 를 출력할때 1, 2 가 출력될 것이다.

=> 이렇게 변수들이 공유되는 것은 편하지만, 동기화(Synchornization) 을 안하면 에러가 발생할 수 있다.


Synchornization(동기화) 가 안되서 발생하는 문제 상세과정

  • 쓰레드를 2개 생성하고 join해서 두 쓰레드모두 함수 실행이 완전히 끝나기 전까지 main 함수는 wait 한다. 그리고 main 으로부터 niters 를 넘겨받고, for문에서 cnt 값을 증가시킬 때 사용된다.

  • cnt 는 전역변수인데, 두 쓰레드가 같이 공유한다. 그런데 volatile 키워드를 전역변수 앞에 붙여주면 컴파일러가 손을 대지 말라는 뜻이된다. (컴파일러가 똑똑해서 자동으로 어떤 값들을 cnt 에 할당시켜줄수도 있는데, 그런 짓을 하지 말라는 것)

  • niters = 10000 이라서, 각 쓰레드가 for문을 돌리면 전역변수 cnt 값이 10000, 그리고 20000 이 나와야한다. 그런데 결과를 보면 cnt = 13051 이 된다. 즉 동기화(Synchronization) 이 안됐다는 것이다.

    => 한 쓰레드가 cnt 에 대해 write 하고 있을때, 다른 한 쓰레드도 write 하려고 해서 write 에 실패한 것이다.

    => 한 쓰레드가 cnt 값을 write 하는데, 다른 한 쓰레드가 cnt 의 동일한 값을 write 하는 것이다.


초록색이 쓰레드1, 보라색이 쓰레드2 이다. 위처럼 각자 동떨어져서 수행되면 문제가 없다.

그런데 쓰레드1이 cnt 값을 가져와서 값을 1 증가시키고 증가된 값을 저장을 해야하지만, 이 증가된 값을 저장하기 전에 쓰레드 2가 증가되기 이전의 cnt 값을 가져와버려서 문제가 발생한다.

  • load 과정은 store 과정 뒤에 발생해야 cnt 값이 증가할텐데, 보듯이 쓰레드 1의 Store 과정(S1) 발생 직전에 쓰레드 2의 load 과정(L2) 이 발생해서 예전의 cnt 값을 가져와버린다.

  • 위에서 살펴본 내용을 그래프로써 표현해보겠다. 그러면 쓰레드1은 명령을 실행한만큼 왼쪽으로 이동하고, 쓰레드2는 오른쪽으로 이동하는 그래프가 그려질 것이다.

  • L : load, U : update, S : store, H : head


위처럼 이동했다고 가정하자.

  • 쓰레드1이 head, load, update 과정까지 마치고 store 하려는데, 이떄 쓰레드2가 load, update 한다. (여기서부터 잘못되었음을 알 수 있다.)

  • 그러면 쓰레드 1이 다시 store, tail 까지 했다. 그러고나서 쓰레드2가 update, store, tail 하고 끝이난다.

  • 앞서 말했듯이, 쓰레드1이 load 하고 store 하기 전까지는 쓰레드2는 load 해오면 절대 안된다. 그러면 동일한 값을 쓰레드2가 또 가져와서 update 하고 store 하기 때문이다.

  • 위의 unsafe region 에 들어가면 연산이 이상하게 수행되어서, 결과값이 이상해지는 것이다.


  • 위의 파란색 라인을 따라가는 과정은 safe 한 방법이나,
  • 빨간색 라인은 비정상줙인 연산이 수행되는 것이다.

Mutually exclusive programming (Mutual Exclusion)

  • 위에서 살펴본 unsafe region 에 들어가지 않도록 하는 프로그래밍을 말한다. 즉 동기화해주는 것이다.

Semaphores

  • 동기화된 양의 정수를 저장하는 전역변수
    (non-negative global integer synchronization variable)

아래와 같은 2가지의 연산 기능을 가지고있다.

P(s)

  • 전역변수 값이 s만큼 감소하도록 동기화를 보장해주는 함수

V(s)

  • 전역변수 값이 s만큼증가하도록 동기화를 보장해주는 함수

Semaphores 사용해서 동기화문제 해결하기 : Mutex

아래 예제는 앞서 살펴봤던 동기화 문제가 발생했던 코드이다. 어떻게 해결할까?

  • 1짜리 semaphore 를 사용한다. 이를 Mutex 라고 부른다.
    => 아무튼 cnt 값을 바꾸는 코드를 P(1) 과 V(1) 로 대체하면 된다.

아래처럼 sem init 을 해서 mutex 를 하나 만들어놓고,

P 와 V로 둘러싸주면 된다(대체하면 된다).
=> P와 V 덕분에 cnt++ 이 명령은 동시간에 한 쓰레드밖에 처리하지 못하게 된다. (동기화 작업이 된것이다!)

  • Mutual Exclusion 을 사용하고 싶다면, 와 V 를 가지고 위험한 부분을 위처럼 감싸주면 된다.

그래서 결과를 보듯이 정상적으로 cnt 값이 20000이 출력된다.


Mutex 의 동작원리

아까와 똑같은 그림이다. 초기값은 1을 부여해준다. L, U, S 구간으로 들어가는 순간 1이 빼진다.

=> P(s) 에서 시작해서 L,U,S 구간으로 들어가는 순간에 1이 빠지고, 나오는 순간, 즉 V(s) 에 오면 1이 다시 증가한다. (P(s) 함수를 호출하는 순간 1이 빠진다.)

unsafe region 부분은 둘다 1을 뺀것이라서 1 -1 -1 = -1 이 되는것이다. 그런데 Semaphores 특성상 저 region 구역이 못들어가도록 막아준다.


쓰레드의 Concurrent Server

  1. 여러 클라이언트에 대한 connection 요청들을 Master thread 가 accept 한다.

  2. Buffer 메모리 공간에 desciprtors 와 함께 내용을 저장한다.

  3. Worker thread 들은 이 Buffer 의 내용을 공유한다. 그리고 descriptor 를 가지고 각 쓰레드끼리 데이터를 주고받고 통신한다.


요청이 들어올때마다 쓰레드를 만드는 지난번 방식과 달리, 쓰레드를 미리 만들어놓고 대기한다.

=> 대기하고 있다가 요청이 들어올 때 마다 처리해준다. 이러한 방식을 쓰레드 풀이라고 한다.

  • while문 안에서 connect 요청을 계속 기다리고있는다.

  • 매 요청을 accep 할때마다 connected desciptor 를 버퍼에 넣어준다.

  • 쓰레드는 미리 만들어졌으므로, 공유하고 있는 데이터를 관리하고 있다가 매 요청이 들어오면 그에 대해 적절히 처리해준다.


  • pthread_once : 첫번쨰로 도착한 쓰레드 딱 하나만 동작하는것

예제의 코드가 중요하는것이 아니라, 쓰레드가 버퍼를 공유해서 사용한다는 것과 그의 메커니즘이 중요하다.


Thread Safety

  • 어떤 함수를 구현했을때, 해당 함수가 thread safe 한지 판단해야한다.

해당 함수가 Thread Safe 하다는 것은, 쓰레드 여러개가 생성되고 해당 여러 쓰레드들이 함수를 호출했을 떄 값이 언제든지 정확히 나오면, 즉 동기화(Synchornization) 이 잘 되어있다면 thread safe 한것이다.

특정 함수의 Thread Safe 하지않은 기준

  • 1) 여러 쓰레드에서 share 하는 변수가 protect 되지않는 함수

  • 2) static 변수와 같은 것들의 state 를 유지하는 함수

  • 3) 해당 함수가 포인터를 리턴하는데, 쓰레드가 해당 포인터의 값을 바꿀 수 있을 때

  • 4) 앞선 1~3번의 함수들을 호출하는 함수 (어떤 함수를 호출하는데, 해당 함수가 safe 하지 않다면 본인도 safe 하지 않다)

=> 이런 함수들을 어떻게 safe 하게 만드는지 알아보자.


1) 여러 쓰레드에서 share 하는 변수가 protect 되지않는 함수

  • P 와 V Semaphore 연산을 사용해서 위험한 구간을 감싸주면 된다.

2) static 변수와 같은 것들의 state 를 유지하지 하는 함수

next 변수가 static 인데, 만일 여러 쓰레드들이 rand 함수를 부른다.

원래대로라면, 함수명 말 그대로 rand 함수이므로 각 쓰레드에 대해 다른 난수 값이 추출되어야한다. 그런데 static 변수인 next에 결과를 계산하고 있는데 이러면 모든 쓰레드에서 동일한 결과가 나온다.

srand 함수로 시드값을 변경해줘야 난수값이 다르게 나온다. 그런데 srand 안의 시드값을 설정해주기 위한 변수로 next 변수가 있다. 그러면 여러 쓰레드들이 srand 를 호출했는데 이처럼 seed 값이 동일하면 난수값이 동일하게 나온다.

그리고 만일 다른 쓰레드가 srand 를 호출할 때, 내가(본인 쓰레드) 가 호출하고 다른 쓰레드가 호출해서 나오는 난수값이 나중에 나오게 되면 next 값이 바뀐다.
즉 내가 난수값이 1이 나왔는데, 다른 곳에서 2가 나와서 overwrite 해버리면 난수값을 만들 수 있다. (뭔 개소리지?)


3) 3) 해당 함수가 포인터를 리턴하는데, 쓰레드가 해당 포인터의 값을 바꿀 수 있을 때

  • 위 예제는 만일 P 와 V 가 없었다면 문제가 발생하는 함수이다.
    => 위 함수를 여러 쓰레드가 동시에 호출한다고 하자.
    그런데 strcpy( ) 로 밖에서 받아온 포인터 privatep 에다 sharedp 를 카피 함으로써 변경을 하려 하고있다. (즉 포인터 privatep 가 가리키는 문자열을 새로운 문자열을 가리키도록 변경하려 하고있다.)
  • 해결법1 : 이렇게 포인터를 직접적으로 건들이지 말자. 함수에서 포인터를 주고받을 때는 파라미터로만 건내받고, 리턴을 할떄는 포인터를 직접 리턴하지 말고 포인터가 가리키는 값을 리턴시키는 방식으로 하자.

  • 해결법2 : lock-and-copy => P 와 V 로 감싸는것.


4) 앞선 1~3번의 함수들을 호출하는 함수 (어떤 함수를 호출하는데, 해당 함수가 safe 하지 않다면 본인도 safe 하지 않다)

  • thread-unsafe 한 함수를 호출하는 것을 지양하자.

Reentrant Functions

  • 함수를 기분할때 threadsafe한 함수인지, unsafe한 함수를 구분해 놓고 thread-safe 함수들 중에서도 완전 safe한 함수들

  • c언어의 standard library (표준 라이브러리) 에 있는 함수들은 thread safe 하다. (ex malloc, free, printf, scanf, ... 등)

Race 발생

  • Synchornalization(동기화) 를 할때 Race 가 발생할 수 있다.

아까 살펴봤던 예제와 거의 똑같은 것이다(10000씩 더하던 아까 예제).
아까 예제처럼, 어떤 쓰레드가 먼저 받는가에 따라서 아웃풋이 달라질 수 있는것이다. 결국 myid 가 같게 출력되는 쓰레드들이 많이 생길 수 있다.


  • main 쓰레드로부터 for문을 한번 돌릴떄 마다 하나의 pthread 를 생성할텐데, 여기서 문제는 i=0 이 i=1 으로 변하는것이 먼저인가, 아니면 0을 가져와서 myid 에 할당하는것이 먼저인가가 관건이다.

둘 중에 어떤것이 먼저 수행될지는 아무도 모른다. 운영체제가 때에 따라서 수행한다.


i가 들어갔다면 여러개의 쓰레드들이 race 상황 떄문에 같은 값을 가져갈 수 있다.
100개의 각 쓰레드가 자신의 i값에 대해 0~99 까지 순차적으로 잘 할당이되지 않는다.(race 발생) 여러 쓰레드들이 동일한 i값을 할당받을 수 있다.


race 해결법

  • race가 발생한 근본적인 이유는, 여러 쓰레드들이 같은 i 값을 보고 있어서 그렇다. 즉 i값이 for문을 돌리면서 증가하고 있는데, 반복문 순환중 특정 타이밍의 i값을 본인이(특정 쓰레드가) 내 고유 메모리에 언제 복사해오는가에 따라서 할당되는 값이 달라진다.

    너무 빨리 복사해버리면 i의 직전 값을 복사해올 수도 있고, 반대로 너무 늦게 복사해버리면 이미 i가 증가해벼렸을 수도 있다.

=> 해결법

i를 그냥 주지말고, malloc 을 해서 메모리 하나를 별도로 만든다음에 넘겨주면 된다. 그러면 malloc 을 100번 해서 100개의 별도의 공간이 생기며, 각자의 쓰레드는 자기 공간에 있는 본인의 i 값을 가져올 것이다.

즉, 이렇게하면 i를 공유하지 않게 된것이다. main 쓰레드가 각 쓰레드한테 메모리 공간(malloc)을 각각 만들어주고, 생성된 메모리 공간(malloc) 을 넘겨주기 때문이다.


Deadlock 발생

프로세스가 resources 를 사용하기 위해 가져올때 lock 을 다른 프로세스 누군가 놓지 못하는 상황

ex) 프로세스 1,2 가 있는고 resource A,B 가 있을때
프로세스1 이 A를 가지고 있고 resource B를 가져오기 위해 기다리고 있고,
프로세스2가 B를 가지고 있고 resource A를 가져오기 위해 기다리고 있는 경우
=> 두 프로세스는 서로 본인의 자원을 놓지 못하며, 계속 기다리고있는 상황이 된다.


우선 위처럼 쓰레드를 2개 생성하는데,


Deadlock 해결법 : 그냥 어려우니, Synchornization 을 해야할 상황이 생긴다면 위처럼 순서를 똑같이 맞춰주면 된다.

profile
블로그 이전 : https://haon.blog

0개의 댓글