1. Trang chủ >
  2. Công Nghệ Thông Tin >
  3. Kỹ thuật lập trình >

8 Đồng bộ hóa quá trình thực thi của nhiều tiểu trình

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (4.45 MB, 563 trang )


118

Chương 4: Tiểu trình, tiến trình, và sự đồng bộ



• 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ứ

hai người dùng nhấn Enter, Monitor.PulseAll sẽ được gọi để giải phóng tất cả các tiểu trình

đang chờ còn lại.

using System;

using System.Threading;

public class ThreadSyncExample {

private static object consoleGate = new Object();



119

Chương 4: Tiểu trình, tiến trình, và sự đồng bộ

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ể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.");



120

Chương 4: Tiểu trình, tiến trình, và sự đồng bộ

Console.ReadLine();

// Thu lấy chốt trên đối tượng consoleGate.

lock (consoleGate) {

// 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

Phương thức



Mô tả



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ị timeout.



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.

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ó



121

Chương 4: Tiểu trình, tiến trình, và sự đồng bộ



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ượ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 (threadsafe).



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

(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



122

Chương 4: Tiểu trình, tiến trình, và sự đồng bộ



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

Bạn cần thực thi một ứng dụng trong một tiến trì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ế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-



Xem Thêm
Tải bản đầy đủ (.doc) (563 trang)

×