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 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 */
-
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ì
startchỉ 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: saupthread_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ể.1Chỉ 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
starttakes just onevoid *argument, to pass several values you wrap them in a struct and pass a pointer to it.13 An easy mistake: afterpthread_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.1Bạ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.1Kiểu trả về void *The void * return type
Hàm
startcó kiểuvoid *(*)(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 quapthread_join().13Thestartfunction has typevoid *(*)(void *): it takes a void pointer and returns a void pointer. That return value is the thread's "result," retrievable later viapthread_join().13Lỗ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ụEAGAINkhi chạm giới hạn số luồng) — nhớ kiểm tra giá trị trả về chứ không phảierrno.3On insufficient resources,pthread_create()returns a positive error number (for exampleEAGAINat the thread limit) — check the return value, noterrno.3 -
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 đọcerrno. 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ã trongerrno(chẳng hạnEINVAL,EAGAIN), chỉ là đường trả về khác.2Vớ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 readerrno. 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 inerrno(such asEINVAL,EAGAIN), just via a different path.2Bạ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ố chostrerror(), không phảierrno.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 tostrerror(), noterrno.2 -
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
startcủa nóreturn; nó gọipthread_exit(); nó bị hủy (cancel); hoặc bất kỳ luồng nào gọiexit()(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ònreturntừmain()hay gọiexit()làm mọi luồng dừng ngay.14 Giá trịretvaltruyền chopthread_exit()trở thành kết quả trả về của luồng.14Mộ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ọiexit()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
startfunctionreturns; it callspthread_exit(); it is cancelled; or any thread callsexit()(ending the whole process).14 The crucial difference:pthread_exit()ends only the calling thread, others keep running; whereas areturnfrommain()or a call toexit()stops all threads at once.14 Theretvalpassed topthread_exit()becomes the thread's return value.14Bạ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ọipthread_exit()thay vìreturntừ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 callspthread_exit()instead ofreturning frommain().14Cẩn thận với retval trỏ vào ngăn xếpBeware a retval into the stack
Không nên cho
retvaltrỏ 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.14retvalshould 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.14Luồ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()hayreturn, 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 callspthread_exit()rather thanexit()orreturn, 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 */
-
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ùngpthread_equal()để so.15 Trên Linux/NPTLpthread_ttì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_ttype 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 usepthread_equal()to compare them.15 On Linux/NPTLpthread_thappens to be an integer, but relying on that makes a program nonportable.1Bạ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: sopthread_self()với ID kia bằngpthread_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: comparepthread_self()with the other viapthread_equal().5 -
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ácwaitpid()ở 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ủawait(NULL)), và không thể nối theo kiểu không chặn.1Chờ 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 fromwaitpid()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 ofwait(NULL)), and no nonblocking join.1Bạ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.6Khô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ề (quareturnhoặcpthread_exit()). Nếu không cần kết quả, truyền NULL.6The second argument topthread_join()is avoid **: it receives thevoid *the thread returned (viareturnorpthread_exit()). If the result is not needed, pass NULL.6 -
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ượngpthread_attr_ttruyền vàopthread_create().18Nế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 apthread_attr_tobject passed topthread_create().18Bạ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.7Vò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àmpthread_attr_set*()(ví dụpthread_attr_setdetachstate()), rồi hủy bằngpthread_attr_destroy()sau khi đã dùng xong để tạo luồng.8Initialize withpthread_attr_init(), set each attribute withpthread_attr_set*()functions (e.g.pthread_attr_setdetachstate()), then destroy withpthread_attr_destroy()once you have used it to create the thread.8Tá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
-
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 -
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.1Cá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 afterexec()); and threads share one finite address space.1Khô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
-
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.
-
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
errnophả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?
-
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ó?
-
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_tbằng%dhoặc so bằng==là không khả chuyển? -
pthread_join()làm gì, và nó khácwaitpid()ở 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?
-
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?
Các luồng dùng chung gì, và mỗi luồng giữ riêng gì?
Làm sao để tạo một luồng mới, và cho nó kết thúc?
Hàm Pthreads báo thành công hay lỗi bằng cách nào?
Khi nào thì "chờ" một luồng (join), khi nào thì "thả" nó (detach)?
Luồng mạnh hơn tiến trình ở điểm nào, và yếu hơn ở điểm nào?
Name the attributes the threads of a process share, and those each thread keeps private. Why must
errnobe per-thread?What arguments does
pthread_create()take? How does a new thread begin and end?How do Pthreads functions report errors differently from ordinary system calls? Write the correct error-check pattern for a Pthreads call.
What does
pthread_join()do, and how does it differ fromwaitpid()? What happens if a joinable thread is never joined?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.1Luồ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