144
Chương 4: Tiểu trình, tiến trình, và sự đồng bộ
Thách thức lớn nhất trong việc viết một ứng dụng hỗ-trợ-đa-tiểu-trình là bảo đảm các tiểu
trình làm việc trong sự hòa hợp. Việc này thường được gọi là “đồng bộ hóa tiểu trình” và bao
gồm:
•
Bảo đảm các tiểu trình truy xuất các đối tượng và dữ liệu dùng chung một cách phù hợp
để không gây ra sai lạc.
•
Bảo đảm các tiểu trình chỉ thực thi khi thật sự cần thiết và phải đảm bảo rằng chúng chỉ
được thực thi với chi phí tối thiểu khi chúng rỗi.
Cơ chế đồng bộ hóa thông dụng nhất là lớp
Monitor
. Lớp này cho phép một tiểu trình đơn thu
lấy chốt (lock) trên một đối tượng bằng cách gọi phương thức tĩnh
Monitor.Enter
. Bằng cách
thu lấy chốt trước khi truy xuất một tài nguyên hay dữ liệu dùng chung, ta chắc chắn rằng chỉ
có một tiểu trình có thể truy xuất tài nguyên đó cùng lúc. Một khi đã hoàn tất với tài nguyên,
tiểu trình này sẽ giải phóng chốt để tiểu trình khác có thể truy xuất nó. Khối mã thực hiện
công việc này thường được gọi là vùng hành căng (critical section).
Bạn có thể sử dụng bất kỳ đối tượng nào đóng vai trò làm chốt, và sử dụng từ khóa
this
để
thu lấy chốt trên đối tượng hiện tại. Điểm chính là tất cả các tiểu trình khi truy xuất một tài
nguyên dùng chung phải thu lấy cùng một chốt. Các tiểu trình khác khi thu lấy chốt trên cùng
một đối tượng sẽ block (đi vào trạng thái
WaitSleepJoin
) và được thêm vào hàng sẵn sàng
(ready queue) của chốt này cho đến khi tiểu trình chủ giải phóng nó bằng phương thức tĩnh
Monitor.Exit
. Khi tiểu trình chủ gọi
Exit
, một trong các tiểu trình từ hàng sẵn sàng sẽ thu lấy
chốt. Nếu tiểu trình chủ không giải phóng chốt bằng
Exit
, tất cả các tiểu trình khác sẽ block
vô hạn định. Vì vậy, cần đặt lời gọi
Exit
bên trong khối
finally
để bảo đảm nó được gọi cả
khi ngoại lệ xảy ra.
Vì
Monitor
thường xuyên được sử dụng trong các ứng dụng hỗ-trợ-đa-tiểu-trình nên C# cung
cấp hỗ trợ mức-ngôn-ngữ thông qua lệnh
lock
. Khối mã được gói trong lệnh
lock
tương
đương với gọi
Monitor.Enter
khi đi vào khối mã này, và gọi
Monitor.Exit
khi đi ra khối mã
này. Ngoài ra, trình biên dịch tự động đặt lời gọi
Monitor.Exit
trong khối
finally
để bảo đảm
chốt được giải phóng khi một ngoại lệ bị ném.
Tiểu trình chủ (sở hữu chốt) có thể gọi
Monitor.Wait
để giải phóng chốt và đặt tiểu trình này
vào hàng chờ (wait queue). Các tiểu trình trong hàng chờ cũng có trạng thái là
WaitSleepJoin
và sẽ tiếp tục block cho đến khi tiểu trình chủ gọi phương thức
Pulse
hay
PulseAll
của lớp
Monitor
. Phương thức
Pulse
di chuyển một trong các tiểu trình từ hàng chờ vào hàng sẵn
sàng, còn phương thức
PulseAll
thì di chuyển tất cả các tiểu trình. Khi một tiểu trình đã được
di chuyển từ hàng chờ vào hàng sẵn sàng, nó có thể thu lấy chốt trong lần giải phóng kế tiếp.
Cần hiểu rằng các tiểu trình thuộc hàng chờ sẽ không thu được chốt, chúng sẽ đợi vô hạn định
cho đến khi bạn gọi
Pulse
hay
PulseAll
để di chuyển chúng vào hàng sẵn sàng. Sử dụng
Wait
và
Pulse
là cách phổ biến khi thread-pool được sử dụng để xử lý các item từ một hàng đợi
dùng chung.
Lớp
ThreadSyncExample
dưới đây trình bày cách sử dụng lớp
Monitor
và lệnh
lock
. Ví dụ này
khởi chạy ba tiểu trình, mỗi tiểu trình (lần lượt) thu lấy chốt của một đối tượng có tên là
consoleGate
. Kế đó, mỗi tiểu trình gọi phương thức
Monitor.Wait
. Khi người dùng nhấn
Enter lần đầu tiên,
Monitor.Pulse
sẽ được gọi để giải phóng một tiểu trình đang chờ. Lần thứ