
개념을 잡기 전에..
결론적으로 시스템 전체의 신뢰와 안정성을 보장하기 위한 방법이다.
저자: 앨런 B. 다우니 (Allen B. Downey)
버전 2.2.1
The Little Book of Semaphores 다운로드 링크
소프트웨어 동기화를 이해하려면 컴퓨터 프로그램이 어떻게 실행되는지에 대해 이해해야 한다.
내일 점심을 내가 bob보다 먼저 먹도록 보장할 수 있는 방법이 있을까?
당신과 친구 밥(Bob)이 각각 다른 도시에 살고 있다.
어느 날 저녁 “오늘 점심을 누가 먼저 먹었을까?”가 궁금해졌다고 생각해보자. 어떻게 확인할 수 있을까?
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>
sem_t done_eating; // Alice가 점심을 끝냈음을 알리는 세마포어
void *alice(void* arg)
{
printf("Alice: 점심 시작!\n");
sleep(2); // 점심 먹는 중
printf("Alice: 점심 다 먹었어. 이제 Bob 시작해도 돼!\n");
sem_post(&done_eating); // Alice가 signal() 보냄
return NULL;
}
void *bob(void* arg)
{
printf("Bob: Alice 신호 기다리는 중...\n");
sem_wait(&done_eating); // Bob은 wait() -> Alice의 signal() 전까지 대기
printf("Bob: 이제 점심 시작!\n");
return NULL;
}
int main()
{
pthread_t t1, t2;
sem_init(&done_eating, 0, 0); // 초기값 0 (Alice가 먼저 signal 해야 Bob이 실행 가능)
pthread_create(&t1, NULL, alice, NULL);
pthread_create(&t2, NULL, bob, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&done_eating);
return 0;
}

클럭이 비교적 정확하더라도 정밀도에는 한계가 있다. 대부분의 시스템에서는 이벤트가 발생한 시점을 기록하지 않는다. 너무 많은 일이 너무 빠르게 일어나기 때문에 모든 시점을 기록하는 것은 불가능하다. (로그가 너무 많아짐. 그래서 중요한 사건만 기록한다. context switch, I/O 완료, 락 획득 등 중요한 시스템 이벤트만)
threadA
x = 5 #(a1)
print(x) #(a2)
threadB
x = 7 #(b1)
출력이 7인데, 최종값이 5인 경우가 가능할까?
| 순서 | output | value |
|---|---|---|
| 쓰레드 A만 | 5 | 5 |
| a1 -> b1 -> a2 | 7 | 7 |
| a1 -> a2 -> b1 | 5 | 7 |
A: 가능하지 않다.
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
sem_t done_write; // Thread B가 x=7을 완료했음을 알리는 세마포어
int x;
void *threadA(void* arg)
{
sem_wait(&done_write); // B가 쓰기 끝낼 때까지 기다림
printf("Thread A: x = %d\n", x); // 출력은 항상 최종 값
return NULL;
}
void *threadB(void* arg)
{
x = 7;
sem_post(&done_write); // B가 쓴 뒤 A에게 신호
return NULL;
}
int main()
{
pthread_t t1, t2;
sem_init(&done_write, 0, 0);
pthread_create(&t2, NULL, threadB, NULL); // B 먼저 실행 가능
pthread_create(&t1, NULL, threadA, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&done_write);
return 0;
}
1) 업데이트: 현재 값을 읽고, 그 값을 기반으로 새 값을 계산하여 저장하는 것
2) 두 개의 스레드가 동시에 공유 변수 count를 업데이트 하는 예
프로그래머의 의도는 2를 원하지만, 최종값이 1이 된다.
threadA
count = count + 1
threadB
count = count + 1
3) 어떻게 해결?
모든 업데이트 및 쓰기 연산은 atomic하지 않다고 가정하고, 동기화 제약 조건을 명시적으로 적용해야 한다.
100개의 스레드가 다음 코드를 동시에 실행한다고 가정한다.
race condition(경쟁 조건)
for i in range(100):
temp = count
count = temp + 1
// race condition
// 10000 보장 x, 동기화가 없어서 실행마다 결과가 달라진다.
#include <stdio.h>
#include <pthread.h>
#define THREADS 100
int count = 0;
void *worker(void *arg)
{
int i;
int temp;
(void)arg;
for (i = 0; i < 100; i++)
{
temp = count;
count = temp + 1;
}
return (NULL);
}
int main(void)
{
pthread_t t[THREADS];
int i;
for (i = 0; i < THREADS; i++)
pthread_create(&t[i], NULL, worker, NULL);
for (i = 0; i < THREADS; i++)
pthread_join(t[i], NULL);
printf("Final count = %d\n", count);
return (0);
}
질문 1. 모든 스레드가 종료된 후, count 의 최댓값은 얼마일 수 있을까?
경쟁 조건이 없다면, 10000.
질문 2. 최솟값은 얼마일까?
100개의 스레드, 100번의 반복 -> 10000번의 연산이지만, 100번 반복(loop) 모두 동시 충돌이라면 100개가 가능하다.
추가 질문. 그렇다면 이 race condition을 해결하는 방법을 알아보자
1) 원자적(atomic)
참고 출처: gcc.gnu.org 7.9.2 Legacy __sync Built-in Functions for Atomic Memory Access
#include <stdio.h>
#include <pthread.h>
#define THREADS 100
#define LOOP 100
int count = 0;
void *worker(void *arg)
{
for (int i = 0; i < 100; i++)
{
__sync_fetch_and_add(&count, 1);
}
return NULL;
}
int main() {
pthread_t threads[THREADS];
for (int i = 0; i < THREADS; i++)
pthread_create(&threads[i], NULL, worker, NULL);
for (int i = 0; i < THREADS; i++)
pthread_join(threads[i], NULL);
printf("Final count = %d\n", count);
return 0;
}
2) mutex -> 자원 보호에 특화(오직 1개의 스레드만 보호 구역에 들어올 수 있다. lock. 열쇠는 하나뿐이다!)
#include <stdio.h>
#include <pthread.h>
#define THREADS 100
int count = 0;
pthread_mutex_t lock;
void *worker(void *arg)
{
int i;
int temp;
(void)arg;
for (i = 0; i < 100; i++)
{
pthread_mutex_lock(&lock); //임계 구역 보호
temp = count;
count = temp + 1;
pthread_mutex_unlock(&lock);
}
return (NULL);
}
int main(void)
{
pthread_t t[THREADS];
int i;
pthread_mutex_init(&lock, NULL);
for (i = 0; i < THREADS; i++)
pthread_create(&t[i], NULL, worker, NULL);
for (i = 0; i < THREADS; i++)
pthread_join(t[i], NULL);
pthread_mutex_destroy(&lock);
printf("Final count = %d\n", count);
return (0);
}
3) semaphore -> 위 뮤텍스 코드와 동일한 동작을 하는 코드.
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#define THREADS 100
int count = 0;
sem_t lock;
void *worker(void *arg)
{
int i;
int temp;
(void)arg;
for (i = 0; i < 100; i++)
{
sem_wait(&lock);
temp = count;
count = temp + 1;
sem_post(&lock);
}
return (NULL);
}
int main(void)
{
pthread_t t[THREADS];
int i;
sem_init(&lock, 0, 1); // 세마포어 초기화 여기서 1
for (i = 0; i < THREADS; i++)
pthread_create(&t[i], NULL, worker, NULL);
for (i = 0; i < THREADS; i++)
pthread_join(t[i], NULL);
sem_destroy(&lock);
printf("Final count = %d\n", count);
return (0);
}
valgrind --tool=helgrind ./a.out
valgrind --tool=drd ./a.out
==1686868== drd, a thread error detector
==1686868== Copyright (C) 2006-2020, and GNU GPL'd, by Bart Van Assche.
==1686868== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==1686868== Command: ./test
==1686868==
==1686868== Thread 3:
==1686868== Conflicting load by thread 3 at 0x00404044 size 4
==1686868== at 0x401169: worker (in /home/borlee/assignment/tlpi/threads/borlee/test)
==1686868== by 0x485437A: ??? (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==1686868== by 0x4929AC2: start_thread (pthread_create.c:442)
==1686868== by 0x49BAA03: clone (clone.S:100)
==1686868== Allocation context: BSS section of /home/borlee/assignment/tlpi/threads/borlee/test
==1686868== Other segment start (thread 2)
==1686868== (thread finished, call stack no longer available)
==1686868== Other segment end (thread 2)
==1686868== (thread finished, call stack no longer available)
==1686868==
==1686868== Conflicting store by thread 3 at 0x00404044 size 4
==1686868== at 0x401179: worker (in /home/borlee/assignment/tlpi/threads/borlee/test)
==1686868== by 0x485437A: ??? (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==1686868== by 0x4929AC2: start_thread (pthread_create.c:442)
==1686868== by 0x49BAA03: clone (clone.S:100)
==1686868== Allocation context: BSS section of /home/borlee/assignment/tlpi/threads/borlee/test
==1686868== Other segment start (thread 2)
==1686868== (thread finished, call stack no longer available)
==1686868== Other segment end (thread 2)
==1686868== (thread finished, call stack no longer available)
==1686868==
Final count = 10000
==1686868==
==1686868== ERROR SUMMARY: 19800 errors from 2 contexts (suppressed: 10 from 6)
==1686868==
==1686868== 9900 errors in context 1 of 2:
==1686868== Conflicting store by thread 3 at 0x00404044 size 4
==1686868== at 0x401179: worker (in /home/borlee/assignment/tlpi/threads/borlee/test)
==1686868== by 0x485437A: ??? (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==1686868== by 0x4929AC2: start_thread (pthread_create.c:442)
==1686868== by 0x49BAA03: clone (clone.S:100)
==1686868== Allocation context: BSS section of /home/borlee/assignment/tlpi/threads/borlee/test
==1686868==
==1686868==
==1686868== 9900 errors in context 2 of 2:
==1686868== Conflicting load by thread 3 at 0x00404044 size 4
==1686868== at 0x401169: worker (in /home/borlee/assignment/tlpi/threads/borlee/test)
==1686868== by 0x485437A: ??? (in /usr/libexec/valgrind/vgpreload_drd-amd64-linux.so)
==1686868== by 0x4929AC2: start_thread (pthread_create.c:442)
==1686868== by 0x49BAA03: clone (clone.S:100)
==1686868== Allocation context: BSS section of /home/borlee/assignment/tlpi/threads/borlee/test
==1686868==
--1686868--
--1686868-- used_suppression: 10 drd-ld /usr/libexec/valgrind/default.supp:566
==1686868==
==1686868== ERROR SUMMARY: 19800 errors from 2 contexts (suppressed: 10 from 6)
1) 핵심 문제
Conflicting load by thread 3 at 0x00404044 size 4
Conflicting store by thread 3 at 0x00404044 size 4
2) 어디서 발생했는가
cc -fsanitize=thread -g -pthread 소스코드명.c -o 실행파일
gdb ./실행파일
위 정리와는 무관한 그냥 참고 영상이다.
[10분 테코톡] 🎲 와일더의 Mutex vs Semaphore
#include <stdio.h>
#include <pthread.h>
#define THREADS 100
#define LOOP 100
int count = 0;
void *worker(void *arg)
{
for (int i = 0; i < 100; i++)
{
__sync_fetch_and_add(&count, 1);
}
return NULL;
}
int main()
{
pthread_t threads[THREADS];
for (int i = 0; i < THREADS; i++)
pthread_create(&threads[i], NULL, worker, NULL);
for (int i = 0; i < THREADS; i++)
pthread_join(threads[i], NULL);
printf("Final count = %d\n", count);
return 0;
}
위 코드를 실행하고
gdb run을 실행
아래 내용은 이 중간의 "info th"의 결과다
(gdb) info th
Id Target Id Frame
1 Thread 0x7ffff7fa2740 (LWP 1269368) "test" clone3 ()
at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:62
* 2 Thread 0x7ffff7bff640 (LWP 1269370) "test" worker (arg=0x0) at test.c:20
3 Thread 0x7ffff73fe640 (LWP 1269384) "test" worker (arg=0x0) at test.c:18
4 Thread 0x7ffff6bfd640 (LWP 1269385) "test" worker (arg=0x0) at test.c:18
5 Thread 0x7ffff63fc640 (LWP 1269386) "test" clone3 ()
strace -c -f ./실행파일
strace: Process 1273835 attached
strace: Process 1273836 attached
strace: Process 1273837 attached
..
..
..
strace: Process 1273932 attached
strace: Process 1273933 attached
strace: Process 1273934 attached
Final count = 10000\n% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
28.69 0.008284 82 100 clone3
26.29 0.007590 18 401 rt_sigprocmask
9.90 0.002857 28 100 madvise
7.75 0.002238 21 104 mprotect
7.42 0.002141 19 108 mmap
6.85 0.001978 19 101 rseq
6.12 0.001767 17 101 set_robust_list
4.60 0.001327 13 97 munmap
1.76 0.000509 509 1 execve
0.11 0.000032 10 3 brk
0.08 0.000024 12 2 openat
0.08 0.000023 7 3 newfstatat
0.08 0.000022 5 4 pread64
0.05 0.000013 13 1 write
0.04 0.000012 6 2 close
0.04 0.000011 11 1 1 access
0.04 0.000011 5 2 1 arch_prctl
0.03 0.000008 8 1 read
0.03 0.000008 8 1 getrandom
0.02 0.000007 7 1 prlimit64
0.02 0.000006 6 1 rt_sigaction
0.02 0.000005 5 1 set_tid_address
------ ----------- ----------- --------- --------- --------
[참고]