Bài 45.528 phút đọc
Atomic, semaphore, và thread pool
Sau bài này bạn sẽ làm được
- Dùng thao tác nguyên tử thay mutex cho phép đơn giản
- Dùng semaphore để đếm tài nguyên
- Nhận ra và sửa false sharing
- Cài bể luồng với hàng đợi công việc
Bài cuối chương gom bốn công cụ nâng cao: thao tác nguyên tử nhanh hơn mutex cho phép đơn giản, semaphore để đếm tài nguyên, false sharing là bẫy hiệu năng ẩn, và thread pool là mẫu thiết kế của mọi web server.
#Atomic
atm.c, nguyên tử thay mutex
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#define N 4
#define LAN 1000000
static atomic_long dem = 0;
static void *tang(void *a) {
(void)a;
for (int i = 0; i < LAN; ++i)
atomic_fetch_add(&dem, 1); /* nguyen tu, KHONG can mutex */
return NULL;
}
int main(void) {
pthread_t t[N];
for (int i = 0; i < N; ++i) pthread_create(&t[i], NULL, tang, NULL);
for (int i = 0; i < N; ++i) pthread_join(t[i], NULL);
printf("mong doi %d, thuc te %ld\n", N * LAN, (long)atomic_load(&dem));
return 0;
}terminal
gcc -O2 -std=c11 -pthread atm.c -o atm.exe && ./atm.exe
mong doi 4000000, thuc te 4000000
API stdatomic
#include <stdatomic.h>
atomic_long dem = 0;
atomic_fetch_add(&dem, 1); /* dem += 1, nguyen tu */
atomic_store(&co, 1); /* ghi nguyen tu */
long v = atomic_load(&dem); /* doc nguyen tu */
/* Compare-and-swap: nen tang cua cau truc lock-free */
long ky_vong = 5;
atomic_compare_exchange_strong(&dem, &ky_vong, 10);
/* neu dem == ky_vong (5) thi dat dem = 10 va tra true;
neu khong, cap nhat ky_vong = gia tri hien tai va tra false */#Semaphore
Semaphore
Một bộ đếm tài nguyên với hai thao tác nguyên tử:
sem_wait giảm một (chặn nếu đã về 0), sem_post tăng một. Khởi tạo với N nghĩa là cho tối đa N luồng qua cùng lúc. Mutex là semaphore với giá trị 1.sem.c, giới hạn số luồng đồng thời
#include <semaphore.h>
static sem_t sem;
static void *w(void *a) {
(void)a;
sem_wait(&sem); /* lay mot ve, giam 1; chan neu = 0 */
/* toi da 3 luong o day cung luc */
lam_viec();
sem_post(&sem); /* tra ve, tang 1 */
return NULL;
}
int main(void) {
sem_init(&sem, 0, 3); /* 3 ve: toi da 3 luong cung luc */
/* ... tao 10 luong ... */
sem_destroy(&sem);
return 0;
}terminal
gcc -O2 -pthread sem.c -o sem.exe && ./sem.exe
cao nhat 3 luong dong thoi (gioi han 3)
#False sharing
#Thread pool
Thread pool
Tạo sẵn N luồng lúc khởi động, cho chúng lấy công việc từ một hàng đợi chung. Tránh chi phí tạo và huỷ luồng cho mỗi việc, và đặt trần cứng cho số luồng đồng thời. Là nền tảng của mọi web server.
Cấu trúc bể luồng
typedef struct {
pthread_t *luong;
size_t n_luong;
CongViec *hang_doi;
size_t dau, cuoi, dem, cap;
pthread_mutex_t m;
pthread_cond_t co_viec; /* co viec de tho lay */
int dang_dung;
} ThreadPool;
/* Moi tho chay vong lap: lay viec tu hang doi, lam, lap lai */
static void *tho(void *arg) {
ThreadPool *tp = arg;
for (;;) {
pthread_mutex_lock(&tp->m);
while (tp->dem == 0 && !tp->dang_dung)
pthread_cond_wait(&tp->co_viec, &tp->m); /* cho viec */
if (tp->dem == 0 && tp->dang_dung) {
pthread_mutex_unlock(&tp->m);
return NULL; /* thoat sach */
}
CongViec cv = tp->hang_doi[tp->dau];
tp->dau = (tp->dau + 1) % tp->cap;
--tp->dem;
pthread_mutex_unlock(&tp->m);
cv.ham(cv.arg); /* LAM viec NGOAI vung khoa */
}
}Một luồng mỗi việc
for (;;) {
int fd = accept(sv, ...);
pthread_t t;
pthread_create(&t, NULL, phuc_vu, fd); /* luong MOI moi ket noi */
pthread_detach(t);
}
/* Voi 10000 ket noi -> 10000 luong. Moi luong ton ngan xep (mac
dinh vai MB ao) va chi phi tao. He thong sup do o quy mo lon. */
Bể luồng
ThreadPool *tp = tp_tao(16); /* 16 tho co dinh */
for (;;) {
int fd = accept(sv, ...);
tp_them_viec(tp, phuc_vu, fd); /* day vao hang doi, tho lay */
}
/* 16 tho phuc vu vo so ket noi. Khong tao luong moi, tran cung
16, chi phi on dinh. Do la kien truc cua web server. */
Tự làm thử
- Sửa bài đếm bằng
atomic_fetch_addvà so tốc độ với bản mutex. - Dùng semaphore giới hạn ba luồng vào một vùng cùng lúc.
- Viết hai phiên bản, có và không có đệm giữa hai biến, và đo chênh lệch tốc độ của false sharing.
- Gom biến cục bộ rồi cộng một lần thay vì tăng biến chung mỗi vòng, và đo cải thiện.
- Cài bể luồng hoàn chỉnh với hàng đợi công việc và thoát sạch.
- Tính tổng mảng một trăm triệu phần tử bằng N luồng, đo tăng tốc theo N là một, hai, bốn, tám, và giải thích vì sao không tuyến tính.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Atomic nhanh hơn mutex cho một biến và một phép đơn giản; mutex cho nhiều thao tác phải nguyên tử cùng nhau.
- Semaphore đếm tài nguyên, cho N luồng qua cùng lúc; mutex là semaphore giá trị 1 có quyền sở hữu.
- False sharing: hai biến cùng cache line làm chậm nhiều lần mà không có lỗi logic; đệm hoặc căn chỉnh để tách.
- Bể luồng tạo sẵn N thợ lấy việc từ hàng đợi, tránh chi phí tạo luồng và đặt trần cứng.
- Bể luồng đúng cần
whilequanhcond_wait, làm việc ngoài vùng khoá, và cờ thoát sạch.