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ủ g
Trang 1Console.WriteLine("{0} : Thread wasn't " +
"suspended.",
DateTime.Now.ToString("HH:mm:ss.ffff"));
}
break;
case 'I':
// Gián đoạn tiểu trình thứ hai.
Console.WriteLine("{0} : Interrupting second " +
"thread.",
DateTime.Now.ToString("HH:mm:ss.ffff"));
thread.Interrupt();
break;
case 'E':
// Hủy bỏ tiểu trình thứ hai và truyền một đối tượng
// trạng thái cho tiểu trình đang bị hủy,
// trong trường hợp này là một thông báo.
Console.WriteLine("{0} : Aborting second thread.",
DateTime.Now.ToString("HH:mm:ss.ffff"));
thread.Abort("Terminating example.");
// Đợi tiểu trình thứ hai kết thúc.
thread.Join();
break;
}
} while (command != 'E');
// Nhấn Enter để kết thúc.
Console.WriteLine("Main method complete Press Enter.");
Console.ReadLine();
}
}
Trang 27. Nh n bi t khi nào m t ti u trình k t thúc Nh n bi t khi nào m t ti u trình k t thúc ậ ậ ế ế ộ ể ộ ể ế ế
Bạn muốn biết khi nào một tiểu trình đã kết thúc.
Sử dụng thuộc tính IsAlive hay phương thức Join của lớp Thread
Cách dễ nhất để kiểm tra một tiểu trình đã kết thúc hay chưa là kiểm tra thuộc tính Thread.IsAlive Thuộc tính này trả về true nếu tiểu trình đã được khởi chạy nhưng chưa kết thúc hay bị hủy
Thông thường, bạn sẽ cần một tiểu trình để đợi một tiểu trình khác hoàn tất việc xử lý của nó Thay vì kiểm tra thuộc tính IsAlive trong một vòng lặp, bạn có thể sử dụng phương thức Thread.Join Phương thức này khiến tiểu trình đang gọi dừng lại (block) cho đến khi tiểu trình được tham chiếu kết thúc Bạn có thể tùy chọn chỉ định một khoảng thời gian (giá trị int hay TimeSpan) mà sau khoảng thời gian này, Join sẽ hết hiệu lực và quá trình thực thi của tiểu trình đang gọi sẽ phục hồi lại Nếu bạn chỉ định một giá trị time-out, Join trả về true nếu tiểu trình đã kết thúc, và false nếu Join đã hết hiệu lực
Ví dụ dưới đây thực thi một tiểu trình thứ hai và rồi gọi Join để đợi tiểu trình thứ hai kết thúc
Vì tiểu trình thứ hai mất 5 giây để thực thi, nhưng phương thức Join chỉ định giá trị time-out
là 3 giây, nên Join sẽ luôn hết hiệu lực và ví dụ này sẽ hiển thị một thông báo ra cửa sổ
Console.
using System;
using System.Threading;
public class ThreadFinishExample {
private static void DisplayMessage() {
// Hiển thị một thông báo ra cửa sổ Console 5 lần.
for (int count = 0; count < 5; count++) {
Console.WriteLine("{0} : Second thread",
DateTime.Now.ToString("HH:mm:ss.ffff"));
// Nghỉ 1 giây.
Thread.Sleep(1000);
}
}
Trang 3// Tạo một thể hiện ủy nhiệm ThreadStart
// tham chiếu đến DisplayMessage.
ThreadStart method = new ThreadStart(DisplayMessage);
// Tạo một đối tượng Thread và truyền thể hiện ủy nhiệm
// ThreadStart cho phương thức khởi dựng của nó.
Thread thread = new Thread(method);
Console.WriteLine("{0} : Starting second thread.",
DateTime.Now.ToString("HH:mm:ss.ffff"));
// Khởi chạy tiểu trình thứ hai.
thread.Start();
// Dừng cho đến khi tiểu trình thứ hai kết thúc,
// hoặc Join hết hiệu lực sau 3 giây.
if (!thread.Join(3000)) {
Console.WriteLine("{0} : Join timed out !!",
DateTime.Now.ToString("HH:mm:ss.ffff"));
}
// Nhấn Enter để kết thúc.
Console.WriteLine("Main method complete Press Enter.");
Console.ReadLine();
}
}
8. Đ ng b hóa quá trình th c thi c a nhi u ti u trình Đ ng b hóa quá trình th c thi c a nhi u ti u trình ồ ồ ộ ộ ự ự ủ ủ ề ể ề ể
Bạn cần phối hợp các hoạt động của nhiều tiểu trình để bảo đảm sử dụng hiệu
quả các tài nguyên dùng chung, và bạn không làm sai lạc dữ liệu dùng chung khi
một phép chuyển ngữ cảnh tiểu trình (thread context switch) xảy ra trong quá
trình thay đổi dữ liệu.
Sử dụng các lớp Monitor , AutoResetEvent , ManualResetEvent , và Mutex (thuộc
không gian tên System.Threading ).
Trang 4Thá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ứ
Trang 5đang chờ còn lại.
using System;
using System.Threading;
public class ThreadSyncExample {
private static object consoleGate = new Object();
private static void DisplayMessage() {
Console.WriteLine("{0} : Thread started, acquiring lock ",
DateTime.Now.ToString("HH:mm:ss.ffff"));
// Thu lấy chốt trên đối tượng consoleGate.
try {
Monitor.Enter(consoleGate);
Console.WriteLine("{0} : {1}",
DateTime.Now.ToString("HH:mm:ss.ffff"),
"Acquired consoleGate lock, waiting ");
// Đợi cho đến khi Pulse được gọi trên đối tượng consoleGate.
Monitor.Wait(consoleGate);
Console.WriteLine("{0} : Thread pulsed, terminating.",
DateTime.Now.ToString("HH:mm:ss.ffff"));
} finally {
Monitor.Exit(consoleGate);
}
}
public static void Main() {
Trang 6// Thu lấy chốt trên đối tượng consoleGate.
lock (consoleGate) {
// Tạo và khởi chạy ba tiểu trình mới
// (chạy phương thức DisplayMesssage).
for (int count = 0; count < 3; count++) {
(new Thread(new ThreadStart(DisplayMessage))).Start(); }
}
Thread.Sleep(1000);
// Đánh thức một tiểu trình đang chờ.
Console.WriteLine("{0} : {1}",
DateTime.Now.ToString("HH:mm:ss.ffff"),
"Press Enter to pulse one waiting thread.");
Console.ReadLine();
// Thu lấy chốt trên đối tượng consoleGate.
lock (consoleGate) {
// Pulse một tiểu trình đang chờ.
Monitor.Pulse(consoleGate);
}
// Đánh thức tất cả các tiểu trình đang chờ.
Console.WriteLine("{0} : {1}",
DateTime.Now.ToString("HH:mm:ss.ffff"),
"Press Enter to pulse all waiting threads.");
Console.ReadLine();
// Thu lấy chốt trên đối tượng consoleGate.
Trang 7
// Pulse tất cả các tiểu trình đang chờ.
Monitor.PulseAll(consoleGate);
}
// Nhấn Enter để kết thúc.
Console.WriteLine("Main method complete Press Enter.");
Console.ReadLine();
}
}
Các lớp thông dụng khác dùng để đồng bộ hóa tiểu trình là các lớp con của lớp System.Threading.WaitHandle, bao gồm AutoResetEvent, ManualResetEvent, và Mutex Thể
hiện của các lớp này có thể ở trạng thái signaled hay unsignaled Các tiểu trình có thể sử dụng
các phương thức của các lớp được liệt kê trong bảng 4.2 (được thừa kế từ lớp WaitHandle) để
đi vào trạng thái WaitSleepJoin và đợi trạng thái của một hay nhiều đối tượng dẫn xuất từ WaitHandle biến thành signaled.
Bảng 4.2 Các phương thức của WaitHandle dùng để đồng bộ hóa quá trình thực thi của các tiểu
trình
WaitAny
Tiểu trình gọi phương thức tĩnh này sẽ đi vào trạng thái WaitSleepJoin
và đợi bất kỳ một trong các đối tượng WaitHandle thuộc một mảng WaitHandle biến thành signaled Bạn cũng có thể chỉ định giá trị time-out
WaitAll
Tiểu trình gọi phương thức tĩnh này sẽ đi vào trạng thái WaitSleepJoin
và đợi tất cả các đối tượng WaitHandle trong một mảng WaitHandle biến
thành signaled Bạn cũng có thể chỉ định giá trị time-out Phương thức
WaitAllExample trong mục 4.2 đã trình bày cách sử dụng phương thức WaitAll.
WaitOne
Tiểu trình gọi phương thức này sẽ đi vào trạng thái WaitSleepJoin và đợi một đối tượng WaitHandle cụ thể biến thành signaled Phương thức WaitingExample trong mục 4.2 đã trình bày cách sử dụng phương thức WaitOne.
Điểm khác biệt chính giữa các lớp AutoResetEvent, ManualResetEvent, và Mutex là cách thức
chúng chuyển trạng thái từ signaled thành unsignaled, và tính khả kiến (visibility) của chúng
Lớp AutoResetEvent và ManualResetEvent là cục bộ đối với một tiến trình Để ra hiệu một AutoResetEvent, bạn hãy gọi phương thức Set của nó, phương thức này chỉ giải phóng một tiểu trình đang đợi sự kiện AutoResetEvent sẽ tự động trở về trạng thái unsignaled Ví dụ trong mục 4.4 đã trình bày cách sử dụng lớp AutoResetEvent
Trang 8Lớp ManualResetEvent phải được chuyển đổi qua lại giữa signaled và unsignaled bằng phương thức Set và Reset của nó Gọi Set trên một ManualResetEvent sẽ đặt trạng thái của nó
là signaled, giải phóng tất cả các tiểu trình đang đợi sự kiện Chỉ khi gọi Reset mới làm cho
ManualResetEvent trở thành unsignaled.
Một Mutex là signaled khi nó không thuộc sở hữu của bất kỳ tiểu trình nào Một tiểu trình giành quyền sở hữu Mutex lúc khởi dựng hoặc sử dụng một trong các phương thức được liệt
kê trong bảng 4.2 Quyền sở hữu Mutex được giải phóng bằng cách gọi phương thức Mutex.ReleaseMutex (ra hiệu Mutex và cho phép một tiểu trình khác thu lấy quyền sở hữu này) Thuận lợi chính của Mutex là bạn có thể sử dụng chúng để đồng bộ hóa các tiểu trình qua các biên tiến trình Mục 4.12 đã trình bày cách sử dụng Mutex
Ngoài các chức năng vừa được mô tả, điểm khác biệt chính giữa các lớp WaitHandle và lớp Monitor là lớp Monitor được hiện thực hoàn toàn bằng mã lệnh được-quản-lý, trong khi các lớp WaitHandle cung cấp vỏ bọc cho các chức năng bên dưới của của hệ điều hành Điều này dẫn đến hệ quả là:
• Sử dụng lớp Monitor đồng nghĩa với việc mã lệnh của bạn sẽ khả chuyển hơn vì không
bị lệ thuộc vào khả năng của hệ điều hành bên dưới
• Bạn có thể sử dụng các lớp dẫn xuất từ WaitHandle để đồng bộ hóa việc thực thi của các tiểu trình được-quản-lý và không-được-quản-lý, trong khi lớp Monitor chỉ có thể đồng
bộ hóa các tiểu trình được-quản-lý
9. T o m t đ i t T o m t đ i t ạ ộ ố ượ ạ ộ ố ượ ng t p h p có tính ch t an-toàn-v -ti u-trình ng t p h p có tính ch t an-toàn-v -ti u-trình ậ ợ ậ ợ ấ ấ ề ể ề ể
Bạn muốn nhiều tiểu trình có thể đồng thời truy xuất nội dung của một tập hợp
một cách an toàn.
Sử dụng lệnh lock để đồng bộ hóa các tiểu trình truy xuất đến tập hợp, hoặc truy
xuất tập hợp thông qua một vỏ bọc có tính chất an-toàn-về-tiểu-trình
(thread-safe).
Theo mặc định, các lớp tập hợp chuẩn thuộc không gian tên System.Collections và System.Collections.Specialized sẽ hỗ trợ việc nhiều tiểu trình đồng thời đọc nội dung của tập hợp Tuy nhiên, nếu một hay nhiều tiểu trình này sửa đổi tập hợp, nhất định bạn sẽ gặp rắc rối Đó là vì hệ điều hành có thể làm đứt quãng các hành động của tiểu trình trong khi tập hợp chỉ mới được sửa đổi một phần Điều này sẽ đưa tập hợp vào một trạng thái vô định, chắc chắn khiến cho một tiểu trình khác truy xuất tập hợp thất bại, trả về dữ liệu sai, hoặc làm hỏng tập hợp
Sử dụng “đồng bộ hóa tiểu trình” sẽ sinh ra một chi phí hiệu năng Cứ để tập
hợp là không-an-toàn-về-tiểu-trình (non-thread-safe) như mặc định sẽ cho hiệu
năng tốt hơn đối với các trường hợp có nhiều tiểu trình không được dùng đến.
Tất cả các tập hợp thông dụng nhất đều hiện thực một phương thức tĩnh có tên là Synchronized; bao gồm các lớp: ArrayList, Hashtable, Queue, SortedList, và Stack (thuộc không gian tên System.Collections) Phương thức Synchronized nhận một đối tượng tập hợp
Trang 9(synchronized wrapper) bao lấy đối tượng tập hợp đã được chỉ định Đối tượng vỏ bọc này có
cùng kiểu với tập hợp gốc, nhưng tất cả các phương thức và thuộc tính dùng để đọc và ghi tập hợp bảo đảm rằng chỉ một tiểu trình có khả năng truy xuất nội dung của tập hợp cùng lúc Đoạn mã dưới đây trình bày cách tạo một Hashtable có tính chất an-toàn-về-tiểu-trình (bạn có thể kiểm tra một tập hợp có phải là an-toàn-về-tiểu-trình hay không bằng thuộc tính IsSynchronized)
// Tạo một Hashtable chuẩn.
Hashtable hUnsync = new Hashtable();
// Tạo một vỏ bọc được-đồng-bộ-hóa.
Hashtable hSync = Hashtable.Synchronized(hUnsync);
Các lớp tập hợp như HybridDictionary, ListDictionary, và StringCollection (thuộc không gian tên System.Collections.Specialized) không hiện thực phương thức Synchronized Để cung cấp khả năng truy xuất an-toàn-về-tiểu-trình đến thể hiện của các lớp này, bạn phải hiện thực quá trình đồng bộ hóa (sử dụng đối tượng được trả về từ thuộc tính SyncRoot) như được trình bày trong đoạn mã dưới đây:
// Tạo một NameValueCollection.
NameValueCollection nvCollection = new NameValueCollection();
// Thu lấy chốt trên NameValueCollection trước khi thực hiện sửa đổi.
lock (((ICollection)nvCollection).SyncRoot) {
// Sửa đổi NameValueCollection
}
Chú ý rằng lớp NameValueCollection dẫn xuất từ lớp NameObjectCollectionBase, lớp cơ sở này sử dụng cơ chế hiện thực giao diện tường minh để hiện thực thuộc tính ICollection.SyncRoot Như đã được trình bày, bạn phải ép NameValueCollection về ICollection trước khi truy xuất thuộc tính SyncRoot Việc ép kiểu là không cần thiết đối với các lớp tập hợp chuyên biệt như HybridDictionary, ListDictionary, và StringCollection (các lớp này không sử dụng cơ chế hiện thực giao diện tường minh để hiện thực SyncRoot) Nếu cần sử dụng rộng khắp lớp tập hợp đã được đồng bộ hóa, bạn có thể đơn giản hóa mã lệnh bằng cách tạo một lớp mới dẫn xuất từ lớp tập hợp cần sử dụng Kế tiếp, chép đè các thành viên của lớp cơ sở cung cấp khả năng truy xuất nội dung của tập hợp và thực hiện đồng
bộ hóa trước khi gọi thành viên lớp cơ sở tương đương Bạn có thể sử dụng lệnh lock một cách bình thường để đồng bộ hóa đối tượng được trả về bởi thuộc tính SyncRoot của lớp cơ sở như đã được thảo luận ở trên Tuy nhiên, bằng cách tạo lớp dẫn xuất, bạn có thể hiện thực các
kỹ thuật đồng bộ hóa cao cấp hơn, chẳng hạn sử dụng System.Threading.ReaderWriterLock
để cho phép nhiều tiểu trình đọc nhưng chỉ một tiểu trình ghi
10. Kh i ch y m t ti n trình m i Kh i ch y m t ti n trình m i ở ạ ộ ế ở ạ ộ ế ớ ớ
Bạn cần thực thi một ứng dụng trong một tiến trình mới.
Trang 10 Sử dụng đối tượng System.Diagnostics.ProcessStartInfo để chỉ định các chi tiết
cho ứng dụng cần chạy Sau đó, tạo đối tượng System.Diagnostics.Process để mô
tả tiến trình mới, gán đối tượng ProcessStartInfo cho thuộc tính StartInfo của đối tượng Process , và rồi khởi chạy ứng dụng bằng cách gọi Process.Start
Lớp Process cung cấp một dạng biểu diễn được-quản-lý cho một tiến trình của hệ điều hành
và cung cấp một cơ chế đơn giản mà thông qua đó, bạn có thể thực thi cả ứng dụng được-quản-lý lẫn không-được-được-quản-lý Lớp Process hiện thực bốn phiên bản nạp chồng cho phương thức Start (bạn có thể sử dụng phương thức này để khởi chạy một tiến trình mới) Hai trong số này là các phương thức tĩnh, cho phép bạn chỉ định tên và các đối số cho tiến
trình mới Ví dụ, hai lệnh dưới đây đều thực thi Notepad trong một tiến trình mới:
// Thực thi notepad.exe, không có đối số.
Process.Start("notepad.exe");
// Thực thi notepad.exe, tên file cần mở là đối số.
Process.Start("notepad.exe", "SomeFile.txt");
Hai dạng khác của phương thức Start yêu cầu bạn tạo đối tượng ProcessStartInfo được cấu hình với các chi tiết của tiến trình cần chạy; việc sử dụng đối tượng ProcessStartInfo cung cấp một cơ chế điều khiển tốt hơn trên các hành vi và cấu hình của tiến trình mới Bảng 4.3 tóm tắt một vài thuộc tính thông dụng của lớp ProcessStartInfo
Bảng 4.3 Các thuộc tính của lớp ProcessStartInfo
Arguments Các đối số dùng để truyền cho tiến trình mới
ErrorDialog
Nếu Process.Start không thể khởi chạy tiến trình đã được chỉ định,
nó sẽ ném ngoại lệ System.ComponentModel.Win32Exception Nếu ErrorDialog là true, Start sẽ hiển thị một thông báo lỗi trước khi ném ngoại lệ
FileName
Tên của ứng dụng Bạn cũng có thể chỉ định bất kỳ kiểu file nào mà bạn đã cấu hình ứng dụng kết giao với nó Ví dụ, nếu bạn chỉ định
một file với phần mở rộng là doc hay xls, Microsoft Word hay
Microsoft Excel sẽ chạy.
WindowStyle
Một thành viên thuộc kiểu liệt kê System.Diagnostics ProcessWindowStyle, điều khiển cách thức hiển thị của cửa sổ Các giá trị hợp lệ bao gồm: Hidden, Maximized, Minimized, và Normal WorkingDirectory Tên đầy đủ của thư mục làm việc
Khi đã hoàn tất với một đối tượng Process, bạn nên hủy nó để giải phóng các tài nguyên hệ thống—gọi Close, Dispose, hoặc tạo đối tượng Process bên trong tầm vực của lệnh using Việc hủy một đối tượng Process không ảnh hưởng lên tiến trình hệ thống nằm dưới, tiến trình này vẫn sẽ tiếp tục chạy