Luồng - Thread
Học để hiểu bản chất chứ không chỉ chạy được code.
Luồng là một thành phần và là đơn vị thực thi nhỏ nhất của tiến trình.
Một tiến trình có thể chỉ có một luồng hoặc có nhiều luồng chạy song song để xử lý các công việc khác nhau.
Giả sử chúng ta có một bài toán như sau: tạo một phần mềm vừa tính toán xử lý dữ liệu rất lớn tốn thời gian (CPU-bound), vừa phải thực hiện truy vấn cơ sở dữ liệu để lấy thông tin (I/O-bound).
Trong những trường hợp như vậy có hai hướng tiếp cận.
Nếu như driver của Database không hỗ trợ API bất đồng bộ, ngoài luồng chính để điều phối chương trình, chúng ta cần thiết kế các luồng chạy song song, một luồng chuyên để tính toán xử lý dữ liệu và một luồng khác chuyên phục vụ truy vấn cơ sở dữ liệu.
Nếu như driver của Database hỗ trợ API bất đồng bộ thì luồng chính có thể gọi API bất đồng bộ, phần I/O sẽ được hệ thống hoặc thư viện xử lý ở tầng bên dưới (thread pool/ event loop), ứng dụng chỉ cần thêm luồng riêng cho tác vụ CPU-bound.
Trên Windows luồng có thể được tạo bằng API CreateThread, trong khi đó trên Linux sử dụng POSIX Threads để tạo và quản lý luồng.
Chúng ta cùng demo một chương trình tạo ra các luồng trên Windows và Linux.
Ví dụ đa luồng trên Linux:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// hàm xử lý cho luồng tính toán
void* Compute(void* args) {
for (int i = 0; i < 10; i++)
{
printf("[Worker thread]::Compute step ... %d\n", i);
sleep(1);
}
return NULL;
}
int main() {
printf("[Main thread]::Start\n");
// tạo luồng tính toán
pthread_t computeThread;
if(pthread_create(&computeThread, NULL, Compute, NULL) != 0)
{
printf("Thread create error\n");
return 1;
}
// luồng chính vẫn đang chạy song song
for (int i = 0; i < 10; i++)
{
printf("[Main thread]::Do other something ... %d\n", i);
sleep(1);
}
// chờ luồng tính toán kết thúc
pthread_join(computeThread, NULL);
printf("[Main thread]::End\n");
return 0;
}
Kết quả chúng ta có 1 luồng mới chạy song song với luồng chính như kết quả dưới đây.
Ví dụ đa luồng trên Windows sử dụng thư viện Win32 API:
#include <stdio.h>
#include <Windows.h>
int x = 0;
// Hàm xử lý cho worker thread
DWORD WINAPI Worker(LPVOID params) {
for (int i = 0; i < 5; i++)
{
printf("Worker thread id = [%lu]: %d\n", GetCurrentThreadId(), i);
// tăng x lên 1
x = x + 1;
Sleep(1000);
}
return 0;
}
int main() {
// ở luồng chính tăng biến x lên 1
x = x + 1;
// Tạo hai luồng
HANDLE threads[2];
for (int i = 0; i < 2; i++)
{
threads[i] = CreateThread(NULL, 0, Worker, NULL, 0, NULL);
if(threads[i] == NULL) {
printf("Create thread failed\n");
return 1;
}
}
// Chờ 2 luồng kết thúc xử lý
WaitForMultipleObjects(2, threads, TRUE, INFINITE);
CloseHandle(threads[0]);
CloseHandle(threads[1]);
// Kiểm tra giá trị biến x
printf("x = %d\n", x);
// Kết thúc luồng chính
printf("Main thread finished\n");
return 0;
}
Kết quả chúng ta có hai luồng chạy song song ngoài luồng chính.

Ở ví dụ trên, biến x trước khi kết thúc hàm main có giá trị là 11, chúng ta có thể thấy x đã được tăng lên 1 bở hàm main và tăng thêm 10 bởi 2 worker thread.
Các thread trên cùng một process chia sẻ chung một không gian địa chỉ (Heap, global, file handler) nhưng mỗi thread có Stack và Contex riêng (register, instruction pointer).

Trên Windows Các thông tin riêng của Tthread như Stack và Contex được Kernel lưu ở TCB (Thread Control Block) của mỗi thread, không nằm trong PCB (Process Control Block) của tiến trình.
Trên linux không phân biệt PCB hay TCB mà các thread đều được biểu diễn bằng struct task_struct, tiến trình là một nhóm các thread chia sẻ tài nguyên.
Lưu ý ở ví dụ trên, biến x là biến dùng chung và được đọc/ghi đồng thời bởi các thread. Để không phát sinh các vấn đề gây ra sai lệch dữ liệu (giá trị x có thể không đảm bảo là 11) chúng ta sẽ cần cơ chế đồng bộ để các thread có thể đọc/ghi an toàn.
Vấn đề này chúng ta sẽ làm rõ ở bài tiếp theo.
The End.
