1. Giới thiệu chung
Giống như các tiến trình (process), luồng (thread) là một cơ chế cho phép ứng dụng thực hiện đồng thời nhiều tác vụ. Một tiến trình có thể chứa nhiều luồng. Tất cả các luồng này đều thực thi độc lập cùng một chương trình và chúng đều chia sẻ cùng một bộ nhớ chung, bao gồm dữ liệu được khởi tạo, dữ liệu chưa được khởi tạo và vùng heap.
Một thread là một lightweight process có thể được quản lý độc lập bởi một bộ lập lịch. Các luồng trong một tiến trình có thể thực thi đồng thời.

Nguyên lý hoạt động:
- Trên một hệ thống mutil-core. Nhiều threads có thể hoạt động song song.
- Nếu một thread bị block, các thread khác vẫn hoạt động bình thường.
- Mỗi khi một thread được tạo, chúng sẽ được đặt trong stack segments.
Thread mang lại nhiều lợi ích hơn so với process trong các trường hợp nhất định. Ta cùng xem xét một ví dụ khi thiết kế network sever trong đó process cha nhận các kết nối từ các cilent và sau đó sử dụng hàm fork() để tạo process con xử lý giáo tiếp với từng client. Điều này cho phép kết nối với nhiều client cùng một lúc. Tuy nhiên, việc này tồn tại các hạn chế sau:
- Rất khó để chia sẻ thông tin giữa các process. Vì process cha và con không chia sẻ bộ nhớ (trừ text segment), chúng ta phải sử dụng một số hình thức giao tiếp giữa các process (thông qua cơ chế IPC) để trao đổi thông tin giữa các process.
- Việc tạo process bằng fork() tương đối tốn thời gian do cần phải nhân đôi các thuộc tính khác nhau của proces.
Thread giải quyết cả 2 vấn đề trên:
- Dữ liệu được chia sẽ giữa các thread trong một tiến trình nhanh và dễ dàng hơn vì chúng cùng nằm trong một không gian bộ nhớ của tiến trình.
- Việc tạo thread nhanh hơn vì không cần phải nhân đôi các thuộc tính do chúng vẫn nằm chung không gian của process.
2. Thao tác với Thread
Hệ điều hành cung cấp API Pthreads bao gồm một số các loại dữ liệu sau:

Tạo Thread
Khi một chương trình được bắt đầu, process lúc này bao gồm một luồng duy nhất, được gọi là main thread. Sử dụng hàm pthread_create() tạo ra một luồng mới.
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine)(void *), void *arg);
- thread: Một con trỏ đến biến kiểu pthread_t, sẽ lưu trữ ID của thread mới được tạo.
- attr: Một con trỏ đến cấu trúc pthread_attr_t chứa các thuộc tính của thread. Bạn có thể chuyển NULL nếu bạn muốn sử dụng các giá trị mặc định.
- start_routine: Một con trỏ đến hàm sẽ được thực thi bởi thread mới.
- arg: Một con trỏ đến đối số (argument) sẽ được truyền cho hàm start_routine.
Đối số thread trỏ đến bộ đệm kiểu pthread_t chứa mã định danh duy nhất cho thread này được sao chép trước khi pthread_create() trả về. Mã định danh này có thể được sử dụng trong các Pthreads calls sau này để tham chiếu đến luồng.
Giá trị trả về:
- Nếu tạo thread thành công, hàm pthread_create trả về 0.
- Nếu có lỗi, nó trả về mã lỗi tương ứng.
Ví dụ: Sử dụng pthread_create tạo ra thread mới
#include <stdio.h>
#include <pthread.h>
void *print_hello(void *arg) {
printf("Hello from the new thread!\n");
return NULL;
}
int main() {
pthread_t new_thread;
if (pthread_create(&new_thread, NULL, print_hello, NULL) != 0) {
fprintf(stderr, "Failed to create a new thread.\n");
return 1;
}
printf("Main thread: New thread has finished.\n");
return 0;
}
Thread IDs
Mỗi thread trong một tiến trình được xác định duy nhất bằng thread ID. ID này được trả về cho người gọi pthread_create() và một thread có thể lấy ID riêng của nó bằng pthread_self().
#include <pthread.h>
pthread_t pthread_self(void);
Hàm trên trả về thread ID của luồng đang gọi hàm
Thread ID sẽ được đại diện bởi kiểu pthread_t. Phần lớn các trường hợp thread ID sẽ là một structure nên để so sánh hai thread ID với nhau ta cần một function có thể thực hiện công việc này (Đối với process ID là một số nguyên thì việc so sánh đơn giản hơn)
Hàm pthread_equal() cho phép chúng ta kiểm tra xem hai ID luồng có giống nhau hay không.
#include <pthread.h>
int pthread_equal(pthread_t t1, pthread_t t2);
Trả về giá trị khác 0 nếu t1 và t2 bằng nhau, nếu không thì trả về giá trị 0.
Ví dụ: Sử dụng hàm pthread_equal()
#include <stdio.h>
#include <pthread.h>
void *start_routine(void *arg) {
printf("Inside the thread function.\n");
return NULL;
}
int main() {
pthread_t thread1, thread2;
if (pthread_create(&thread1, NULL, start_routine, NULL) != 0) {
fprintf(stderr, "Failed to create thread1.\n");
return 1;
}
if (pthread_create(&thread2, NULL, start_routine, NULL) != 0) {
fprintf(stderr, "Failed to create thread2.\n");
return 1;
}
if (pthread_equal(thread1, thread2) != 0) {
printf("thread1 and thread2 do not reference the same thread.\n");
} else {
printf("thread1 and thread2 reference the same thread.\n");
}
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
Trong ví dụ này, chúng ta tạo hai thread (thread1 và thread2) và sau đó sử dụng hàm pthread_equal() để kiểm tra xem chúng có tham chiếu đến cùng một thread hay không. Nếu không, hàm pthread_equal() trả về 0 và thông báo rằng thread1 và thread2 không tham chiếu đến cùng một thread.
Kết thúc Thread
Một thread đang thực thi có thể bị kết thúc bởi một trong số những cách sau:
- Hàm xử lý của thread thực hiện return.
- Hàm xử lý của thread thực hiện gọi pthread_exit().
- Thread bị hủy bỏ bởi hàm pthread_cancel().
- Bất kì threads nào gọi exit() hoặc main thread thực hiện return. Nếu điều này xảy ra thì tất cả các threads còn lại sẽ bị kết thúc ngay lập tức.
Hàm pthread_exit() kết thúc thead đang gọi hàm và chỉ định giá trị trả về có thể nhận được trong một thread khác bằng cách gọi pthread_join().
#include <pthread.h>
void pthread_exit(void *retval);
Đối số retval chỉ định giá trị trả về cho thread. Giá trị được chỉ ra bởi retval không được nằm trên thread’s stack, vì nội dung của ngăn xếp đó không được xác định khi chấm dứt thread.
Nếu main thread gọi pthread_exit() thay vì gọi exit() hoặc thực hiện return thì các thread khác sẽ tiếp tục thực thi.
Đợi kết thúc một Thread với hàm pthread_join()
Hàm pthread_join() được sử dụng để đợi cho đến khi một luồng (thread) kết thúc thực thi. (Nếu như luồng đó đã kết thúc, pthread_join() sẽ trả về ngay lập tức và thực hiện dọn dẹp thread).
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
- thread: Đối tượng kiểu pthread_t của thread mà bạn muốn đợi cho đến khi nó kết thúc.
- retval: Một con trỏ đến con trỏ (pointer to pointer) sẽ chứa giá trị trả về từ thread. Thông thường, giá trị này là NULL hoặc được sử dụng để nhận giá trị trả về từ hàm
Nếu một thread không gọi hàm pthread_detach(xem Phần tiếp theo), thì chúng ta phải đợi nó kết thúc bằng pthread_join(). Nếu chúng ta không thực hiện được điều này thì khi thread kết thúc, nó sẽ tạo ra thread tương đương với một zombie process. Ngoài việc lãng phí tài nguyên hệ thống, nếu tích lũy đủ số lượng zombie thread, chúng ta sẽ không thể tạo thêm thread.
Ví dụ: Sử dụng hàm pthread_join()
#include <stdio.h>
#include <pthread.h>
void *start_routine(void *arg) {
printf("Inside the thread function.\n");
return NULL;
}
int main() {
pthread_t thread1;
if (pthread_create(&thread1, NULL, start_routine, NULL) != 0) {
fprintf(stderr, "Failed to create the thread.\n");
return 1;
}
// Đợi cho đến khi thread1 kết thúc.
if (pthread_join(thread, NULL) != 0) {
fprintf(stderr, "Failed to join the thread.\n");
return 1;
}
printf("Main thread: The thread has finished.\n");
return 0;
}
Tuy nhiên, hàm pthread_join() sẽ block chương trình cho tới khi thread được gọi tới trong hàm kết thúc. Điều này sẽ gây lãng phí tài nguyên không cần thiết. Trường hợp này chúng ta có thể đặt thread vào trạng thái detached thông qua việc gọi pthread_detached().
Pthread_detach()
Theo mặc định, “a thread is joinable”, nghĩa là khi nó kết thúc, một thread khác có thể lấy trạng thái trả về của nó bằng cách sử dụng pthread_join(). Đôi khi, chúng ta không quan tâm đến trạng thái trả về của thread; ta chỉ muốn hệ thống tự động dọn dẹp và xóa thread khi nó kết thúc. Trong trường hợp này, chúng ta có thể đánh dấu là đã tách rời bằng cách thực hiện lệnh gọi pthread_detach() trong thread.
#include <pthread.h>
int pthread_detach(pthread_t thread);
Trả về 0 khi thành công và trả về số âm khi xảy ra lỗi.
Ví dụ về việc sử dụng pthread_detach(), một thread có thể tự tách ra bằng lệnh gọi sau trong chính thread đó.
pthread_detach(pthread_self());
Khi một thread đã gắn với chế độ detached, bạn không thể sử dụng pthread_join() để lấy trạng thái trả về của nó nữa.
pthread_detach() chỉ đơn giản kiểm soát những gì xảy ra sau khi một luồng kết thúc, chứ không phải cách thức và thời điểm nó kết thúc.
3. Thread Synchronization
Trong phần này mô tả hai công cụ mà các thread có thể sử dụng để đồng bộ hóa các hành động của chúng: mutex và các biến điều kiện( condition variables ). Mutexes cho phép các thread đồng bộ hóa việc sử dụng tài nguyên chung, chẳng hạn, một thread sẽ không truy cập vào một biến được chia sẻ cùng lúc khi một thread khác đang sửa đổi nó. Các biến điều kiện thực hiện nhiệm vụ: chúng cho phép các thread thông báo cho nhau rằng một biến dùng chung (hoặc tài nguyên dùng chung khác) đã thay đổi trạng thái.
Ngoài Mutex ( khóa 1 chìa ), Semaphore ( khóa nhiều chìa ) cũng là một công cụ giúp đồng bộ hóa dữ liệu giữa các thread. Công cụ này sẽ được đề cập ở bài viết khác.
Mutex
Một trong những ưu điểm chính của multithread là chúng có thể chia sẻ thông tin thông qua các biến toàn cục. Tuy nhiên, việc chia sẻ dễ gây ra bất đồng bộ dữ liệu: nhiều thread cố gắng sửa đổi cùng một biến cùng một lúc hoặc một thread cố đọc giá trị của một biến trong khi một thread khác đang sửa đổi nó. Điều này sẽ làm cho dữ liệu không đảm bảo tính chính xác.
Thuật ngữ critical section được dùng để chỉ đoạn code truy cập vào vùng tài nguyên được chia sẻ giữa (shared resource) giữa các threads và việc thực thi của nó nằm trong bối cảnh atomic. Tức là, thời điểm đoạn code được thực thi sẽ không bị gián đoạn bởi bất cứ một thread nào truy cập đồng thời vào shared resource đó.
Ví dụ: Chương trình bất đồng bộ dữ liệu khi 2 thread cùng truy cập vào biến sum
#include <stdio.h>
#include<pthread.h>
#include<string.h>
unsigned int sum = 0;
char name[10] = {0};
void *sum_cal1(void *arg)
{
int n = 1000000;
int i = 0;
for(i = 0; i <= n; i++)
{
sum += i;
}
printf("Da tinh tong xong thread1 \n");
return NULL;
}
void *sum_cal2(void *arg)
{
int n = 1000000;
int i = 0;
for(i = 0; i <= n; i++)
{
sum += i;
}
printf("Da tinh tong xong thread2 \n");
return NULL;
}
void *get_name(void *arg)
{
memset(name, 0, sizeof(name));
printf("Nhap ten: ");
fflush(stdin);
scanf("%s", name);
return NULL;
}
int main()
{
pthread_t sum_thread1;
pthread_t sum_thread2;
pthread_t name_thread;
pthread_create(&sum_thread1, NULL, sum_cal1, NULL);
pthread_create(&sum_thread2, NULL, sum_cal2, NULL);
pthread_create(&name_thread, NULL, get_name, NULL);
pthread_join(sum_thread1, NULL);
pthread_join(sum_thread2, NULL);
pthread_join(name_thread, NULL);
printf("Hello %s, tong cac so la: %d \n", name, sum);
return 0;
}
Kết quả trả về kết quả sum khác nhau với mỗi lần thực thi:

Để tránh các sự cố có thể xảy ra khi các thread cố gắng cập nhật một biến được chia sẻ, chúng ta sử dụng một mutex để đảm bảo rằng mỗi lần chỉ một luồng có thể truy cập vào biến đó.
Một mutex có hai trạng thái: locked và unlocked. Tại bất kỳ thời điểm nào, chỉ một thread có thể giữ khóa trên mutex. Cố gắng khóa một mutex đã khóa sẽ bị block hoặc không thành công do có lỗi.
Khi một thread khóa một mutex, nó sẽ trở thành chủ sở hữu của mutex đó. Chỉ chủ sở hữu mutex mới có thể mở khóa mutex.
Việc triển khai mutex nhìn chung thực hiện qua 3 bước:
- Khởi tạo khóa mutex
- Thực hiện hiện khóa mutex cho các shared resource trước khi vào critical section. Thực hiện truy cập vào shared resources.
- Mở khóa/giải phóng khóa mutex.

Khởi tạo Mutex tĩnh
Một mutex là một biến có kiểu pthread_mutex_t. Trước khi có thể sử dụng, mutex phải luôn được khởi tạo. Đối với một mutex được cấp phát tĩnh, ta có thể khởi tạo bằng việc gán cho nó giá trị PTHREAD_MUTEX_INITIALIZER:
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
Khởi tạo Mutex động
PTHREAD_MUTEX_INITIALIZER chỉ có thể được sử dụng để khởi tạo một mutex được cấp phát tĩnh với các thuộc tính mặc định. Trong tất cả các trường hợp khác, chúng ta cấp phát động mutex bằng pthread_mutex_init(). Việc này giúp người dùng có thể thay đổi thuộc tính của mutex
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
- mutex: Con trỏ tới biến kiểu pthread_mutex_t sẽ được khởi tạo.
- attr: Con trỏ tới một cấu trúc pthread_mutexattr_t chứa các thuộc tính của mutex. Thông thường, bạn có thể truyền NULL để sử dụng các giá trị mặc định.
Hàm pthread_mutex_init trả về 0 nếu khởi tạo thành công. Nếu có lỗi, nó trả về một mã lỗi tương ứng.
Khi một mutex được cấp phát động và không cần sử dụng đến nữa, nó nên được hủy bằng cách sử dụng câu lệnh pthread_mutex_destroy().
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
Hàm trả về 0 nếu thành công, trả về mã lỗi nếu không thành công.
Locking và Unlocking một Mutex
Sau khi được khởi tạo, một mutex được mở khóa. Để khóa và mở khóa một mutex, chúng ta sử dụng hàm pthread_mutex_lock() và pthread_mutex_unlock().
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Cả 2 hàm đều trả về 0 nếu thành công và trả về mã lỗi khi thất bại.
Để khóa một mutex, ta truyền mutex đó vào hàm pthread_mutex_lock (). Nếu mutex hiện đang được mở khóa, lệnh gọi này sẽ khóa mutex và return ngay lập tức. Nếu mutex hiện đang bị khóa bởi một thread khác thì pthread_mutex_lock() sẽ block cho đến khi mutex được mở khóa, lúc đó nó sẽ khóa mutex và return.
Hàm pthread_mutex_unlock() mở khóa một mutex đã bị khóa trước đó bởi thread gọi hàm. Sẽ có lỗi khi mở khóa một mutex hiện chưa bị khóa hoặc mở khóa một mutex bị khóa bởi một thread khác.
Ví dụ: Sử dụng Mutex để khắc phục lỗi đã đề cập ở ví dụ trên
#include <stdio.h>
#include<pthread.h>
#include<string.h>
unsigned int sum = 0;
char name[10] = {0};
pthread_mutex_t sum_lock;
void *sum_cal1(void *arg)
{
int n = 10000;
int i = 0;
pthread_mutex_lock(&sum_lock);
for(i = 0; i <= n; i++)
{
sum += i;
}
pthread_mutex_unlock(&sum_lock);
printf("Da tinh tong xong thread1 \n");
return NULL;
}
void *sum_cal2(void *arg)
{
int n = 10000;
int i = 0;
for(i = 0; i <= n; i++)
{
sum += i;
}
printf("Da tinh tong xong thread2 \n");
return NULL;
}
void *get_name(void *arg)
{
memset(name, 0, sizeof(name));
printf("Nhap ten: ");
fflush(stdin);
scanf("%s", name);
return NULL;
}
int main()
{
pthread_t sum_thread1;
pthread_t sum_thread2;
pthread_t name_thread;
pthread_mutex_init(&sum_lock, NULL);
pthread_create(&sum_thread1, NULL, sum_cal1, NULL);
pthread_create(&sum_thread2, NULL, sum_cal2, NULL);
pthread_create(&name_thread, NULL, get_name, NULL);
pthread_join(sum_thread1, NULL);
pthread_join(sum_thread2, NULL);
pthread_join(name_thread, NULL);
pthread_mutex_destroy(&sum_lock);
printf("Hello %s, tong cac so la: %d \n", name, sum);
return 0;
}
Kết quả sau 2 lần thực thi chương trình:

Mutex Deadlocks
Đôi khi, một thread cần truy cập đồng thời hai hoặc nhiều tài nguyên được chia sẻ khác nhau, mỗi tài nguyên được quản lý bởi một mutex riêng biệt. Khi có nhiều hơn một thread đang khóa cùng một tập hợp các mutex, deadlock có thể xảy ra.
Ví dụ về deadlock trong đó mỗi thread khóa thành công một mutex và sau đó cố gắng khóa mutex mà thread kia đã khóa. Cả hai thread sẽ vẫn bị block vô thời hạn.

Cách đơn giản nhất để khắc phục được tình huống trên: giảm số lượng mutex có thể có trong sourcecode
Thông báo tín hiệu thay đổi trạng thái: Condition variables
Một mutex ngăn nhiều thread truy cập vào một biến được chia sẻ cùng một lúc. Biến điều kiện (condition variables) cho phép một thread thông báo cho các thread khác về những thay đổi về trạng thái của biến chia sẻ (hoặc tài nguyên được chia sẻ khác) và cho phép các thread khác chờ thông báo đó.
Condition variable là một biến kiểu pthread_cond_t. Trước khi sử dụng thì ta luôn phải khởi tạo nó.
Condition variable có thể được cấp phát tĩnh hoặc động.
- pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
- int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
Hai hoạt động chính của condition variable là signal và wait. Hoạt động signal là thông báo cho một hoặc nhiều thread đang chờ rằng trạng thái của biến chia sẻ đã thay đổi. Hoạt động wait là block thread cho đến khi nhận được thông báo trên.
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
Cả hai hàm pthread_cond_signal() và pthread_cond_broadcast() đều báo hiệu biến điều kiện được chỉ định bởi cond. Hàm pthread_cond_wait() block một thread cho đến khi biến điều kiện cond được báo hiệu.
Ví dụ sử dụng condition variables:
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t mutex; // Mutex
pthread_cond_t condition; // Condition variable
int sharedData = 0; // Dữ liệu chia sẻ
void *producer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
sharedData++; // Thay đổi dữ liệu chia sẻ
printf("Producer: Produced data %d\n", sharedData);
pthread_cond_signal(&condition); // Thông báo tín hiệu cho consumer
pthread_mutex_unlock(&mutex);
sleep(1);
}
return NULL;
}
void *consumer(void *arg) {
while (1) {
pthread_mutex_lock(&mutex);
while (sharedData == 0) {
pthread_cond_wait(&condition, &mutex); // Chờ cho đến khi có dữ liệu
}
printf("Consumer: Consumed data %d\n", sharedData);
sharedData--;
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main() {
pthread_t producerThread, consumerThread;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&condition, NULL);
pthread_create(&producerThread, NULL, producer, NULL);
pthread_create(&consumerThread, NULL, consumer, NULL);
pthread_join(producerThread, NULL);
pthread_join(consumerThread, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&condition);
return 0;
}
Trong ví dụ này, chúng ta sử dụng condition variables để đồng bộ hóa giữa producer và consumer. Biến sharedData là tài nguyên chia sẻ mà producer tạo ra và consumer tiêu thụ. Mutex được sử dụng để bảo vệ sharedData và condition variable condition được sử dụng để đợi khi sharedData thay đổi.