1.1 Nhận biết khi nào một tiểutrình kết thúc V V Bạn muốn biết khi nào một tiểutrì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ểutrì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ểutrì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ểutrình để đợi một tiểutrì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ểutrình đang gọi dừng lại (block) cho đến khi tiểutrì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ểutrì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ểutrì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ểutrình thứ hai và rồi gọi Join để đợi tiểutrình thứ hai kết thúc. Vì tiểutrì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); } } public static void Main() { // 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ểutrình thứ hai. thread.Start(); // Dừng cho đến khi tiểutrì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(); } } 1.2 Đồngbộ hóa quá trình thực thi của nhiều tiểutrình V V Bạn cần phối hợp các hoạt động của nhiều tiểutrì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ểutrì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). 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ểutrì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ểutrì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ểutrì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ế đồngbộ hóa thông dụng nhất là lớp Monitor. Lớp này cho phép một tiểutrì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ểutrì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ểutrình này sẽ giải phóng chốt để tiểutrì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ểutrì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ểutrì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ểutrình chủ giải phóng nó bằng ph ương thức tĩnh Monitor.Exit. Khi tiểutrình chủ gọi Exit, một trong các tiểutrình từ hàng sẵn sàng sẽ thu lấy chốt. Nếu tiểutrình chủ không giải phóng chốt bằng Exit, tất cả các tiểutrì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ểutrì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ểutrì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ểutrì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ểutrì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ểutrì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ểutrì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ểutrình, mỗi tiểutrì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ểutrì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ểutrình đang chờ. Lần thứ hai người dùng nhấn Enter, Monitor.PulseAll sẽ được gọi để giải phóng tất cả các tiểutrình đ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() { // Thu lấy chốt trên đối tượng consoleGate. lock (consoleGate) { // Tạo và khởi chạy ba tiểutrì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ểutrì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ểutrình đang chờ. Monitor.Pulse(consoleGate); } // Đánh thức tất cả các tiểutrì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. lock (consoleGate) { // Pulse tất cả các tiểutrì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 để đồngbộ hóa tiểutrì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ểutrì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 để đồngbộ hóa quá trình thực thi của các tiểutrình Phương thức Mô tả WaitAny Tiểutrì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ểutrì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ểutrì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ểutrì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. Lớ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ểutrì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ểutrình nào. Một tiểutrì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ểutrì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 để đồngbộ hóa các tiểutrì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 để đồngbộ hóa việc thực thi của các tiểutrình được-quản-lý và không-được-quản-lý, trong khi lớp Monitor chỉ có thể đồngbộ hóa các tiểutrình được-quản-lý. 1.3 Tạo một đối tượng tập hợp có tính chất an-toàn-về-tiểu-trình V V Bạn muốn nhiều tiểutrì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 để đồngbộ hóa các tiểutrì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ểutrì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ểutrì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ểutrì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ểutrì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ểutrì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 (với kiểu phù hợp) làm đối số và trả về một đối tượng cung cấp một vỏ bọc được-đồng-bộ-hóa (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 để đọcvà ghi tập hợp bảo đảm rằng chỉ một tiểutrì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đồngbộ 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 đồngbộ 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 đồngbộ 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 để đồngbộ 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 đồngbộ hóa cao c ấp hơn, chẳng hạn sử dụng System.Threading.ReaderWriterLock để cho phép nhiều tiểutrìnhđọc nhưng chỉ một tiểutrình ghi. 1.4 Khởi chạy một tiếntrình mới V V Bạn cần thực thi một ứng dụng trong một tiếntrình mới. # # 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ếntrì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ếntrì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-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ếntrì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ếntrình m ới. Ví dụ, hai lệnh dưới đây đều thực thi Notepad trong một tiếntrì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ếntrì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ếntrì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 Thuộc tính Mô tả Arguments Các đối số dùng để truyền cho tiếntrình mới. ErrorDialog Nếu Process.Start không thể khởi chạy tiếntrì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ếntrình hệ thống nằm dưới, tiếntrình này vẫn sẽ tiếp tục chạy. Ví dụ dưới đây sử dụng Process để thực thi Notepad trong mộ t cửa sổ ở trạng thái phóng to và mở một file có tên là C:\Temp\file.txt. Sau khi tạo, ví dụ này sẽ gọi phương thức Process.WaitForExit để dừng tiểutrình đang chạy cho đến khi tiếntrình kết thúc hoặc giá trị time-out (được chỉ định trong phương thức này) hết hiệu lực. . 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. 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