TÀI LIỆU

Thread và sự đồng bộ

Science and Technology

Thread là một process “nhẹ cân” cung cấp khả năng multitasking trong một ứng dụng. Vùng tên System.Threading cung cấp nhiều lớp và giao diện để hỗ trợ lập trình nhiều thread.

Thread

Thread thường được tạo ra khi bạn muốn làm đồng thời 2 việc trong cùng một thời điểm. Giả sử ứng dụng của bạn đang tiến hành đọc vào bộ nhớ một tập tin có kích thước khoảng 500MB, trong lúc đang đọc thì dĩ nhiên ứng dụng không thể đáp ứng yêu cầu xử lý giao diện. Giả sử người dùng muốn ngưng giữa chừng, không cho ứng dụng đọc tiếp tập tin lớn đó nữa, do đó cần một thread khác để xử lý giao diện, lúc này khi người dùng ấn nút Stop thì ứng dụng đáp ứng được yêu cầu trong khi thread ban đầu vẫn đang đọc tập tin.

Tạo Thread

Cách đơn giản nhất là tạo một thể hiện của lớp Thread. Contructor của lớp Thread nhận một tham số kiểu delegate. CLR cung cấp lớp delegate ThreadStart nhằm mục đích chỉ đến phương thức mà bạn muốn thread mới thực thi. Khai báo delegate ThreadStart như sau:

public delegate void ThreadStart( );

Phương thức mà bạn muốn gán vào delegate phải không chứa tham số và phải trả về kiểu void. Sau đây là ví dụ:

Thread myThread = new Thread( new ThreadStart(myFunc) );

myFunc phải là phương thức không tham số và trả về kiểu void.

Xin lưu ý là đối tượng Thread mới tạo sẽ không tự thực thi (execute), để đối tượng thực thi, bạn càn gọi phương thức Start() của nó.

Thread t1 = new Thread( new ThreadStart(Incrementer) ); Thread t2 = new Thread( new ThreadStart(Decrementer) ); t1.Start( ); t2.Start( );

Thread sẽ chấm dứt khi hàm mà nó thực thi trở về (return).

Gia nhập Thread

Hiện tượng thread A ngưng chạy và chờ cho thread B chạy xong được gọi là thread A gia nhập thread B.

Để thread 1 gia nhập thread 2:

t2.Join( );

Nếu câu lệnh trên được thực hiện bởi thread 1, thread 1 sẽ dừng lại và chờ cho đến khi thread kết thúc.

Treo thread lại (suspend thread)

Nếu bạn muốn treo thread đang thực thi lại một khoảng thời gian thì bạn sử dụng hàm Sleep() của đối tượng Thread. Ví dụ để thread ngưng khoảng 1 giây:

Thread.Sleep(1000);

Câu lệnh trên báo cho bộ điều phối thread (của hệ điều hành) biết bạn không muốn bộ điều phối thread phân phối thời gian CPU cho thread thực thi câu lệnh trên trong thời gian 1 giây.

Giết một Thread (Kill thread)

Thông thường thread sẽ chấm dứt khi hàm mà nó thực thi trở về. Tuy nhiên bạn có thể yêu cầu một thread “tự tử” bằng cách gọi hàm Interrupt() của nó. Điều này sẽ làm cho exception ThreadInterruptedException được ném ra. Thread bị yêu cầu “tự tử” có thể bắt exception này để tiến hành dọn dẹp tài nguyên.

catch (ThreadInterruptedException) { Console.WriteLine("[{0}] Interrupted! Cleaning up...", Thread.CurrentThread.Name); }

Đồng bộ hóa (Synchronization)

Khi bạn cần bảo vệ một tài nguyên, trong một lúc chỉ cho phép một thread thay đổi hoặc sử dụng tài nguyên đó, bạn cần đồng bộ hóa.

Đồng bộ hóa được cung cấp bởi một khóa trên đối tượng đó, khóa đó sẽ ngăn cản thread thứ 2 truy cập vào đối tượng nếu thread thứ nhất chưa trả quyền truy cập đối tượng.

Sau đây là ví dụ cần sự đồng bộ hóa. Giả sử 2 thread sẽ tiến hành tăng tuần tự 1 đơn vị một biến tên là counter.

int counter = 0; Hàm làm thay đổi giá trị của Counter: public void Incrementer( ) { try { while (counter < 1000) { int temp = counter; temp++; // increment // simulate some work in this method Thread.Sleep(1); // assign the Incremented value // to the counter variable // and display the results counter = temp; Console.WriteLine("Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, counter); } }

Vấn đề ở chỗ thread 1 đọc giá trị counter vào biến tạm rồi tăng giá trị biến tạm, trước khi thread 1 ghi giá trị mới từ biến tạm trở lại counter thì thread 2 lại đọc giá trị counter ra biến tạm của thread 2. Sau khi thread 1 ghi giá trị vừa tăng 1 đơn vị trở lại counter thì thread 2 lại ghi trở lại counter giá trị mới bằng với giá trị mà thread 1 vừa ghi. Như vậy sau 2 lần truy cập giá trị của biến counter chỉ tăng 1 đơn vị trong khi yêu cầu là phải tăng 2 đơn vị.

Sử dụng Interlocked

CLR cung cấp một số cơ chế đồng bộ từ cơ chế đơn giản Critical Section (gọi là Locks trong .NET) đến phức tạp như Monitor.

Tăng và giảm giá trị làm một nhu cầu phổ biến, do đó C# cung cấp một lớp đặc biệt Interlocked nhằm đáp ứng nhu cầu trên. Interlocked có 2 phương thức Increment() và Decrement() nhằm tăng và giảm giá trị trong sự bảo vệ của cơ chế đồng bộ. Ví dụ ở phần trước có thể sửa lại như sau:

public void Incrementer( ) { try { while (counter < 1000) { Interlocked.Increment(ref counter); // simulate some work in this method Thread.Sleep(1); // assign the decremented value // and display the results Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, counter); } } }

Khối catch và finally không thay đổi so với ví dụ trước.

Sử dụng Locks

Lock đánh dấu một đoạn mã “gay cấn” (critical section) trong chương trình của bạn, cung cấp cơ chế đồng bộ cho khối mã mà lock có hiệu lực.

C# cung cấp sự hỗ trợ cho lock bằng từ chốt (keyword) lock. Lock được gỡ bỏ khi hết khối lệnh.

public void Incrementer( ) { try { while (counter < 1000) { lock (this) { // lock bắt đầu có hiệu lực int temp = counter; temp ++; Thread.Sleep(1); counter = temp; } // lock hết hiệu lực -> bị gỡ bỏ // assign the decremented value // and display the results Console.WriteLine( "Thread {0}. Incrementer: {1}", Thread.CurrentThread.Name, counter); } }

Khối catch và finally không thay đổi so với ví dụ trước.

Sử dụng Monitor

Để có thể đồng bộ hóa phức tạp hơn cho tài nguyên, bạn cần sử dụng monitor. Một monitor cho bạn khả năng quyết định khi nào thì bắt đầu, khi nào thì kết thúc đồng bộ và khả năng chờ đợi một khối mã nào đó của chương trình “tự do”.

Khi cần bắt đầu đồng bộ hóa, trao đối tượng cần đồng bộ cho hàm sau:

Monitor.Enter(đối tượng X);

Nếu monitor không sẵn dùng (unavailable), đối tượng bảo vệ bởi monitor đang được sử dụng. Bạn có thể làm việc khác trong khi chờ đợi monitor sẵn dùng (available) hoặc treo thread lại cho đến khi có monitor (bằng cách gọi hàm Wait())

Ví dụ bạn đang download và in một bài báo từ Web. Để hiệu quả bạn cần tiến hành in sau hậu trường (background), tuy nhiên bạn cần chắc chắn rằng 10 trang đã được download trước khi bạn tiến hành in.

Thread in ấn sẽ chờ đợi cho đến khi thread download báo hiệu rằng số lượng trang download đã đủ. Bạn không muốn gia nhập (join) với thread download vì số lượng trang có thể lên đến vài trăm. Bạn muốn chờ cho đến khi ít nhất 10 trang đã được download.

Để giả lập việc này, bạn thiết lập 2 hàm đếm dùng chung 1 biến counter. Một hàm đếm tăng 1 tương ứng với thread download, một hàm đếm giảm 1 tương ứng với thread in ấn.

Trong hàm làm giảm bạn gọi phương thức Enter(), sau đó kiểm tra giá trị counter, nếu < 5 thì gọi hàm Wait()

if (counter < 5) { Monitor.Wait(this); }

Lời gọi Wait() giải phóng monitor nhưng bạn đã báo cho CLR biết là bạn muốn lấy lại monitor ngay sau khi monitor được tự do một lần nữa. Thread thực thi phương thức Wait() sẽ bị treo lại. Các thread đang treo vì chờ đợi monitor sẽ tiếp tục chạy khi thread đang thực thi gọi hàm Pulse().

Monitor.Pulse(this);

Pulse() báo hiệu cho CLR rằng có sự thay đổi trong trạng thái monitor có thể dẫn đến việc giải phóng (tiếp tục chạy) một thread đang trong tình trạng chờ đợi.

Khi thread hoàn tất việc sử dụng monitor, nó gọi hàm Exit() để trả monitor.

namespace Programming_CSharp { using System; using System.Threading; class Tester { static void Main( ) { // make an instance of this class Tester t = new Tester( ); // run outside static Main t.DoTest( ); } public void DoTest( ) { // create an array of unnamed threads Thread[] myThreads = {new Thread( new ThreadStart(Decrementer) ), new Thread( new ThreadStart(Incrementer) ) }; // start each thread int ctr = 1; foreach (Thread myThread in myThreads) { myThread.IsBackground=true; myThread.Start( ); myThread.Name = "Thread" + ctr.ToString( ); ctr++; Console.WriteLine("Started thread {0}",myThread.Name); Thread.Sleep(50); } // wait for all threads to end before continuing foreach (Thread myThread in myThreads) { myThread.Join( ); } // after all threads end, print a message Console.WriteLine("All my threads are done."); } void Decrementer( ) { try { // synchronize this area of code Monitor.Enter(this); // if counter is not yet 10 // then free the monitor to other waiting // threads, but wait in line for your turn if (counter < 10) { Console.WriteLine( "[{0}] In Decrementer. Counter: {1}. Gotta Wait!", Thread.CurrentThread.Name, counter); Monitor.Wait(this); } while (counter > 0) { long temp = counter; temp--; Thread.Sleep(1); counter = temp; Console.WriteLine("[{0}] In Decrementer. Counter: {1}.", Thread.CurrentThread.Name, counter); } } finally { Monitor.Exit(this); } } void Incrementer( ) { try { Monitor.Enter(this); while (counter < 10) { long temp = counter; temp++; Thread.Sleep(1); counter = temp; Console.WriteLine("[{0}] In Incrementer. Counter: {1}", Thread.CurrentThread.Name, counter); } // I'm done incrementing for now, let another // thread have the Monitor Monitor.Pulse(this); } finally { Console.WriteLine("[{0}] Exiting...", Thread.CurrentThread.Name); Monitor.Exit(this); } } private long counter = 0; } }

Kết quả:

Started thread Thread1 [Thread1] In Decrementer. Counter: 0. Gotta Wait! Started thread Thread2 [Thread2] In Incrementer. Counter: 1 [Thread2] In Incrementer. Counter: 2 [Thread2] In Incrementer. Counter: 3 [Thread2] In Incrementer. Counter: 4 [Thread2] In Incrementer. Counter: 5 [Thread2] In Incrementer. Counter: 6 [Thread2] In Incrementer. Counter: 7 [Thread2] In Incrementer. Counter: 8 [Thread2] In Incrementer. Counter: 9 [Thread2] In Incrementer. Counter: 10 [Thread2] Exiting... [Thread1] In Decrementer. Counter: 9. [Thread1] In Decrementer. Counter: 8. [Thread1] In Decrementer. Counter: 7. [Thread1] In Decrementer. Counter: 6. [Thread1] In Decrementer. Counter: 5. [Thread1] In Decrementer. Counter: 4. [Thread1] In Decrementer. Counter: 3. [Thread1] In Decrementer. Counter: 2. [Thread1] In Decrementer. Counter: 1. [Thread1] In Decrementer. Counter: 0. All my threads are done.

Race condition và DeadLock

Đồng bộ hóa thread khá rắc rối trong những chương trình phức tạp. Bạn cần phải cẩn thận kiểm tra và giải quyết các vấn đề liên quan đến đồng bộ hóa thread: race condition và deadlock

Race condition

Một điều kiện tranh đua xảy ra khi sự đúng đắn của ứng dụng phụ thuộc vào thứ tự hoàn thành không kiểm soát được của 2 thread độc lập với nhau.

Giả sử bạn có 2 thread. Thread 1 tiến hành mở tập tin, thread 2 tiến hành ghi lên cùng tập tin đó. Điều quan trọng là bạn cần phải điều khiển thread 2 sao cho nó chỉ tiến hành công việc sau khi thread 1 đã tiến hành xong. Nếu không, thread 1 sẽ không mở được tập tin vì tập tin đó đã bị thread 2 mở để ghi. Kết quả là chương trình sẽ ném ra exception hoặc tệ hơn nữa là crash.

Để giải quyết vấn đề trong ví dụ trên, bạn có thể tiến hành join thread 2 với thread 1 hoặc thiết lập monitor.

Deadlock

Giả sử thread A đã nắm monitor của tài nguyên X và đang chờ monitor của tài nguyên Y. Trong khi đó thì thread B lại nắm monitor của tài nguyên Y và chờ monitor của tài nguyên X. 2 thread cứ chờ đợi lẫn nhau mà không thread nào có thể thoát ra khỏi tình trạng chờ đợi. Tình trạng trên gọi là deadlock.

Trong một chương trình nhiều thread, deadlock rất khó phát hiện và gỡ lỗi. Một hướng dẫn để tránh deadlock đó là giải phóng tất cả lock đang sở hữu nếu tất cả các lock cần nhận không thể nhận hết được. Một hướng dẫn khác đó là giữ lock càng ít càng tốt.