“Một CreateThread mỗi client.” “Một cho timer.” “Một để chờ sự kiện.” — Trong mã Windows native, luồng có xu hướng sinh sôi theo cách này. Mỗi luồng tiêu thụ một stack và một đối tượng kernel, và việc tạo rồi hủy chúng cũng có chi phí. Công việc thì mịn, nhưng luồng thì nặng — thread pool là thứ hệ điều hành cung cấp để hấp thụ sự lệch đó.
Sự tiện của ThreadPool và Task.Run của .NET đã được biết rõ, nhưng thực tế Win32 native cũng có một API thread pool chuẩn của hệ điều hành, được thiết kế tốt. Được thiết kế lại toàn diện trong Windows Vista, API này là nền tảng của đồng thời native: nó có thể xử lý work, timer, wait và I/O bất đồng bộ qua một cơ chế callback thống nhất. Hướng tới các nhà phát triển viết ứng dụng, dịch vụ và DLL Windows bằng C/C++, bài viết này giải thích cấu trúc và cách dùng API này, cùng những cái bẫy dễ rơi vào, dựa trên nguồn gốc.
1. Kết luận trước
- Để phát ra rất nhiều công việc sống ngắn, và để thay các luồng chỉ để chờ, thread pool thắng
CreateThreadtự viết. Bạn để việc quản lý luồng cho hệ điều hành và có thể giảm số luồng cùng chuyển ngữ cảnh.1 - Thứ bạn nên dùng là API mới (họ
CreateThreadpoolWork). Việc thiết kế lại ở Vista làm nó đơn giản hơn, tin cậy hơn và hiệu năng cao hơn API cũ (họQueueUserWorkItem), và bạn cũng có thể tạo vài pool độc lập trong một tiến trình.12 - Có bốn loại đối tượng. work, nơi bạn gửi công việc vào; timer, kích hoạt theo thời điểm hoặc chu kỳ; wait, kích hoạt khi một đối tượng kernel được tín hiệu; và io, kích hoạt khi I/O bất đồng bộ hoàn tất. Tất cả chúng đi trên cùng một cơ chế callback.3
- Tắt là “chờ, rồi đóng”. Kỷ luật không để lại callback đang chạy — chờ hoàn tất bằng họ
WaitForThreadpoolWorkCallbacks, hoặc xử lý hàng loạt qua nhóm dọn dẹp — là bắt buộc.4 - Bên trong callback: đừng chặn lâu (nếu sẽ,
CallbackMayRunLong); đừng chờ đồng bộ hoàn tất trên cùng pool; đừng làm bẩn trạng thái của luồng. Ba điều này là quy tắc sắt.56 - Dùng từ DLL: đề phòng cuộc đua unload. Chờ hoàn tất trong một hàm tắt tường minh, và biết các API chuyên dụng như
FreeLibraryWhenCallbackReturns.3
2. Vì sao dùng pool, và khi nào dùng pool
Ý tưởng của thread pool rất đơn giản. Thay vì tạo một luồng mỗi việc, bạn ném công việc (callback) vào một nhóm luồng worker do hệ điều hành quản lý. Các worker thực thi công việc lần lượt, và hệ điều hành điều chỉnh số lượng theo tải.
Tài liệu chính thức liệt kê các loại ứng dụng cụ thể nơi pool mang lại lợi ích.1
- Ứng dụng phát ra rất nhiều hạng mục công việc nhỏ song song (tìm kiếm, I/O mạng, v.v.)
- Ứng dụng thường xuyên tạo rồi dỡ luồng sống ngắn
- Ứng dụng xử lý công việc độc lập song song ở nền
- Ứng dụng giữ các luồng chuyên để chờ đối tượng kernel hoặc sự kiện
Mục cuối dễ bị bỏ sót. Nếu bạn có năm luồng tồn tại chỉ để ngủ hầu “chạy khi sự kiện được tín hiệu”, chúng có thể được thay bằng năm đối tượng wait trên pool, và việc chờ được gom vào các luồng waiter của pool.
Ngược lại, cũng có công việc không hợp với pool. Công việc cần đổi mức ưu tiên luồng, cần COM STA, chạy suốt vòng đời tiến trình — công việc cần một “tính cách” trên luồng thì được giữ trên luồng chuyên dụng. Luồng worker là tài nguyên dùng chung; nó được mượn.
flowchart TB
accTitle: Chọn giữa luồng chuyên dụng và pool
accDescr: Trước hết kiểm tra xem có cần tính cách luồng như mức ưu tiên hoặc STA không, và xem nó có chạy lâu không; chỉ công việc sống ngắn, khối lượng lớn hoặc kiểu chờ không khớp cả hai mới vào thread pool
q1{"Cần tính cách như mức ưu tiên hoặc STA?"} -->|"Có"| ded["Giữ trên luồng chuyên dụng"]
q1 -->|"Không"| q2{"Nó có chạy lâu không?"}
q2 -->|"Có"| ded
q2 -->|"Không"| pool["Đưa vào thread pool"]
pool -.-> ex["Công việc sống ngắn, chờ, timer, hoàn tất I/O"]
Hình 1: Công việc duy nhất bạn được đưa vào pool là công việc “không cần tính cách và kết thúc ngắn”. Mọi thứ khác ở lại trên luồng chuyên dụng như trước.
Còn một mảnh lịch sử cần chốt. API thread pool có hai thế hệ. API cũ tiếp tục từ Windows 2000 (QueueUserWorkItem, RegisterWaitForSingleObject, v.v.), và API mới được thiết kế lại toàn diện ở Vista (họ CreateThreadpoolWork). API mới thống nhất các loại luồng worker, cung cấp luồng bền vững chuyên dụng, nhiều pool trong một tiến trình, nhóm dọn dẹp, và hơn thế, và tài liệu chính thức nói thẳng rằng nó “đơn giản hơn, tin cậy hơn, hiệu năng tốt hơn và linh hoạt hơn”.1 API cũ cũng có ràng buộc cấu trúc như “bạn không thể hủy công việc một khi đã được xếp hàng”.2 Từ đây trở đi, bài viết này chỉ xử lý API mới.
flowchart TB
accTitle: Tương ứng giữa API thread pool cũ và API mới
accDescr: QueueUserWorkItem của API cũ ánh xạ sang đối tượng work của API mới, hàng đợi timer sang timer, registered wait sang wait, và BindIoCompletionCallback sang io
o1["QueueUserWorkItem"] --> n1["work"]
o2["Hàng đợi timer"] --> n2["timer"]
o5["Registered wait"] --> n3["wait"]
o4["BindIoCompletionCallback"] --> n4["io"]
Hình 2: Mục tiêu di chuyển từ API cũ là một-một. Việc kiểm kê mã hiện có có thể bắt đầu từ sự tương ứng này.
3. Bốn đối tượng — work, timer, wait và io
Ở trung tâm API mới là bốn loại đối tượng mà điều kiện kích hoạt callback khác nhau.3
| Đối tượng | Hàm tạo | Khi nào callback kích hoạt |
|---|---|---|
| work | CreateThreadpoolWork | Khi được gửi bằng SubmitThreadpoolWork |
| timer | CreateThreadpoolTimer | Khi thời điểm hoặc chu kỳ đã chỉ định tới |
| wait | CreateThreadpoolWait | Khi một đối tượng kernel trở thành tín hiệu |
| io | CreateThreadpoolIo | Khi I/O bất đồng bộ trên handle gắn kết hoàn tất |
flowchart TB
accTitle: Bốn đối tượng của thread pool và cơ chế callback
accDescr: work kích hoạt khi gửi tường minh, timer theo thời gian, wait theo tín hiệu đối tượng kernel, và io khi I/O bất đồng bộ hoàn tất; tất cả chạy như callback trên cùng nhóm luồng worker
kind{"Đối tượng nào?"}
kind --> w["work(khi gửi)"]
kind --> more{"Timer, wait hay io?"}
more --> t["timer(thời điểm / chu kỳ)"]
more --> rest{"Wait hay io?"}
rest --> wt["wait(khi tín hiệu)"]
rest --> io["io(hoàn tất I/O)"]
w --> pool["Worker chạy callback"]
t --> pool
wt --> pool
io --> pool
Hình 3: Điều kiện kích hoạt khác nhau, nhưng cả bốn được thống nhất trong cơ chế “các worker trên cùng pool chạy callback”.
Sự thống nhất này là một sức mạnh thực tiễn. Thay vì viết xử lý định kỳ, phản hồi sự kiện và xử lý hoàn tất I/O mỗi thứ trên một luồng chuyên dụng, bạn có thể xếp chúng trên một phong cách callback. Timer được gom vào một hàng đợi timer duy nhất cho cả pool, và việc chờ được gom vào một số ít luồng waiter — các luồng “chỉ ngủ” biến mất khỏi tiến trình.1
flowchart TB
accTitle: Thay các luồng chỉ để chờ bằng đối tượng wait
accDescr: Các luồng chỉ để chờ từng ngủ một luồng mỗi sự kiện trở thành đối tượng wait và được gom vào các luồng waiter của pool, nên callback chỉ chạy khi có tín hiệu
old2["5 luồng waiter chuyên dụng ngủ riêng"] -.-> waste["Tiêu thụ 5 stack và 5 luồng"]
new2["5 đối tượng wait"] --> agg["Gom vào các luồng waiter của pool"]
agg --> cb2["Callback chỉ chạy khi có tín hiệu"]
Hình 4: Các luồng “chỉ ngủ và chờ” có thể được gỡ bằng cách biến chúng thành đối tượng wait. Đây là bước đầu rõ ràng cho việc chuyển sang pool.
4. Mẫu cơ bản — Một vòng khứ hồi với đối tượng work
Chúng ta đi qua phép lịch sự một lần với đối tượng work, loại được dùng nhiều nhất.4
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance,
PVOID context, PTP_WORK work)
{
// The context is fixed at creation time. Per-item data is passed through a synchronised queue
WORK_QUEUE* queue = (WORK_QUEUE*)context;
ITEM* item = Dequeue(queue); // Take one item under exclusive control
ProcessItem(item);
}
// 1) Create (bind the callback to the shared context = the queue)
PTP_WORK work = CreateThreadpoolWork(WorkCallback, &queue, NULL);
if (!work) { /* Failure handling with GetLastError */ }
// 2) Submit once for each item you enqueue (keep the item count and the submit count in step)
Enqueue(&queue, item);
SubmitThreadpoolWork(work);
// 3) Stop the submitter, then wait for completion (TRUE also attempts to cancel work that has not yet started)
WaitForThreadpoolWorkCallbacks(work, FALSE);
// 4) Close
CloseThreadpoolWork(work);
Có hai điểm cần nắm. Thứ nhất, bạn có thể SubmitThreadpoolWork cùng một đối tượng work nhiều lần. Mỗi lần gửi chạy callback (song song).7 Tuy nhiên, context truyền vào callback được cố định lúc tạo, nên khi bạn viết “N hạng mục cùng loại công việc” với một đối tượng work, bạn đặt một hàng đợi đồng bộ vào context như trong mã trên và lấy một hạng mục mỗi lần gửi (một thiết kế tạo một đối tượng work mỗi hạng mục cũng ổn). Thứ hai, luôn chờ hoàn tất trước khi đóng. Đóng đối tượng trong khi còn callback đang chạy hoặc đang xếp hàng, hoặc giải phóng bộ nhớ mà callback tham chiếu, chính là use-after-free. Để lần chờ này trở thành lần đóng an toàn, dừng bên gửi trước là điều kiện tiên quyết — trong cấu trúc mà một luồng khác vẫn có thể Submit song song với lần chờ, một lần gửi sau lần chờ đua với Close. Truyền TRUE làm đối số thứ hai của WaitForThreadpoolWorkCallbacks cũng cố hủy các lần gửi chưa bắt đầu.
flowchart TB
accTitle: Vòng đời của đối tượng work
accDescr: Tạo bằng CreateThreadpoolWork; gửi bằng SubmitThreadpoolWork và các callback chạy song song. Khi tắt, trước hết dừng các lần gửi mới, chờ mọi callback hoàn tất bằng WaitForThreadpoolWorkCallbacks, rồi đóng bằng CloseThreadpoolWork
c["Tạo bằng CreateThreadpoolWork"] --> s["Gửi bằng SubmitThreadpoolWork(có thể lặp)"]
s --> run["Các callback chạy song song"]
run --> stop3["Dừng các lần gửi mới"]
stop3 --> w["Chờ hoàn tất bằng WaitForThreadpoolWorkCallbacks"]
w --> cl["Đóng bằng CloseThreadpoolWork"]
Hình 5: Thứ tự tắt là “dừng gửi → chờ hoàn tất → đóng”. Bỏ bất kỳ bước nào thì bạn gặp use-after-free hoặc cuộc đua.
Theo mặc định, callback chạy trên pool mặc định của tiến trình. Với nhiều mục đích điều đó đủ. Chương tiếp theo dành cho khi bạn muốn tách pool.
5. Pool tùy chỉnh và nhóm dọn dẹp
Tách pool. Bạn có thể tạo một pool độc lập bằng CreateThreadpool và đặt cận trên cùng cận dưới của số luồng bằng SetThreadpoolThreadMaximum / SetThreadpoolThreadMinimum.8 Mục đích điển hình là cô lập. Để “công việc kiểu hàng loạt có thể chậm” không ăn hết worker của “công việc cần phản hồi ngay”, bạn tách các pool và cho mỗi pool ngân sách luồng riêng.
Gắn chúng bằng môi trường callback. Pool mà công việc chạy trên được chỉ định bằng cách khởi tạo một TP_CALLBACK_ENVIRON (môi trường callback), trỏ nó tới pool bằng SetThreadpoolCallbackPool, và truyền cái đó làm đối số thứ ba của CreateThreadpoolWork và tương tự.9
Gấp chúng bằng nhóm dọn dẹp. Trong một mô-đun tạo nhiều đối tượng, xử lý tắt dễ trở thành bài đọc “chờ tất cả, đóng tất cả”. Nếu bạn tạo một nhóm bằng CreateThreadpoolCleanupGroup và gắn từng đối tượng vào đó qua môi trường callback, một lần CloseThreadpoolCleanupGroupMembers thực hiện chờ hoàn tất và giải phóng mọi đối tượng thành viên cùng lúc.34
flowchart TB
accTitle: Gắn cấu hình qua môi trường callback
accDescr: Môi trường callback trỏ tới một pool tùy chỉnh và một nhóm dọn dẹp; work và timer tạo với môi trường đó chạy trên pool đó, và một thao tác hàng loạt trên nhóm dọn dẹp gom chờ hoàn tất và giải phóng
env["Môi trường callback(TP_CALLBACK_ENVIRON)"] --> cp["Pool tùy chỉnh(kiểm soát số luồng)"]
env --> cg["Nhóm dọn dẹp"]
env --> obj["Được truyền khi tạo work / timer / wait / io"]
cg -.-> close["Chờ hoàn tất và giải phóng một lần"]
Hình 6: Môi trường callback là cơ chế tiêm “chạy trên pool nào, và ai dọn” tại thời điểm tạo đối tượng.
6. Những cái bẫy — Kỷ luật bên trong callback
Gần như mọi lỗi thread pool đến từ “làm theo ý mình trên một luồng mượn”.
Chặn lâu. Pool điều chỉnh số luồng với giả định callback trở về nhanh. Làm việc lâu hoặc chờ lâu với các mặc định làm chậm việc thực thi các callback khác. Một callback có thể chạy lâu nên khai báo “cái này sẽ chạy lâu” bằng CallbackMayRunLong (pool lấy đó làm gợi ý để thêm một luồng), hoặc được gửi sang luồng chuyên dụng ngay từ đầu. Lưu ý rằng CallbackMayRunLong trả về FALSE khi nó không thể chuẩn bị một worker cho các callback khác. Nếu bạn tiếp tục chặn mà không kiểm tra giá trị trả về, bạn vẫn làm tắc pool, nên khi nó là FALSE, hãy chuyển sang phía không chặn — tách công việc, gửi sang luồng chuyên dụng, v.v.5
Chờ đồng bộ hoàn tất trên cùng pool. Một hình dạng mà, bên trong callback A, bạn chờ bằng WaitForThreadpoolWorkCallbacks hoặc tương tự cho hoàn tất của công việc B đã gửi vào cùng pool trở thành deadlock đói pool ngay khi mọi worker đều “chờ một worker khác”. Hãy viết lại phụ thuộc giữa các việc không phải như một lần chờ mà như một tiếp nối “gửi việc tiếp từ callback hoàn tất của B”.
flowchart TB
accTitle: Cấu trúc của deadlock đói pool
accDescr: Nếu mọi luồng worker chờ đồng bộ hoàn tất của công việc khác đã gửi vào cùng pool, không còn worker trống để chạy công việc đó, và mọi người chờ mãi
w1["Worker 1: đang chờ work X hoàn tất"] --> q["Work X và Y đang chờ chạy"]
w2["Worker 2: đang chờ work Y hoàn tất"] --> q
q -.-> none["Không còn worker trống để chạy chúng"]
none -.-> dead["Mọi người chờ mãi(deadlock đói)"]
Hình 7: Nếu bạn chờ đồng bộ một worker từ bên trong một worker, không còn ai để chạy công việc đang được chờ.
Làm bẩn trạng thái của luồng. Một luồng worker được tái sử dụng cho callback tiếp theo. Đổi mức ưu tiên luồng, trạng thái khởi tạo COM, một giá trị để lại trong TLS, một khóa quên nhả — bất kỳ thứ nào trong số đó trở thành nhiễm bẩn của callback (không liên quan) tiếp theo. “Một hàm bạn ném vào pool không được phụ thuộc vào tính cách của luồng” đã là cảnh báo chính thức từ thời API cũ.6 Có các cơ chế chuyên dụng cho việc dọn dẹp; ví dụ, LeaveCriticalSectionWhenCallbackReturns có thể nhờ pool “nhả khóa này khi callback này trở về”.3
Cuộc đua với việc unload DLL. Nếu DLL chứa mã bị unload trong khi callback đang chạy, bạn gặp vi phạm truy cập. Hình dạng cơ bản là chờ triệt để hoàn tất trong hàm tắt của DLL; FreeLibraryWhenCallbackReturns được cung cấp cho tình huống “callback này là việc cuối, và khi nó xong tôi muốn DLL được giải phóng kể cả bản thân tôi”. Tuy nhiên, API này chỉ “buông một tham chiếu khi callback đang chạy trở về”; nó không ngăn unload trước khi callback bắt đầu. Bạn dùng nó thành một cặp: lấy một tham chiếu mô-đun của riêng mình bằng GetModuleHandleEx trước khi gửi, và để callback buông tham chiếu đó bằng API này.3 Và bạn không được làm lần chờ hoàn tất này bên trong DllMain — như đã nêu trong “DllMain và Loader Lock”, chờ một luồng khác bên trong DllMain là mẫu deadlock.
flowchart TB
accTitle: Chuẩn bị cho cuộc đua giữa unload DLL và callback
accDescr: Unload DLL trong khi callback đang chạy trở thành vi phạm truy cập, nên hình dạng cơ bản là chờ hoàn tất trong hàm tắt tường minh rồi đóng; khi callback cuối tự giải phóng DLL, dùng FreeLibraryWhenCallbackReturns
risk["Unload giữa lúc callback"] -.-> av["Vi phạm truy cập"]
g1["Chờ, rồi đóng"] --> safe["Unload an toàn"]
g1 -.-> g1N["Trong hàm tắt"]
g2["FreeLibraryWhenCallbackReturns"] --> safe
g2 -.-> g2n["callback cuối giải phóng DLL"]
g1 -.-> ng["Không trong DllMain"]
av ~~~ g1
Hình 8: Hình dạng cơ bản là làm “chờ, rồi đóng” trong một hàm tắt tường minh. Chờ bên trong DllMain mời một deadlock khác.
Ngoại lệ và sự cố bên trong công việc đã gửi. Một ngoại lệ không được xử lý trên luồng worker kéo cả tiến trình theo. Áp dụng chính sách bắt ngoại lệ toàn diện ở cửa vào của callback rồi ghi log, giống như bạn sẽ làm với hàm luồng của một luồng chuyên dụng.
7. Quan hệ với thư viện chuẩn và .NET — Bạn viết ở tầng nào
Cuối cùng, chúng ta sắp xếp vị trí của API này với các công cụ khác.
- Nếu C++
std::async/std::threadđủ, chúng là ứng viên đầu tiên. Chúng di động, mã ngắn, và ngay cả ngữ nghĩa củafuturecũng đã được chuẩn chốt.10 - Lý do dùng thread pool Win32 trực tiếp là khi bạn (1) muốn một cơ chế callback thống nhất gồm timer, wait và io, (2) muốn tách pool hoặc kiểm soát số luồng, hoặc (3) không muốn giữ luồng riêng bên trong DLL hoặc thành phần COM.
- Ở phía .NET,
ThreadPoolvàTaskđóng cùng vai trò, và hoàn tất I/O gắn với IOCP. Cấu trúc tầng hầm này được giải thích trong “IOCP và Thread Pool của .NET”.
flowchart TB
accTitle: Quyết định viết bằng công cụ ở tầng nào
accDescr: Nếu async hoặc thread chuẩn của C++ đủ thì dùng chúng; dùng thread pool Win32 trực tiếp khi bạn cần tích hợp timer, wait và hoàn tất I/O, tách pool hoặc kiểm soát số luồng, hoặc không muốn luồng riêng bên trong DLL hay thành phần COM
q1{"Các công cụ C++ chuẩn có đủ không?"} -->|"Có"| std["std::async / std::thread"]
q1 -->|"Không"| q2{"Bạn cần gì?"}
q2 -->|"Tích hợp timer, wait và io"| tp["Thread pool Win32"]
q2 -->|"Tách pool / kiểm soát số"| tp
q2 -->|"Tránh luồng riêng trong DLL"| tp
Hình 9: Khi phân vân, hãy bắt đầu với thư viện chuẩn; tới lượt API này khi một yêu cầu mà chúng không diễn đạt được xuất hiện.
Nói cách khác, API này là nền tảng của đồng thời tại điểm bạn đã quyết định “viết native”. Một cuộc dọn dẹp thực tế là theo giai đoạn: với mục tiêu di chuyển từ sự sinh sôi của CreateThread tự viết, hãy bắt đầu bằng việc đưa đối tượng work vào, rồi thay các luồng chỉ để chờ bằng wait và các luồng timer bằng timer.
8. Tóm tắt
- Để phát ra rất nhiều công việc sống ngắn, và để dọn các luồng chỉ để chờ cùng các luồng chỉ để hẹn giờ, hãy dùng thread pool chuẩn của hệ điều hành chứ không phải luồng tự viết. Thứ bạn dùng là API mới từ Vista trở đi.
- Ở trung tâm là bốn đối tượng work, timer, wait và io. Điều kiện kích hoạt khác nhau; chúng được thống nhất trên cùng nhóm worker và cùng phong cách callback.
- Phép lịch sự là “tạo → gửi → chờ hoàn tất → đóng”. Nhiều lần gửi chạy song song. Một nhóm dọn dẹp có thể gom xử lý tắt.
- Ba quy tắc sắt của callback: đừng chặn lâu (nếu sẽ,
CallbackMayRunLong); đừng chờ đồng bộ trên cùng pool; đừng làm bẩn trạng thái của luồng. - Dùng từ DLL: đề phòng cuộc đua unload. Chờ hoàn tất trong một hàm tắt tường minh; đừng làm trong
DllMain. - Nơi C++ chuẩn hoặc .NET đủ, hãy dùng chúng. Tới lượt API này khi bạn cần tích hợp timer/wait/io hoặc kiểm soát pool.
API thread pool, trong số các API Win32, thuộc phía mới hơn và được thiết kế tốt hơn. Một khi bạn đã xong bước chuyển từ ý tưởng “tạo một luồng” sang ý tưởng “ném một callback”, đồng thời trong mã native trở nên rõ ràng hơn nhiều để viết.
Bài viết liên quan
- Chiều sâu I/O của Windows (Phần 3) — I/O Completion Port (IOCP) và Thread Pool của .NET: Tầng hầm dưới async/await
- Thực hành tốt nhất về đa luồng: Bản C++ — Loại bỏ tai nạn bằng cấu trúc với RAII và jthread
- Thực hành tốt nhất về đa luồng: Bản C — Viết an toàn theo cách Win32 API
- Thức tỉnh giả — Vì sao biến điều kiện thức “mà không được thông báo” và cách chờ đúng trên Windows
- DllMain và Loader Lock — Lý do thật sự bạn bị bảo “đừng làm gì trong khởi tạo DLL”
- Vì sao nên ưu tiên chờ sự kiện hơn Sleep(1) trên Windows
Lĩnh vực tư vấn liên quan
KomuraSoft LLC đảm nhận thiết kế di chuyển từ mã native mà luồng đã sinh sôi sang thread pool, rà soát thiết kế xử lý đồng thời trong ứng dụng C++ và DLL, và điều tra nguyên nhân gốc rễ của treo và sự cố do đói pool hoặc callback. Bạn được chào đón tư vấn bắt đầu từ việc kiểm kê mã hiện có.
- Phát triển ứng dụng Windows
- Tư vấn kỹ thuật và rà soát thiết kế
- Điều tra lỗi và phân tích nguyên nhân gốc rễ
- Liên hệ với chúng tôi
Liên kết tham khảo
-
Microsoft Learn, Thread Pools. Về việc thread pool là tập hợp các luồng worker thực thi hiệu quả các callback bất đồng bộ thay mặt ứng dụng; về các loại ứng dụng mà nó phù hợp (phát ra rất nhiều hạng mục công việc nhỏ song song, thường xuyên tạo rồi dỡ luồng sống ngắn, xử lý công việc độc lập song song, chờ độc quyền trên đối tượng kernel, v.v.); và về việc thiết kế lại toàn diện ở Vista (thống nhất các loại luồng worker, một hàng đợi timer duy nhất, các luồng bền vững chuyên dụng, nhóm dọn dẹp, nhiều pool trong một tiến trình, và API mới). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Thread Pooling. Về cấu trúc các API thread pool cũ hơn (QueueUserWorkItem, hàng đợi timer, registered wait, BindIoCompletionCallback); về việc không có cách hủy công việc một khi đã được xếp hàng; và về việc API thread pool mới được đưa vào ở Vista được nêu là đơn giản hơn và vượt trội về độ tin cậy, hiệu năng và tính linh hoạt. ↩ ↩2
-
Microsoft Learn, threadpoolapiset.h header. Về danh sách hàm gồm bốn hàm tạo đối tượng CreateThreadpoolWork, CreateThreadpoolTimer, CreateThreadpoolWait và CreateThreadpoolIo; nhóm dọn dẹp (CreateThreadpoolCleanupGroup); và dọn dẹp gắn với hoàn tất callback (LeaveCriticalSectionWhenCallbackReturns, FreeLibraryWhenCallbackReturns, v.v.). ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Using the Thread Pool Functions. Về quy trình cơ bản tạo bằng CreateThreadpoolWork, gửi bằng SubmitThreadpoolWork, chờ hoàn tất bằng WaitForThreadpoolWorkCallbacks, và đóng bằng CloseThreadpoolWork; và về ví dụ cấu hình kết hợp pool tùy chỉnh với môi trường callback và nhóm dọn dẹp. ↩ ↩2 ↩3
-
Microsoft Learn, CallbackMayRunLong function (threadpoolapiset.h). Về việc thông báo cho pool rằng callback hiện tại có thể chạy lâu, để pool dùng điều đó làm chất liệu quyết định có bảo đảm một luồng cho các callback khác hay không; và về việc cân nhắc một luồng chuyên dụng cho callback chạy lâu khi có thể. ↩ ↩2
-
Microsoft Learn, Thread Pooling. Về việc các hạng mục công việc gửi vào thread pool, và các hàm chúng gọi, phải an toàn với thread pool; về việc không giả định rằng luồng đang thực thi là một luồng bền vững chuyên dụng; và về việc tránh dùng TLS cùng các lời gọi bất đồng bộ đòi hỏi một luồng bền vững. ↩ ↩2
-
Microsoft Learn, SubmitThreadpoolWork function (threadpoolapiset.h). Về việc có thể gửi cùng một đối tượng work nhiều lần mà không chờ callback trước hoàn tất, để các callback chạy song song; và về việc pool có thể điều chỉnh (throttle) số luồng vì hiệu quả. ↩
-
Microsoft Learn, SetThreadpoolThreadMaximum function (threadpoolapiset.h). Về việc có thể đặt cận trên cho số luồng worker của một pool tạo bằng CreateThreadpool (cận dưới là SetThreadpoolThreadMinimum). ↩
-
Microsoft Learn, CreateThreadpoolWork function (threadpoolapiset.h). Về việc tạo đối tượng work từ một hàm callback và một con trỏ context; và về đối số thứ ba, TP_CALLBACK_ENVIRON, có thể chỉ định môi trường thực thi của callback (pool nó thuộc về, v.v.), với NULL nghĩa là nó chạy trong môi trường mặc định. ↩
-
Microsoft Learn, <future>. Về việc thực thi bất đồng bộ theo tác vụ qua std::async và future được cung cấp như thư viện chuẩn, nên bạn có thể viết đồng thời mà không quản lý luồng trực tiếp. ↩
Bài viết liên quan
Các bài viết gần đây có cùng thẻ để tìm hiểu sâu hơn những chủ đề lân cận.
Named pipe trong thực tiễn — IPC chuẩn của Windows, từ thiết kế đến bảo mật
Hướng dẫn thực tiễn về named pipe, cơ chế giao tiếp giữa các tiến trình chuẩn của Windows. Bài viết sắp xếp, từ nguồn gốc, việc chọn giữa...
Bên trong ảo hóa Windows (Phần 3) — Máy ảo khởi động trong vài giây: Vì sao WSL2, Windows Sandbox và container lại nhẹ
Vì sao WSL2 và Windows Sandbox khởi động trong vài giây và cảm giác nhẹ đến vậy? Bài viết này giải thích các cơ chế, từ ảnh cơ sở động và...
Bên trong ảo hóa Windows (Phần 2) — Bộ nhớ ngay cả kernel cũng không thấy: VBS, HVCI và Credential Guard
Khi cài sạch trên phần cứng tương thích, VBS được bật mặc định và dùng hypervisor cùng SLAT để tạo cô lập mạnh hơn kernel. Bài viết này g...
Bên trong ảo hóa Windows (Phần 1) — Windows của bạn thực sự chạy ở đâu? Hypervisor và phân vùng
Khi bạn bật Hyper-V, chính Windows máy chủ chạy trên hypervisor như phân vùng gốc. Bài viết này giải thích nền tảng ảo hóa qua vai trò củ...
Giúp website xuất hiện khi khách tìm theo tên khu vực — Hướng dẫn Local SEO thực tiễn cho doanh nghiệp vừa và nhỏ (trang khu vực và Google Business Profile)
Dành cho các doanh nghiệp vừa và nhỏ có website không xuất hiện khi ai đó tìm kiếm "tên khu vực + ngành nghề". Bài viết này trình bày thứ...
Chủ đề liên quan
Các trang này đặt chủ đề trong bối cảnh rộng hơn của dịch vụ và quyết định.
Chủ đề kỹ thuật Windows
Cổng vào phát triển Windows, điều tra lỗi và khai thác tài sản hiện có.
Dịch vụ liên quan đến chủ đề này
Bài viết liên quan trực tiếp đến các dịch vụ sau.
Phát triển ứng dụng Windows
Ứng dụng nghiệp vụ, tích hợp thiết bị và công cụ liên lạc, từ yêu cầu đến phát triển.
Câu hỏi thường gặp
Các câu hỏi thường gặp khi tư vấn về chủ đề của bài viết.
- Thread pool tốt hơn việc tự tạo luồng bằng CreateThread ở chỗ nào?
- Hiệu quả khi bạn có rất nhiều công việc sống ngắn cần làm, và việc giảm mã quản lý luồng. Tạo rồi hủy một luồng có chi phí không thể bỏ qua, nên một ứng dụng lặp "CreateThread cho mỗi việc rồi hủy khi xong", hoặc giữ nhiều luồng tồn tại chỉ để ngủ chờ sự kiện, có thể giảm số luồng và chuyển ngữ cảnh bằng cách chuyển sang pool. Tài liệu chính thức cũng liệt kê là ứng viên của pool những ứng dụng phát ra rất nhiều hạng mục công việc nhỏ song song, ứng dụng tạo nhiều luồng sống ngắn, và ứng dụng có luồng chỉ để chờ đối tượng kernel. Ngược lại, công việc "cần một tính cách riêng trên luồng" — đổi mức ưu tiên, COM STA, xử lý chuyên dụng chạy lâu — vẫn nên được giữ trên luồng chuyên dụng như trước.
- Điều này khác các hàm thread pool cũ như QueueUserWorkItem như thế nào?
- Thread pool được thiết kế lại toàn diện trong Windows Vista. Các API họ threadpoolapiset hiện tại (CreateThreadpoolWork và tương tự) là API mới; QueueUserWorkItem, RegisterWaitForSingleObject và tương tự là API cũ (legacy). API mới thống nhất các loại luồng worker, cho phép tạo vài pool độc lập trong một tiến trình, và cung cấp các cơ chế như giải phóng hàng loạt qua nhóm dọn dẹp cùng việc nhả khóa hoặc unload DLL gắn với hoàn tất callback. Tài liệu chính thức cũng nói API mới đơn giản hơn và vượt trội về độ tin cậy, hiệu năng và tính linh hoạt. API cũ còn có ràng buộc cấu trúc như "không có cách hủy công việc một khi đã được xếp hàng", nên hãy dùng API mới trong mã mới.
- Có những việc không được làm bên trong callback không?
- Có ba việc lớn. Thứ nhất, chặn lâu hoặc làm việc lâu với các mặc định. Pool điều chỉnh số luồng với giả định callback kết thúc nhanh, nên với công việc sẽ mất nhiều thời gian bạn hoặc khai báo bằng CallbackMayRunLong hoặc dùng luồng chuyên dụng. Thứ hai, chờ đồng bộ hoàn tất của công việc khác bạn đã gửi vào cùng pool. Nếu mọi worker đều "chờ một worker khác", bạn gặp deadlock đói pool. Thứ ba, phụ thuộc vào tính cách của luồng. Luồng worker được chia sẻ giữa các callback, nên trở về với mức ưu tiên luồng hoặc trạng thái khởi tạo COM đã đổi, hoặc để lại trạng thái trong TLS, sẽ nhiễm bẩn callback tiếp theo. Để dọn dẹp lúc kết thúc (nhả khóa hoặc unload DLL), các cơ chế chuyên dụng như LeaveCriticalSectionWhenCallbackReturns và FreeLibraryWhenCallbackReturns được cung cấp.
- Có điểm cần chú ý khi dùng thread pool từ một DLL không?
- Nguy hiểm lớn nhất là "DLL bị unload trong khi callback vẫn đang chạy". Nếu callback chạy sau khi unload, bạn gặp vi phạm truy cập. Phía DLL phải, trong xử lý tắt, chờ đáng tin cậy hoàn tất các callback nó đã phát — bằng hàm chờ như WaitForThreadpoolWorkCallbacks, hoặc CloseThreadpoolCleanupGroupMembers trên một nhóm dọn dẹp — rồi mới đóng các đối tượng. Tuy nhiên, chờ việc này bên trong DllMain có thể deadlock do tương tác với loader lock, nên quy tắc là làm trong một hàm tắt tường minh, không trong DllMain. Với tình huống bản thân callback muốn giải phóng DLL vì "việc này là việc cuối", một API chuyên dụng, FreeLibraryWhenCallbackReturns, được cung cấp.
- Giờ đã có C++ std::async và ThreadPool của .NET, vẫn còn dịp dùng API này trực tiếp không?
- Có. Tiêu chí là "công cụ ở tầng đó có đủ không". Nếu độ hạt đồng thời bạn cần trong C++ được std::async hoặc std::thread bao phủ, thư viện chuẩn là ứng viên đầu tiên cả từ góc độ tính di động. Mặt khác, muốn thống nhất timer, chờ đối tượng kernel, và hoàn tất I/O bất đồng bộ trong một cơ chế callback; muốn tách pool và kiểm soát số luồng theo loại công việc; không muốn giữ luồng riêng bên trong DLL hoặc thành phần COM — những yêu cầu đó là những gì thread pool Win32 bao phủ. Mối quan hệ với ThreadPool của .NET và IOCP được trình bày trong một bài liên quan, và miễn là bạn đang viết native, biết cơ chế nằm ở tầng dưới này không lãng phí.