The Linux Programming Interface Chương 29 · Threads: Introduction

Chương 29 · Luồng: Giới thiệu Chapter 29 · Threads: Introduction

Nhiều dòng thực thi trong cùng một tiến trình Nhiều việc chạy song song bên trong một tiến trình Many lines of execution inside one process

Kerrisk mở đầu loạt chương về luồng bằng một ý then chốt: một luồng (thread) là một dòng thực thi độc lập bên trong một tiến trình, và một tiến trình có thể chứa nhiều luồng cùng chạy.1 Điểm phân biệt lớn so với tiến trình là các luồng chia sẻ cùng một không gian địa chỉ ảo — cùng bộ nhớ toàn cục, cùng heap, cùng bảng bộ mô tả tệp, cùng các disposition tín hiệu.12 Chương đi qua: API Pthreads và cách biên dịch với -pthread; tạo luồng bằng pthread_create(); kết thúc luồng bằng pthread_exit() và giá trị trả về; thread ID (pthread_self(), pthread_equal()); nối một luồng đã kết thúc bằng pthread_join(); tách luồng bằng pthread_detach(); thuộc tính luồng (pthread_attr_t); và cuối cùng là ưu — nhược điểm của luồng so với tiến trình.1

Hãy hình dung một tiến trình là một văn phòng, còn mỗi luồng là một nhân viên trong đó. Nhiều nhân viên cùng làm việc trong một văn phòng, dùng chung tủ hồ sơ và bảng thông báo — nên trao đổi với nhau rất nhanh. Nhưng mỗi người vẫn có bàn làm việc riêng. Chương này dạy cách thuê thêm nhân viên (tạo luồng), cho họ nghỉ việc (kết thúc luồng), gọi tên họ (thread ID), và chờ họ làm xong một việc (nối luồng). Nhớ nhé: tiến trình là văn phòng, luồng là các nhân viên dùng chung văn phòng đó.

Kerrisk opens the thread chapters with a key idea: a thread is an independent line of execution within a process, and a single process can hold several threads running at once.1 The great distinction from processes is that threads share the same virtual address space — the same global memory, the same heap, the same file-descriptor table, the same signal dispositions.12 The chapter walks through: the Pthreads API and compiling with -pthread; creating a thread with pthread_create(); terminating one with pthread_exit() and the thread return value; thread IDs (pthread_self(), pthread_equal()); joining a terminated thread with pthread_join(); detaching one with pthread_detach(); thread attributes (pthread_attr_t); and finally the advantages and disadvantages of threads versus processes.1

Góc nhìn thực thi: tất cả các luồng của một tiến trình cùng thực thi một chương trình và cùng ở trong một không gian địa chỉ, nhưng mỗi luồng có ngăn xếp riêng để chạy hàm của mình một cách độc lập. Vì bộ nhớ toàn cục là chung, một luồng có thể để lại dữ liệu cho luồng khác đọc chỉ bằng cách ghi vào một biến toàn cục — nhanh hơn nhiều so với các cơ chế liên tiến trình (IPC) mà tiến trình buộc phải dùng. Đổi lại, sự chia sẻ ấy đòi hỏi đồng bộ hóa cẩn thận, chủ đề của chương kế tiếp.12The execution view: all the threads of a process run one program and live in a single address space, yet each thread has its own stack for running its functions independently. Because global memory is shared, one thread can leave data for another simply by writing a global variable — far faster than the inter-process communication (IPC) that separate processes must use. In exchange, that sharing demands careful synchronization, the subject of the next chapter.12

Luận đề của chương: luồng là cách để một tiến trình có nhiều dòng thực thi song song cùng chia sẻ bộ nhớ; hiểu đúng ranh giới giữa cái dùng chung (bộ nhớ ảo, bộ mô tả tệp, disposition tín hiệu) và cái riêng từng luồng (ngăn xếp, errno, mặt nạ tín hiệu, thread ID), cùng với bộ API Pthreads tối thiểu để tạo, kết thúc, nối và tách luồng — là nền tảng cho mọi lập trình đa luồng về sau.12

Ý chính: luồng giúp một chương trình làm nhiều việc cùng lúc mà vẫn dùng chung một kho dữ liệu. Biết cái gì dùng chung, cái gì riêng, và vài lệnh cơ bản để tạo và chờ luồng — là đủ để bắt đầu. Nhớ nhé: luồng = làm song song + dùng chung bộ nhớ.

The chapter's thesis: threads let one process have several parallel lines of execution sharing memory; grasping the boundary between what is shared (virtual memory, file descriptors, signal dispositions) and what is per-thread (the stack, errno, the signal mask, the thread ID), together with the minimal Pthreads API to create, terminate, join and detach threads, is the foundation of all later multithreaded programming.12

Cụm khái niệm 01 Concept cluster 01

Luồng chia sẻ không gian địa chỉ — nhưng mỗi luồng vẫn có ngăn xếp và errno riêng Luồng dùng chung bộ nhớ, nhưng mỗi luồng vẫn có vài thứ riêng Threads share the address space — yet each keeps its own stack and errno

Các luồng của một tiến trình cùng chia sẻ nhiều thuộc tính: cùng PID và PPID, cùng bộ nhớ toàn cục và heap, cùng bảng bộ mô tả tệp đang mở, cùng các disposition tín hiệu (signal dispositions), cùng thư mục làm việc hiện tại, và cùng ID người dùng/nhóm.12 Nhưng mỗi luồng lại giữ riêng một số thuộc tính thiết yếu: thread ID của nó, ngăn xếp riêng (nơi chứa biến cục bộ), biến errno riêng, mặt nạ tín hiệu (signal mask) riêng, cùng chính sách và độ ưu tiên định thời riêng.12

Trong một văn phòng (tiến trình), mọi nhân viên (luồng) dùng chung tủ hồ sơ, bảng thông báo, và danh sách điện thoại đang mở — nên ai cũng thấy được thay đổi của người khác ngay. Nhưng mỗi người vẫn có bàn làm việc riêng, cuốn sổ nháp riêng, và cách nhận thông báo riêng. Nhớ nhé: đồ chung để trao đổi, đồ riêng để làm việc độc lập.

The threads of a process share many attributes: the same PID and PPID, the same global memory and heap, the same table of open file descriptors, the same signal dispositions, the same current working directory, and the same user and group IDs.12 But each thread keeps certain essentials to itself: its thread ID, its own stack (holding local variables), its own errno, its own signal mask, and its own scheduling policy and priority.12

Định nghĩa. Một luồng là một đơn vị thực thi được định thời độc lập, cùng chia sẻ không gian địa chỉ với các luồng anh em trong cùng tiến trình. Vì bộ nhớ toàn cục là chung, một luồng ghi vào một biến toàn cục thì mọi luồng khác đều thấy ngay — đây là cách trao đổi dữ liệu chính giữa các luồng, không cần cơ chế IPC.1

Nói đơn giản. Luồng là một "người làm" trong tiến trình. Vì tất cả cùng nhìn chung một kho dữ liệu, một người ghi gì lên đó thì những người kia đọc được liền. Nhớ nhé: ghi vào biến chung là cách luồng "nói chuyện" với nhau.

Definition. A thread is an independently scheduled unit of execution that shares its address space with sibling threads in the same process. Because global memory is shared, one thread writing a global variable is immediately visible to every other — the primary way threads exchange data, needing no IPC.1

DÙNG CHUNG: text, data, heap,
  bộ mô tả tệp, signal dispositions,
  PID/PPID, cwd, UID/GID
RIÊNG   : thread ID, ngăn xếp,
  errno, signal mask, ưu tiên

CHUNG: bộ nhớ, tệp mở, tín hiệu
RIÊNG: ngăn xếp, errno,
  mặt nạ tín hiệu, tên luồng

SHARED : text, data, heap,
  file descriptors, signal dispositions,
  PID/PPID, cwd, UID/GID
PRIVATE: thread ID, stack,
  errno, signal mask, priority

  1. shared memory

    Bộ nhớ toàn cục là chung — tiện lợi và nguy hiểm Dữ liệu toàn cục dùng chung cho mọi luồng Global memory is shared — convenient and dangerous

    Mọi luồng cùng thấy các biến toàn cục và tĩnh, cùng vùng heap. Điều này làm việc trao đổi dữ liệu trở nên dễ dàng: chỉ cần ghi vào một con trỏ hay một biến toàn cục.1 Nhưng cũng chính vì thế, hai luồng cùng sửa một dữ liệu mà không có trật tự sẽ gây ra điều kiện tranh đua (race condition) — lý do vì sao chương kế tiếp dành trọn cho đồng bộ hóa.1

    Mọi luồng cùng nhìn một kho dữ liệu chung, nên gửi đồ cho nhau chỉ việc đặt lên kho. Nhưng nếu hai người cùng sửa một món một lúc, mọi thứ có thể rối tung. Nhớ nhé: dùng chung thì nhanh, nhưng phải xếp hàng khi cùng sửa.

    Every thread sees the same global and static variables and the same heap. This makes data exchange easy: just write a pointer or a global variable.1 But for the same reason, two threads modifying one datum without ordering produce a race condition — which is why the next chapter is devoted entirely to synchronization.1

    Bạn gặp nó ở đâuWhere you meet it Khi một luồng điền kết quả vào một mảng toàn cục rồi luồng khác đọc ra — không cần đường ống hay vùng nhớ chia sẻ như giữa các tiến trình.1 Khi một luồng để kết quả lên biến chung và luồng kia lấy ra — không cần cơ chế gì phức tạp. When one thread fills a global array and another reads it out — no pipe or shared-memory segment needed as between processes.1

    So với tiến trìnhVersus processes

    Các tiến trình riêng biệt không chia sẻ bộ nhớ (trừ khi cố ý lập vùng nhớ chia sẻ); để trao đổi, chúng phải dùng IPC như đường ống, hàng đợi thông điệp hay socket. Luồng bỏ qua toàn bộ chi phí đó vì mặc định đã chung bộ nhớ.1Separate processes do not share memory (unless they deliberately set up a shared-memory region); to communicate they must use IPC such as pipes, message queues, or sockets. Threads skip all that cost because they share memory by default.1

    Hệ quả thiết kếA design consequence

    Vì heap là chung, một luồng có thể malloc() rồi trao con trỏ cho luồng khác free(). Sự linh hoạt này đòi hỏi quy ước rõ ràng về "ai sở hữu" vùng nhớ để tránh giải phóng hai lần hay rò rỉ.1Because the heap is shared, one thread can malloc() and hand the pointer to another to free(). This flexibility demands a clear convention about who "owns" memory to avoid double frees or leaks.1

  2. per-thread errno

    errno là riêng mỗi luồng — nếu không thì vô dụng Mỗi luồng có errno riêng của mình errno is per-thread — otherwise it would be useless

    Trong chương trình đa luồng, errno được duy trì riêng cho từng luồng. Nếu nó là một biến toàn cục dùng chung, một luồng đặt errno rồi luồng khác có thể ghi đè trước khi luồng đầu kịp kiểm tra — biến nó thành vô nghĩa.12 Vì vậy trong UNIX hỗ trợ luồng, errno được định nghĩa lại thành một biểu thức trỏ tới một ô nhớ riêng của luồng gọi.1

    Mỗi luồng có một "ô báo lỗi" riêng tên là errno. Nhờ vậy, lỗi của người này không ghi đè lỗi của người kia. Nếu dùng chung một ô thì chẳng ai biết lỗi của mình còn nguyên hay đã bị thay. Nhớ nhé: errno riêng từng luồng, nên lỗi không lẫn vào nhau.

    In a multithreaded program, errno is maintained per-thread. If it were one shared global, one thread could set errno and another overwrite it before the first checked — making it meaningless.12 So on threads-capable UNIX, errno is redefined as an expression pointing to a storage location private to the calling thread.1

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi gọi một system call trong một luồng và kiểm tra errno ngay sau đó — kết quả luôn là lỗi của chính luồng đó, không bị luồng khác làm nhiễu.1 Khi kiểm tra lỗi ngay sau một lời gọi trong luồng — đó luôn là lỗi của riêng luồng đó. Whenever you call a system call in a thread and check errno right after — the result is always that thread's own error, undisturbed by others.1
  3. signal mask

    Disposition tín hiệu thì chung, mặt nạ tín hiệu thì riêng Cách xử lý tín hiệu chung, nhưng chặn tín hiệu thì riêng Dispositions are shared, the signal mask is private

    Đây là một ranh giới dễ nhầm. Các disposition tín hiệu — handler nào gắn với tín hiệu nào — là thuộc tính chung toàn tiến trình: mọi luồng cùng dùng một bộ handler.12 Nhưng mặt nạ tín hiệu — tín hiệu nào đang bị chặn — lại là thuộc tính riêng của mỗi luồng, điều chỉnh bằng pthread_sigmask().12

    Cách phản ứng với một tiếng chuông báo (ai sẽ chạy ra xử lý) là quy định chung cho cả văn phòng. Nhưng việc một nhân viên cụ thể có "bịt tai" tạm thời để khỏi bị làm phiền hay không thì tùy từng người. Nhớ nhé: luật xử lý tín hiệu là chung, còn ai bịt tai là chuyện riêng.

    This is an easy boundary to confuse. The signal dispositions — which handler is attached to which signal — are a process-wide attribute: all threads share one set of handlers.12 But the signal mask — which signals are currently blocked — is a per-thread attribute, adjusted with pthread_sigmask().12

    Bạn gặp nó ở đâuWhere you meet it Khi một thiết kế dùng một luồng chuyên trách nhận tín hiệu: luồng đó mở (bỏ chặn) các tín hiệu cần, còn mọi luồng khác chặn chúng trong mặt nạ riêng.1 Khi một luồng được giao chuyên nhận tín hiệu, còn các luồng khác thì "bịt tai" lại. When a design dedicates one thread to receiving signals: that thread unblocks the needed signals while all others block them in their private masks.1

    Tín hiệu và luồng là chủ đề tinh tếSignals and threads are subtle

    Kerrisk lưu ý mô hình tín hiệu UNIX và mô hình luồng vốn không ăn khớp dễ dàng; sự tương tác giữa chúng phức tạp đến mức được dành riêng một phần sâu hơn ở chương về luồng và tín hiệu sau này.1Kerrisk notes that the UNIX signal model and the thread model do not fit together easily; their interaction is intricate enough to warrant its own deeper treatment in a later chapter on threads and signals.1

    Một tín hiệu gửi tới tiến trìnhA signal sent to the process

    Khi một tín hiệu được gửi tới cả tiến trình (chứ không nhắm một luồng cụ thể), nhân chọn một luồng bất kỳ đang không chặn tín hiệu đó để chuyển tới — nên mặt nạ riêng của từng luồng quyết định ai có thể là người nhận.2When a signal is sent to the whole process (not aimed at a particular thread), the kernel picks one arbitrary thread that is not blocking it to deliver to — so each thread's private mask decides who can be the recipient.2

Cụm khái niệm 02 Concept cluster 02

Tạo luồng với pthread_create(), kết thúc với pthread_exit() — và biên dịch với -pthread Tạo một luồng mới và cho nó kết thúc Create with pthread_create(), end with pthread_exit() — and compile with -pthread

API Pthreads (POSIX threads) là bộ giao diện chuẩn để lập trình luồng, gồm các hàm và kiểu dữ liệu định nghĩa trong <pthread.h>.12 Một đặc điểm quan trọng: khác với system call thông thường, các hàm Pthreads không trả về −1 và đặt errno; thay vào đó chúng trả về 0 khi thành công hoặc một số lỗi dương khi thất bại.12 Chương trình dùng luồng phải được biên dịch với tùy chọn -pthread của trình biên dịch.12 Hàm pthread_create() tạo một luồng mới bắt đầu chạy từ một hàm cho trước.139

Để lập trình luồng, ta dùng một bộ công cụ chuẩn tên là Pthreads. Khi dịch chương trình, nhớ thêm cờ -pthread để báo cho trình dịch biết. Có một điểm lạ cần nhớ: các hàm này không báo lỗi theo kiểu cũ (trả về −1); chúng trả về 0 nếu ổn, hoặc một con số lỗi dương nếu hỏng. Nhớ nhé: Pthreads trả về 0 là ổn, số dương là có lỗi — và luôn dịch với -pthread.

The Pthreads API (POSIX threads) is the standard interface for thread programming, a set of functions and types declared in <pthread.h>.12 One important trait: unlike ordinary system calls, Pthreads functions do not return −1 and set errno; instead they return 0 on success or a positive error number on failure.12 A program that uses threads must be compiled with the compiler's -pthread option.12 The pthread_create() function creates a new thread that begins in a given function.139

Định nghĩa. pthread_create() nhận bốn đối số: con trỏ thread để nhận thread ID mới; con trỏ attr tới các thuộc tính (hoặc NULL cho mặc định); start — hàm mà luồng mới sẽ chạy đầu tiên; và arg — một đối số void * duy nhất truyền cho hàm đó.13 Luồng mới kết thúc khi hàm start trả về, hoặc khi nó gọi pthread_exit().14

Nói đơn giản. pthread_create giống như giao cho một nhân viên mới một tờ việc (start) và một món đồ cần thiết (arg), rồi bảo "bắt đầu đi". Làm xong việc, nhân viên nghỉ. Nhớ nhé: tạo luồng = giao một hàm để chạy cùng một món đầu vào.

Definition. pthread_create() takes four arguments: a thread pointer to receive the new thread ID; an attr pointer to attributes (or NULL for defaults); start — the function the new thread runs first; and arg — a single void * argument passed to it.13 The new thread ends when start returns, or when it calls pthread_exit().14

#include <pthread.h>
int pthread_create(pthread_t *thread,
  const pthread_attr_t *attr,
  void *(*start)(void *), void *arg);
void pthread_exit(void *retval);
/* trả về 0 = ổn, >0 = số lỗi */

pthread_create(&t, NULL,
  start, arg); /* tạo luồng */
pthread_exit(retval); /* luồng nghỉ */
// dịch với: cc -pthread ...

#include <pthread.h>
int pthread_create(pthread_t *thread,
  const pthread_attr_t *attr,
  void *(*start)(void *), void *arg);
void pthread_exit(void *retval);
/* returns 0 = ok, >0 = error number */

  1. pthread_create

    Một đối số void * — và trật tự thực thi không xác định Chỉ truyền được một món, và ai chạy trước không chắc One void * argument — and unspecified execution order

    Vì start chỉ nhận một đối số void *, muốn truyền nhiều giá trị ta gói chúng vào một struct rồi truyền con trỏ tới struct đó.13 Một điểm dễ sai: sau pthread_create(), chuẩn không quy định luồng nào chạy trước — luồng gọi hay luồng mới; đừng viết mã dựa vào một thứ tự cụ thể.1

    Chỉ gửi được một món cho luồng mới; muốn gửi nhiều thứ thì bỏ chung vào một "hộp" (struct) rồi gửi cái hộp. Và đừng đoán ai chạy trước: luồng cũ hay luồng mới đều có thể. Nhớ nhé: gói nhiều thứ vào một hộp, và đừng tin vào thứ tự chạy.

    Because start takes just one void * argument, to pass several values you wrap them in a struct and pass a pointer to it.13 An easy mistake: after pthread_create(), the standard does not specify which thread runs first — the caller or the new one; never write code that depends on a particular order.1

    Bạn gặp nó ở đâuWhere you meet it Khi khởi một nhóm luồng trong vòng lặp: mỗi lần truyền một struct riêng cho từng luồng, tránh dùng chung một biến vòng lặp mà mọi luồng cùng đọc.1 Khi tạo nhiều luồng trong vòng lặp — mỗi luồng cần một hộp riêng, đừng để chúng dùng chung một biến đếm. When starting a group of threads in a loop: pass each a separate struct, avoiding a shared loop variable all threads read.1

    Kiểu trả về void *The void * return type

    Hàm start có kiểu void *(*)(void *): nhận một con trỏ void và trả về một con trỏ void. Giá trị trả về này chính là "kết quả" của luồng, có thể lấy lại qua pthread_join().13The start function has type void *(*)(void *): it takes a void pointer and returns a void pointer. That return value is the thread's "result," retrievable later via pthread_join().13

    Lỗi khi tạoOn creation error

    Nếu không đủ tài nguyên, pthread_create() trả về số lỗi dương (ví dụ EAGAIN khi chạm giới hạn số luồng) — nhớ kiểm tra giá trị trả về chứ không phải errno.3On insufficient resources, pthread_create() returns a positive error number (for example EAGAIN at the thread limit) — check the return value, not errno.3

  2. return vs errno

    Bẫy kiểm lỗi: Pthreads không đặt errno Kiểm lỗi khác với system call thường The error-check trap: Pthreads does not set errno

    Đây là một thay đổi thói quen quan trọng. Với system call thường, ta kiểm tra if (call() == -1) rồi đọc errno. Với Pthreads thì khác: hàm trả thẳng số lỗi, nên mẫu đúng là lưu giá trị trả về vào một biến rồi so sánh, ví dụ s = pthread_create(...); if (s != 0) ....12 Các mã lỗi mang cùng ý nghĩa như những mã trong errno (chẳng hạn EINVAL, EAGAIN), chỉ là đường trả về khác.2

    Với lệnh cũ, ta xem nó có trả về −1 không rồi mới đọc ô báo lỗi. Với Pthreads, con số lỗi nằm ngay trong giá trị trả về — nên hãy xem thẳng cái nó trả về. Nhớ nhé: Pthreads đưa lỗi ra ngay chỗ trả về, đừng đi tìm errno.

    This is an important habit change. With ordinary system calls you check if (call() == -1) then read errno. With Pthreads it differs: the function returns the error number directly, so the correct pattern saves the return value and compares it, e.g. s = pthread_create(...); if (s != 0) ....12 The error codes carry the same meanings as those in errno (such as EINVAL, EAGAIN), just via a different path.2

    Bạn gặp nó ở đâuWhere you meet it Mỗi khi bọc một lời gọi Pthreads trong mã kiểm lỗi — dùng giá trị trả về làm đối số cho strerror(), không phải errno.2 Khi in thông báo lỗi của một hàm Pthreads — lấy số nó trả về mà tra, đừng tra errno. Whenever you wrap a Pthreads call in error-checking — feed the return value to strerror(), not errno.2
  3. pthread_exit

    pthread_exit() kết thúc một luồng, không phải cả tiến trình Một luồng nghỉ việc, văn phòng vẫn mở pthread_exit() ends a thread, not the process

    Một luồng chấm dứt theo vài cách: hàm start của nó return; nó gọi pthread_exit(); nó bị hủy (cancel); hoặc bất kỳ luồng nào gọi exit() (kết thúc cả tiến trình).14 Khác biệt mấu chốt: pthread_exit() chỉ kết thúc luồng gọi, các luồng khác vẫn chạy; còn return từ main() hay gọi exit() làm mọi luồng dừng ngay.14 Giá trị retval truyền cho pthread_exit() trở thành kết quả trả về của luồng.14

    Một nhân viên có thể tự xin nghỉ bằng pthread_exit() mà văn phòng vẫn mở, người khác vẫn làm. Nhưng nếu ai đó gọi exit() thì cả văn phòng đóng cửa, mọi người nghỉ hết. Nhớ nhé: pthread_exit chỉ cho một luồng nghỉ; exit đóng cửa cả tiến trình.

    A thread terminates in several ways: its start function returns; it calls pthread_exit(); it is cancelled; or any thread calls exit() (ending the whole process).14 The crucial difference: pthread_exit() ends only the calling thread, others keep running; whereas a return from main() or a call to exit() stops all threads at once.14 The retval passed to pthread_exit() becomes the thread's return value.14

    Bạn gặp nó ở đâuWhere you meet it Khi luồng chính muốn kết thúc phần việc của mình mà vẫn để các luồng khác chạy tiếp — nó gọi pthread_exit() thay vì return từ main().14 Khi luồng chính làm xong việc nhưng muốn các luồng khác chạy tiếp — dùng pthread_exit, đừng return khỏi main. When the main thread finishes its own work but wants the others to continue — it calls pthread_exit() instead of returning from main().14

    Cẩn thận với retval trỏ vào ngăn xếpBeware a retval into the stack

    Không nên cho retval trỏ tới một biến cục bộ trên ngăn xếp của luồng đang kết thúc, vì ngăn xếp đó bị thu hồi — con trỏ sẽ treo. Hãy trỏ tới bộ nhớ toàn cục hoặc heap.14retval should not point to a local variable on the terminating thread's stack, since that stack is reclaimed — the pointer would dangle. Point it at global or heap memory.14

    Luồng chính là một trường hợp đặc biệtThe main thread is special

    Nếu luồng chính gọi pthread_exit() thay vì exit() hay return, các luồng còn lại vẫn tiếp tục; tiến trình chỉ thật sự kết thúc khi luồng cuối cùng chấm dứt.4If the main thread calls pthread_exit() rather than exit() or return, the remaining threads continue; the process only truly ends when the last thread terminates.4

Cụm khái niệm 03 Concept cluster 03

Gọi tên luồng, nối luồng đã kết thúc, tách luồng — và thuộc tính luồng Tên luồng, chờ luồng xong, và tách luồng Naming a thread, joining a finished one, detaching — and thread attributes

Mỗi luồng có một thread ID kiểu pthread_t, dùng để nhận diện nó trong các lời gọi Pthreads. Một luồng lấy ID của chính mình bằng pthread_self(), và so sánh hai ID bằng pthread_equal() — không dùng toán tử ==.15 Để chờ một luồng khác kết thúc và lấy giá trị trả về của nó, ta gọi pthread_join().16 Nếu không quan tâm tới kết quả và muốn hệ thống tự thu hồi tài nguyên của luồng khi nó kết thúc, ta tách nó bằng pthread_detach().17 Các thuộc tính của luồng (như kích thước ngăn xếp hay trạng thái tách) được gói trong một đối tượng pthread_attr_t.18

Mỗi luồng có một "tên gọi" (thread ID). Luồng tự hỏi tên mình bằng pthread_self, và muốn so hai tên thì dùng pthread_equal chứ không so bằng dấu ==. Muốn chờ một luồng làm xong thì gọi pthread_join. Còn nếu "thả" luồng tự lo liệu thì dùng pthread_detach. Nhớ nhé: join là đứng chờ, detach là buông tay.

Each thread has a thread ID of type pthread_t, used to identify it in Pthreads calls. A thread gets its own ID with pthread_self(), and compares two IDs with pthread_equal() — not the == operator.15 To wait for another thread to terminate and collect its return value, you call pthread_join().16 If you do not care about the result and want the system to reclaim the thread's resources automatically when it ends, you detach it with pthread_detach().17 A thread's attributes (such as stack size or detach state) are bundled in a pthread_attr_t object.18

Định nghĩa. pthread_join(thread, retval) chặn luồng gọi cho tới khi luồng thread kết thúc; nếu retval khác NULL, nó nhận giá trị trả về của luồng đó. Nối một luồng vừa trả về cả giá trị kết quả vừa cho phép hệ thống thu hồi tài nguyên của luồng. Nối một luồng đã bị tách, hoặc nối hai lần cùng một luồng, là lỗi.16

Nói đơn giản. pthread_join là đứng chờ trước cửa cho tới khi một luồng làm xong việc, rồi nhận "sản phẩm" nó đưa ra. Chờ một luồng đã bị thả ra (detach) là không được. Nhớ nhé: join vừa để chờ, vừa để nhận kết quả, vừa để dọn dẹp.

Definition. pthread_join(thread, retval) blocks the caller until thread terminates; if retval is non-NULL, it receives that thread's return value. Joining a thread both returns its result and lets the system reclaim the thread's resources. Joining a thread that has already been detached, or joining the same thread twice, is an error.16

pthread_t pthread_self(void);
int pthread_equal(pthread_t, pthread_t);
int pthread_join(pthread_t, void **retval);
int pthread_detach(pthread_t);
/* join XOR detach — chọn một */

pthread_self() --> tên của tôi
pthread_equal(a,b) --> so hai tên
pthread_join(t,&r) --> chờ + nhận
pthread_detach(t) --> buông tay

pthread_t pthread_self(void);
int pthread_equal(pthread_t, pthread_t);
int pthread_join(pthread_t, void **retval);
int pthread_detach(pthread_t);
/* join XOR detach — pick one */

  1. pthread_equal

    Thread ID là mờ đục — so sánh bằng pthread_equal() Đừng so hai tên luồng bằng dấu bằng Thread IDs are opaque — compare with pthread_equal()

    Kiểu pthread_t được quy định là một kiểu mờ đục (opaque): chuẩn không cho giả định nó là một số nguyên. Vì vậy, không được so sánh hai thread ID bằng ==, cũng không nên in nó bằng %d; phải dùng pthread_equal() để so.15 Trên Linux/NPTL pthread_t tình cờ là một số nguyên, nhưng dựa vào điều đó làm chương trình mất tính khả chuyển.1

    "Tên gọi" của luồng là một thứ bí ẩn — ta không được coi nó như một con số thường. Muốn biết hai tên có trùng không thì dùng pthread_equal, đừng dùng dấu ==. Nhớ nhé: so tên luồng thì dùng pthread_equal, không dùng ==.

    The pthread_t type is specified as an opaque type: the standard forbids assuming it is an integer. So two thread IDs must not be compared with ==, nor printed with %d; you must use pthread_equal() to compare them.15 On Linux/NPTL pthread_t happens to be an integer, but relying on that makes a program nonportable.1

    Bạn gặp nó ở đâuWhere you meet it Khi một luồng cần kiểm tra xem một thread ID có phải chính nó không: so pthread_self() với ID kia bằng pthread_equal().5 Khi một luồng muốn biết "có phải chính mình không" — so tên mình với tên kia bằng pthread_equal. When a thread checks whether a thread ID is itself: compare pthread_self() with the other via pthread_equal().5
  2. pthread_join

    Nối luồng không giống waitpid() — không có thứ bậc Chờ luồng khác chờ con: luồng nào cũng nối được Joining is unlike waitpid() — there is no hierarchy

    Kerrisk nhấn mạnh pthread_join() khác waitpid() ở vài điểm. Giữa các luồng không có quan hệ cha–con: bất kỳ luồng nào trong tiến trình cũng có thể nối bất kỳ luồng nào khác, không chỉ luồng "tạo" ra nó.16 Ngoài ra không có cách "nối bất kỳ luồng nào kết thúc" (không có tương đương của wait(NULL)), và không thể nối theo kiểu không chặn.1

    Chờ một luồng xong thì khác với chờ một tiến trình con. Luồng không có "cha con" — bất kỳ luồng nào cũng chờ được một luồng khác bất kỳ. Nhưng không thể nói "chờ đứa nào xong trước cũng được". Nhớ nhé: luồng nào cũng nối được luồng khác, nhưng phải chỉ đích danh.

    Kerrisk stresses that pthread_join() differs from waitpid() in several ways. Among threads there is no parent–child relationship: any thread in the process can join any other, not just the one that "created" it.16 There is also no way to "join any terminated thread" (no equivalent of wait(NULL)), and no nonblocking join.1

    Bạn gặp nó ở đâuWhere you meet it Cuối một chương trình chia việc cho nhiều luồng: luồng chính nối lần lượt từng luồng để chắc chắn tất cả đã xong trước khi tổng hợp kết quả.6 Khi chia việc cho nhiều luồng — luồng chính chờ (join) từng luồng xong rồi mới gom kết quả. At the end of a program that split work across threads: the main thread joins each in turn to be sure all are done before combining results.6

    Không nối sẽ rò rỉNot joining leaks

    Nếu một luồng có thể nối (joinable) kết thúc mà không ai nối, nó trở thành "thây ma" (zombie) tương tự tiến trình: tài nguyên không được thu hồi. Vì vậy mỗi luồng joinable cuối cùng phải được nối, hoặc phải được tách.16If a joinable thread terminates and no one joins it, it becomes a "zombie" analogous to a process: its resources are not reclaimed. So every joinable thread must eventually be joined, or must be detached.16

    Lấy giá trị trả vềCollecting the return value

    Đối số thứ hai của pthread_join() là void **: nó nhận con trỏ void * mà luồng đã trả về (qua return hoặc pthread_exit()). Nếu không cần kết quả, truyền NULL.6The second argument to pthread_join() is a void **: it receives the void * the thread returned (via return or pthread_exit()). If the result is not needed, pass NULL.6

  3. pthread_detach / attr

    Tách luồng, và chỉnh thuộc tính qua pthread_attr_t Thả luồng tự lo, và tùy chỉnh luồng từ đầu Detaching, and tuning attributes via pthread_attr_t

    pthread_detach(thread) đánh dấu một luồng là đã tách: khi nó kết thúc, tài nguyên được tự động thu hồi, và ta không thể (và không cần) nối nó nữa.17 Một luồng đã tách không thể quay lại thành joinable. Thay vì chỉnh sau, ta có thể quy định trạng thái tách — cùng các thuộc tính như kích thước và địa chỉ ngăn xếp — ngay từ lúc tạo, thông qua một đối tượng pthread_attr_t truyền vào pthread_create().18

    Nếu không muốn đứng chờ một luồng, ta "thả" nó bằng pthread_detach — xong việc nó tự dọn dẹp. Nhưng đã thả thì không chờ lại được nữa. Muốn đặt trước các tính chất của luồng (như độ lớn bàn làm việc) thì dùng một "phiếu cấu hình" là pthread_attr_t. Nhớ nhé: đã detach thì khỏi join; muốn tùy chỉnh thì dùng pthread_attr_t.

    pthread_detach(thread) marks a thread as detached: when it terminates, its resources are reclaimed automatically, and you cannot (and need not) join it.17 A detached thread cannot be made joinable again. Instead of adjusting afterward, you can specify the detach state — along with attributes such as stack size and address — at creation time, via a pthread_attr_t object passed to pthread_create().18

    Bạn gặp nó ở đâuWhere you meet it Với các luồng "chạy rồi quên" — ví dụ mỗi kết nối mạng một luồng phục vụ — tách chúng để khỏi phải nối và khỏi rò rỉ tài nguyên.7 Với luồng kiểu "làm xong là xong, không cần kết quả" — hãy tách nó ra để khỏi phải chờ. For "fire-and-forget" threads — say, one server thread per network connection — detach them so you need not join and do not leak resources.7

    Vòng đời của một pthread_attr_tA pthread_attr_t's life cycle

    Khởi tạo bằng pthread_attr_init(), đặt từng thuộc tính bằng các hàm pthread_attr_set*() (ví dụ pthread_attr_setdetachstate()), rồi hủy bằng pthread_attr_destroy() sau khi đã dùng xong để tạo luồng.8Initialize with pthread_attr_init(), set each attribute with pthread_attr_set*() functions (e.g. pthread_attr_setdetachstate()), then destroy with pthread_attr_destroy() once you have used it to create the thread.8

    Tách không chấm dứt luồngDetaching does not end a thread

    Lưu ý: pthread_detach() chỉ thay đổi cách dọn dẹp sau này, nó không làm luồng dừng lại; luồng đã tách vẫn chạy tiếp cho tới khi tự kết thúc hoặc tiến trình kết thúc.7Note: pthread_detach() only changes how cleanup happens later; it does not stop the thread. A detached thread runs on until it terminates itself or the process ends.7

Cụm khái niệm 04 Concept cluster 04

Luồng so với tiến trình: nhanh và dễ chia sẻ, nhưng mong manh hơn Luồng và tiến trình: mỗi bên mạnh yếu khác nhau Threads versus processes: fast and sharing, but more fragile

Kerrisk khép chương bằng một cân nhắc thẳng thắn về việc chọn luồng thay vì tạo nhiều tiến trình bằng fork(). Ưu điểm: chia sẻ dữ liệu giữa các luồng dễ và nhanh (chỉ cần bộ nhớ chung), còn việc tạo luồng và chuyển ngữ cảnh giữa các luồng thường rẻ hơn so với tiến trình.1 Nhược điểm: lập trình luồng đòi hỏi code phải an toàn với luồng (thread-safe); một lỗi trong một luồng — như làm hỏng bộ nhớ — có thể hạ gục toàn bộ tiến trình vì mọi luồng chung một không gian địa chỉ; và các luồng phải cạnh tranh cùng một không gian địa chỉ hữu hạn.1

Nên dùng nhiều luồng hay nhiều tiến trình? Luồng thắng ở chỗ: chia sẻ dữ liệu dễ, tạo ra nhanh, đổi qua đổi lại nhẹ nhàng. Nhưng luồng cũng mong manh hơn: một người làm sai có thể kéo sập cả văn phòng, vì tất cả ngồi chung một phòng. Nhớ nhé: luồng nhanh và gần gũi, nhưng cùng chung một mái nên cùng chịu rủi ro.

Kerrisk closes the chapter with a candid weighing of choosing threads over creating multiple processes with fork(). Advantages: sharing data between threads is easy and fast (just shared memory), and creating threads and switching between them is usually cheaper than with processes.1 Disadvantages: thread programming demands thread-safe code; a bug in one thread — such as corrupting memory — can bring down the entire process because all threads share one address space; and threads must compete for one finite address space.1

Định nghĩa. Lựa chọn luồng hay tiến trình là một sự đánh đổi giữa tốc độ và sự gần gũi (luồng) với sự cô lập và an toàn (tiến trình). Không có câu trả lời đúng tuyệt đối — nó phụ thuộc vào mức độ chia sẻ dữ liệu và mức độ chịu đựng rủi ro của bài toán.1

Nói đơn giản. Chọn luồng khi cần nhanh và chia sẻ nhiều; chọn tiến trình khi cần mỗi phần việc được cách ly an toàn khỏi phần khác. Nhớ nhé: cần gần gũi chọn luồng, cần an toàn chọn tiến trình.

Definition. Choosing threads or processes is a trade-off between speed and closeness (threads) and isolation and safety (processes). There is no absolute right answer — it depends on how much data is shared and how much risk the problem can tolerate.1

LUỒNG  : + chia sẻ dễ, tạo rẻ
        − một lỗi sập cả tiến trình
TIẾN TRÌNH: + cô lập, an toàn
        − chia sẻ phải qua IPC

LUỒNG: nhanh + chung đồ
  nhưng cùng chịu rủi ro
TIẾN TRÌNH: an toàn, cách ly
  nhưng chia sẻ khó hơn

THREADS  : + easy sharing, cheap
         − one bug sinks the process
PROCESSES: + isolation, safety
         − sharing needs IPC

  1. advantages

    Vì sao chọn luồng: chia sẻ dễ và tạo rẻ Điểm mạnh của luồng Why choose threads: easy sharing, cheap creation

    Hai lợi thế lớn nhất của luồng. Thứ nhất, chia sẻ dữ liệu giữa các luồng là chuyện đương nhiên — chúng dùng chung bộ nhớ, nên không cần những cơ chế IPC tốn kém và phức tạp mà các tiến trình buộc phải dùng.1 Thứ hai, tạo một luồng nhanh hơn tạo một tiến trình, và chuyển ngữ cảnh giữa các luồng của cùng tiến trình thường rẻ hơn giữa các tiến trình.1

    Luồng mạnh ở hai chỗ: chia đồ cho nhau rất dễ (vì chung kho), và "thuê" một luồng mới nhanh hơn, nhẹ hơn so với mở một tiến trình mới. Nhớ nhé: luồng được chọn vì chia sẻ dễ và tạo nhanh.

    The two biggest advantages of threads. First, sharing data between threads is a given — they share memory, so none of the costly, complex IPC that separate processes must use.1 Second, creating a thread is faster than creating a process, and context-switching between threads of one process is usually cheaper than between processes.1

    Bạn gặp nó ở đâuWhere you meet it Trong các bài toán xử lý song song trên một tập dữ liệu lớn chung — nhiều luồng cùng cày trên một mảng trong bộ nhớ mà không cần sao chép dữ liệu.1 Khi nhiều luồng cùng xử lý một kho dữ liệu lớn — không cần chép dữ liệu qua lại. In parallel work over one large shared dataset — many threads grind on one in-memory array without copying data around.1
  2. disadvantages

    Cái giá: một lỗi có thể hạ cả tiến trình Điểm yếu: cùng chung nên cùng sập The price: one bug can sink the whole process

    Mặt trái của chia sẻ. Vì mọi luồng chung một không gian địa chỉ, một luồng viết lầm vào bộ nhớ có thể làm hỏng dữ liệu của luồng khác và khiến cả tiến trình sụp đổ — trong khi với các tiến trình tách biệt, một tiến trình chết không kéo theo các tiến trình khác.1 Hơn nữa, lập trình luồng đòi hỏi mọi hàm được gọi phải an toàn với luồng (thread-safe), một ràng buộc không nhỏ.1

    Chung kho có cái dở: một người làm đổ mực lên hồ sơ chung là hỏng cho tất cả, thậm chí sập cả văn phòng. Với các văn phòng riêng (tiến trình) thì một nơi cháy không lan sang nơi khác. Nhớ nhé: chung một phòng nghĩa là cùng chịu khi một luồng gây họa.

    The flip side of sharing. Because all threads share one address space, a thread that writes wrongly into memory can corrupt another thread's data and crash the entire process — whereas with separate processes, one dying process does not take the others with it.1 Moreover, thread programming requires every function called to be thread-safe, no small constraint.1

    Bạn gặp nó ở đâuWhere you meet it Khi một thư viện cũ dùng bộ nhớ tĩnh nội bộ (không khả tái nhập) khiến một chương trình đa luồng thỉnh thoảng hỏng khó lần — đó là cái giá của việc chia sẻ địa chỉ.1 Khi một chương trình đa luồng thỉnh thoảng hỏng khó hiểu — thường vì dùng một hàm không an toàn với luồng. When an old library using internal static storage (nonreentrant) makes a multithreaded program fail intermittently and hard to trace — the price of a shared address space.1

    Các điểm so sánh khácOther points of comparison

    Kerrisk còn nêu: xử lý tín hiệu trong chương trình đa luồng phức tạp hơn nhiều; mọi luồng buộc phải chạy cùng một chương trình (khác với các tiến trình có thể chạy chương trình khác nhau sau exec()); và các luồng chia nhau một không gian địa chỉ hữu hạn.1Kerrisk also notes: signal handling is far more complex in a multithreaded program; all threads must run the same program (unlike processes, which can run different programs after exec()); and threads share one finite address space.1

    Không chỉ có hai lựa chọnNot only two choices

    Tác giả lưu ý còn các mô hình khác ngoài "luồng thuần" và "tiến trình thuần" — chẳng hạn dùng tiến trình kèm bộ nhớ chia sẻ, hay các khung xử lý sự kiện — tùy bài toán mà cân nhắc.1The author notes there are models beyond "pure threads" and "pure processes" — such as processes plus shared memory, or event-driven frameworks — to weigh per problem.1

  3. NPTL

    Trên Linux, Pthreads hiện đại là NPTL Bản cài Pthreads trên Linux ngày nay On Linux, modern Pthreads is NPTL

    Kerrisk giải thích Linux có hai hiện thực Pthreads theo thời gian: LinuxThreads cũ và NPTL (Native POSIX Threads Library) mới hơn, bám sát chuẩn SUSv3 hơn và có hiệu năng tốt hơn. Trên các hệ thống Linux hiện đại, NPTL là hiện thực được dùng.12

    Trên Linux có hai "bản cài" của Pthreads qua các thời kỳ; bản mới và tốt hơn tên là NPTL, và đó là bản bạn gặp trên máy Linux ngày nay. Nhớ nhé: Pthreads trên Linux hiện đại chính là NPTL.

    Kerrisk explains that Linux has had two Pthreads implementations over time: the older LinuxThreads and the newer NPTL (Native POSIX Threads Library), which conforms more closely to SUSv3 and performs better. On modern Linux systems, NPTL is the implementation in use.12

    Bạn gặp nó ở đâuWhere you meet it Khi đọc tài liệu hay mã cũ nhắc tới LinuxThreads với các khác biệt hành vi — hầu hết đã lỗi thời trên hệ thống dùng NPTL ngày nay.2 Khi gặp tài liệu cũ nói về LinuxThreads — phần lớn không còn đúng với NPTL hiện nay. When reading old docs or code mentioning LinuxThreads with behavioral quirks — most are obsolete on today's NPTL systems.2

Ba ngộ nhận thường gặp Three common misconceptions

“Mỗi luồng có bộ nhớ riêng như một tiến trình nhỏ” “Each thread has its own memory like a mini-process”

Ngộ nhận: luồng giống tiến trình, mỗi luồng một không gian bộ nhớ riêng.

Thực tế: các luồng chia sẻ cùng một không gian địa chỉ — chung bộ nhớ toàn cục, heap, bộ mô tả tệp. Chỉ ngăn xếp, errno, mặt nạ tín hiệu và thread ID là riêng.12

Myth: a thread is like a process, each with its own memory space.

Reality: threads share one address space — common global memory, heap, file descriptors. Only the stack, errno, signal mask and thread ID are private.12

“Hàm Pthreads đặt errno khi lỗi” “Pthreads functions set errno on error”

Ngộ nhận: kiểm lỗi Pthreads bằng cách xem −1 rồi đọc errno, như system call.

Thực tế: hàm Pthreads trả về 0 khi thành công hoặc một số lỗi dương; chúng không đặt errno. Hãy kiểm tra giá trị trả về.12

Myth: check Pthreads errors by looking for −1 and reading errno, like a system call.

Reality: Pthreads functions return 0 on success or a positive error number; they do not set errno. Check the return value.12

“Luồng đã tách vẫn nối được để lấy kết quả” “A detached thread can still be joined for its result”

Ngộ nhận: cứ pthread_detach() rồi vẫn pthread_join() sau để lấy giá trị trả về.

Thực tế: join và detach loại trừ nhau. Nối một luồng đã tách là lỗi; luồng đã tách tự thu hồi tài nguyên và không trả kết quả cho ai.17

Myth: you can pthread_detach() and still pthread_join() later for the return value.

Reality: join and detach are mutually exclusive. Joining a detached thread is an error; a detached thread reclaims its own resources and returns a result to no one.17

Tự kiểm Self-check

Năm câu hỏi để biết bạn đã nắm nền tảng về luồng Năm câu hỏi kiểm tra nhanh Five questions to check you grasped the basics of threads

Trả lời được những câu này nghĩa là bạn đã nắm bản chất của luồng, ranh giới chung — riêng, và bộ API Pthreads cốt lõi — nền cho chương về đồng bộ hóa luồng.

Nếu trả lời được, nghĩa là bạn đã hiểu chương này.

Answering these means you grasp what a thread is, the shared-versus-private boundary, and the core Pthreads API — the groundwork for the chapter on thread synchronization.

  1. Kể những thuộc tính mà các luồng của một tiến trình dùng chung, và những thuộc tính mỗi luồng giữ riêng. Vì sao errno phải là riêng từng luồng?

    Gợi ý sâu hơn: disposition tín hiệu và mặt nạ tín hiệu — cái nào chung, cái nào riêng?

  2. pthread_create() nhận những đối số nào? Một luồng mới bắt đầu và kết thúc như thế nào?

    Gợi ý sâu hơn: vì sao không nên cho giá trị trả về của luồng trỏ vào biến cục bộ trên ngăn xếp của nó?

  3. Các hàm Pthreads báo lỗi khác system call thông thường ra sao? Viết mẫu kiểm lỗi đúng cho một lời gọi Pthreads.

    Gợi ý sâu hơn: vì sao in một pthread_t bằng %d hoặc so bằng == là không khả chuyển?

  4. pthread_join() làm gì, và nó khác waitpid() ở những điểm nào? Điều gì xảy ra nếu một luồng joinable không bao giờ được nối?

    Gợi ý sâu hơn: vì sao join và detach loại trừ nhau?

  5. Nêu một ưu điểm và một nhược điểm của luồng so với tiến trình. Khi nào bạn sẽ chọn tiến trình thay vì luồng?

    Gợi ý sâu hơn: vì sao một lỗi bộ nhớ trong một luồng lại nguy hiểm cho cả tiến trình?

  1. Các luồng dùng chung gì, và mỗi luồng giữ riêng gì?

  2. Làm sao để tạo một luồng mới, và cho nó kết thúc?

  3. Hàm Pthreads báo thành công hay lỗi bằng cách nào?

  4. Khi nào thì "chờ" một luồng (join), khi nào thì "thả" nó (detach)?

  5. Luồng mạnh hơn tiến trình ở điểm nào, và yếu hơn ở điểm nào?

  1. Name the attributes the threads of a process share, and those each thread keeps private. Why must errno be per-thread?

  2. What arguments does pthread_create() take? How does a new thread begin and end?

  3. How do Pthreads functions report errors differently from ordinary system calls? Write the correct error-check pattern for a Pthreads call.

  4. What does pthread_join() do, and how does it differ from waitpid()? What happens if a joinable thread is never joined?

  5. Give one advantage and one disadvantage of threads versus processes. When would you choose processes over threads?

Khép lại chương Closing the chapter

Một luồng là một dòng thực thi trong một tiến trình, cùng anh em chia sẻ bộ nhớ nhưng giữ riêng ngăn xếp và errno. Với vài lời gọi Pthreads — tạo, kết thúc, nối và tách — ta đã có đủ bộ xương để dựng chương trình đa luồng. Nhưng chính sự chia sẻ bộ nhớ ấy đặt ra câu hỏi lớn kế tiếp: làm sao để nhiều luồng chạm vào cùng một dữ liệu mà không giẫm chân nhau. Đó là đồng bộ hóa.1

Luồng là một việc chạy song song trong một tiến trình, dùng chung bộ nhớ nhưng mỗi luồng có góc riêng. Biết tạo, kết thúc, chờ và thả luồng là đủ để bắt đầu. Câu hỏi tiếp theo: làm sao để nhiều luồng dùng chung dữ liệu mà không rối. Nhớ nhé: chia sẻ xong là phải học cách xếp hàng — đó là đồng bộ hóa.

A thread is a line of execution within a process, sharing memory with its siblings yet keeping its own stack and errno. With a handful of Pthreads calls — create, terminate, join and detach — you have the skeleton for building multithreaded programs. But that very memory sharing poses the next big question: how do many threads touch the same data without stepping on each other. That is synchronization.1