Đa luồng trong C#

Một phần của tài liệu Lập trình C# (Trang 28 - 42)

Đa luồng ( multithreading) dờng nh là một thuật ngữ mới, tuy vậy nó lại rất quen thuộc với chúng ta nhất là trong cuộc sống hằng ngày. Một ví dụ điển hình của đa luồng mà ai cũng có thể nhận ra là hoạt động của các cơ quan trong cơ thể. Cơ thể chúng ta có thể thực hiện cùng một lúc vô số công việc nh hô hấp, tuần hoàn, tiêu hoá … Các hoạt động này diễn ra đồng thời. Chúng ta cũng có thể tìm thấy hình ảnh của đa luồng trong ví dụ khác là hoạt động của một chiếc xe máy. Một chiếc xe máy có thể thực hiện nhiều động tác nh là rẽ, tăng tốc, nháy đèn đồng thời. Trong các phần…

sau, chúng ta sẽ tìm hiểu đa luồng trong thực hiện chơng trình máy tính.

I. Khái niệm đa luồng

Trớc khi hiểu hoàn toàn về luồng, chúng ta sẽ tìm hiểu thế nào hệ điều hành Windows hoạt động nh thế nào trong chế độ đa nhiệm:

I.1. Đa nhiệm ( multitasking )

Trớc hết ta có thể nói Windows là một hệ thống đa nhiệm. Trong chế độ đa nhiệm, hệ điều hành sẽ phân bố thời gian giữa các tiến trình và quyết định tiến trình nào nên đợc chạy kế tiếp khi tiến trình hiện hành kết thúc thời gian đợc chia sẻ trên vi xử lý. Do đó hệ điều hành ngắt tiến trình đều đặn giữa các khoảng thời gian để đa tiến trình kế tiếp trong hàng đợi vào thực hiện vì vậy không có tiến trình nào có độc quyền chiếm CPU tại bất cứ thời điểm của thời gian. Số lợng thời gian đa tới mỗi tiến trình phụ thuộc vào bộ vi xử lý và hệ điều hành. Thời gian xử lý cho mỗi tiến trình rất nhỏ điều này đa ra cảm tởng rằng một tập hợp các tiến trình chạy đồng thời nhng thực tế mỗi tiến trình chạy trong môt số miligiây nào đó sau đó tới tiến trình khác và sự chuyển đổi này xảy ra rất nhanh. Đây là phơng cách mà các Windows 95/98/NT hay ngay cả Unix dùng để quản lý các tiến trình. Tuy nhiên ở các hệ điều hành ban đầu nh Win 3.x và DOS thì nó lại quản lý ở chế độ đơn nhiệm (monotasking). Trong chế độ này mỗi tiến trình có thể điều khiển CPU trong bao lâu mà nó cần, với cách thực hiện này một tiến trình ngăn chặn các tiến trình khác đợc xử lí đồng thời.

I.2. Đa luồng ( multitasking)

Không gian bộ nhớ, trong đó một ứng dụng đợc thực hiện đợc gọi là tiến trình (process). Trong phạm vi một tiến trình thờng có nhiều công việc cần đợc thực hiện. Quá trình thực hiện một công việc đợc gọi là một luồng (tiểu trình). Các công việc trong một tiến trình có thể thực hiện tuần tự, lúc này chỉ có một luồng chạy thực hiện

các công việc, ta gọi đây là chế độ đơn luồng. Các công việc của tiến trình cũng có thể thực hiện đồng thời, lúc này có nhiều luồng thực hiện nhiều công việc.

Nh vậy ta gặp lại hình ảnh đa nhiệm trong đa luồng, chúng có thể phân biệt chúng đơn giản nh sau:

 Đa nhiệm: bao gồm các ứng dụng khác nhau chạy đồng thời dới sừ điều khiển của hệ điều hành, chúng chia sẻ CPU và bộ nhớ.

 Đa luồng: chỉ tồn tại trong một ứng dụng, bao gồm các công việc của một ứng dụng đợc chạy đồng thời, chúng chia sẻ tài nguyên CPU và bộ nhớ mà hệ điều hành phân cho ứng dụng đó, chúng đợc điều khiển bởi một mô đun trong ứng dụng.

Trong một ứng dụng Window có các loại luồng sau:

 Luồng đơn ( Single Threading ): chỉ có một luồng trong ứng dụng và nó phải làm tất cả các công việc. Luồng đó có tất cả các không gian phân phối cho process. Lúc này ta có thể đồng nhất luồng với ứng dụng.

 Luồng Apartment ( ở đây lấy hình ảnh ứng dụng là khu nhà, mỗi luồng là mộ căn hộ): có nhiều luồng trong ứng dụng. ứng dụng sẽ định khi nào và bao lâu cho một luồng nên thực hiện. Mỗi luồng có gán cho một không gian riêng trong phạm vi không gian cho ứng dụng và các luồng khác không chia sẻ các nguồn tài nguyên đó.

 Luồng tự do ( Free Threading ): có nhiều luồng trong một ứng dụng và những luồng này chia sẻ nguồn tài nguyên chung. Các luồng khác nhau có thể gọi cùng một phơng thức và thuộc tính tại một thời điểm. Mô hình Apartment thì hiệu quả hơn mô hình single threading bởi vì công việc đợc phân chia giữa nhiều đối tợng trong khi mô hình free thì nhanh nhất và hữu hiệu nhất. Nhng chế độ free ẩn chứa nhiều rủi ro bởi vì sự chia sẻ tài nguyên giữa nhiều đối tợng, ngời lập trình cần phải chú ý nhiều đến sự đồng bộ và tránh các xung đột xảy ra.

II. Đa luồng trong C#

Trong phần trên ta thấy đợc phần nào hệ điều hành quản lý các ứng dụng cùng chạy đồng thời.Từ thực tế ta cũng có những ứng dụng tính toán phức tạp cần nhiều thời gian thực hiện hay các ứng dụng cần xử lý song song (paralell), hay ứng dụng cần lấy dữ liệu từ xa đồng thời hiển thị dữ liệu trên màn hình cho ta thấy đa luồng đóng vai trò quan trọng trong cuộc sống của chúng ta. Tuy nhiên không phải hầu hết các ngôn ngữ lập trình đều cho phép lập trình viên chỉ định các hành động diễn ra đồng thời. Thay vào đó ngôn ngữ lập trình cung cấp cho chúng ta một tập cấu trúc đơn giản để cho một chơng trình hoạt động tại một thời điểm và chỉ tiếp tục hành động tiếp theo khi hành động thứ nhất đã kết thúc. Kiểu hoạt động đồng thời mà các máy tính ngày nay thờng cài đặt là tính năng hoạt động đồng thời nguyên thủy của hệ điều hành vốn chỉ sử dụng bởi các lập trình viên hệ thống cực kỳ kinh nghiệm. Ngôn ngữ C# cùng với các ngôn ngữ khác trong bộ .NET đã biến các tính năng nguyên thủy đồng thời trở nên sẵn sàng

cho ngời lập trình xây dựng các ứng dụng cá nhân.Lập trình viên C# có thể xây dựng ứng dụng có nhiều luồng thi hành mà mỗi luồng chỉ định rõ một phần công việc trong số nhiều phần việc của một chơng trình và có thể thi hành đồng thời với các luồng khác.Một ứng dụng cụ thể của kỹ thuật đa luồng là khả năng tự gom rác của C# trong đó C# cung cấp một luồng gom rác tự động chạy thu hồi phần bộ nhớ đã đợc phân phối cấp phát mà chơng trình chính không dùng nữa.Tuy nhiên bên cạnh tạo ra các ra các ứng dụng đa luồng có thể cải thiện đợc hiệu suất của ứng dụng tuy nhiên việc thêm nhiều tiểu trình có thể làm tăng độ phức tạp của ứng dụng của chúng ta cũng nh làm làm chậm việc tính toán (do vi xử lý phải chuyển đổi liên tục từ luồng này sang thực hiện luồng tiếp theo sau đó lại quay lại thực hiện luồng đó) và làm cho việc bảo trì và gỡ rối tăng lên.

II.1. Cấu trúc các lớp điều khiển luồng của C#

Trong ngôn ngữ lập trình C# không gian tên System.Threading cung cấp các lớp, các giao diện cho việc lập trình đa luồng. Trong không gian tên này bao gồm lớp ThreadPool quản lý các nhóm các luồng, lớp Timer cho phép một đại diện chuyển giao đợc gọi sau một lợng thời gian chỉ định và lớp Mutex cho sự đồng bộ giữa các luồng với nhau. Ngoài ra System.Threading cũng cung cấp các lớp cho việc định mức u tiên của các luồng, chờ đợi thông báo…

Sau đây ta có một sơ đồ cấu trúc của không gian tên System.Threading:

Classes

Class Mô tả

AutoResetEvent Thông báo một or nhiều luồng đang chờ đợi cho một sự kiện đã xảy ra và tự động khởi động.

InterLocked Cung cấp hoạt động tối thiểu cho các biến đợc chia sẻ bởi nhiều luồng

ManualResetEvent Xảy ra khi thông báo một hoặc nhiều luồng có một sự kiện vừa xảy ra

Monitor Cung cấp một cơ chế cái đồng bộ hóa truy nhập các đối tợng

RegisteredWaitHandle Diễn tả một đăng ký cái mà đã đợc đăng ký khi goi đối tợng RegisterWaitForSingleObject. (adsbygoogle = window.adsbygoogle || []).push({});

SynchronizationLockExcept ion

Ngoại lệ đợc đa ra khi một phơng thức đồng bộ hóa đợc đa ra từ một khối mã đóng không đồng bộ hóa

Thread Tạo và điều khiển luồng, và nhận các trạng thái của nó

ThreadAbortException Một ngoại lệ cái mà đợc đa ra khi một lời gọi đợc chế tới phơng thức Abort

ThreadExceptionEventArgs Cung cấp dữ liệu cho một sự kiện ThreadException

ThreadInterruptedException Một ngoại lệ đợc đa ra khi một luồng bị ngắt trong khi nó ở trong trạng thái chờ đợi.

ThreadPool Cung cấp một sự dùng chung của các luồng có thể sử dụng xử lý không đồng bộ vào ra, chờ đợi ,xử lý thời gian, xử lý chuyển công việc. ThreadStateException Ngoại lệ đợc đa ra khi luồng trong một trạng

thái không hợp cho lời gọi phơng thức.

Timeout Chứa đựng một hằng chỉ định cho một khoảng thời gian nhất định.

Timer Chỉ định một cơ chế cho thực hiện một phơng thức giữa khoảng thời gian chỉ định,

Structure:

Structure Mô tả

LockCookie Định nghĩa một lock thực hiện các văn phạm singer_writer hoặc multiple_writer

NativeOverlapped Cung cấp một cấu trúc rõ ràng cái mà hiện từ code không quản lý và cái đó có câứ trúc giống cấu trúc Win32 overlapped với các trờng dự trữ thêm vào tai cuối.

Delegates

Delegate Mô tả

ThreadStart Mô tả phơng thức cái mà bắt đầu với Thread class ThreadExceptionEventHandl

er

Mô tả phơng thức cái mà điều khiển sự kiện ThreadException của ứng dụng.

IOCompletionCallback Nhận mã lỗi ,số các bytes ,và các giá trị trùng khi hoạt động vào ra hoàn thành trên threadpool

TimerCallback Mô tả phơng thức cái mà điều khiển t/thái của Timer

Enumeration:

Enumeration Mô tả

ApartmentState Chỉ định trạng apartment của một Thread ThreadPriority Chỉ định thời gian biểu cho Thread

ThreadState Chỉ định trạng thái của Thread

II.2.1. Tạo luồng ( create thread )

Để tạo một luồng với lớp Thread, trớc hết ta cần có một phơng thức làm việc nh một đại diện chuyển giao luồng. Trong .NET dùng đại diện chuyển giao để định nghĩa một phơng thức mà luồng mới sẽ dùng. Giả sử rằng chúng ta có một phơng thức MyThread, thì phơng thức này đóng vai trò đại diện, định nghĩa cho luồng. Sau đó tạo một đối tợng của lớp Thread và constructor cho một Thread sẽ là một thông số cho đại diện ThreadStart. Nh vậy đại diện ThreadState định nghĩa các phơng thức mà luồng mới tạo thành sẽ thi hành.

Vi dụ:

ThreadStart mt=new ThreadStart(MyThread); //Doan tren tao ra mot dai dien chuyen giao

//Tao mot đối tợng của lớp Thread lây tham sô là đai diện chuyển giao. Thread myluong=new Thread(mt); (adsbygoogle = window.adsbygoogle || []).push({});

Tuy nhiên đến bớc trên luồng vẫn cha thực hiện đợc và vẫn ở trạng thái Unstarted và khi ta gọi phơng thức Start của lớp Thread thì hệ điều hành sẽ thay đổi trạng thái của luồng tới trạng thái ThreadState.Running. Khi Start đợc gọi thì thực hiên dòng đầu tiên của phơng thức ( ở vi dụ này MyThread ) tham chiếu bởi đại diện chuyển giao Delegate. Khi phơng thức Start đợc gọi thì nó có thể gây ra một số ngoại lệ mà ta cần chú ý :

ThreadStateException Luông đã bắt rồi.

SecurityException Lời gọi không có cho phép thực hiện

OutOfMemoryException Không đủ bộ nhớ thực hiện luồng NullReferenceException Lời gọi tham chiếu một luồng mà

null

ở đây ta cũng chú ý là một khi luồng đã ngắt thì không thể có lời gọi Start lần nữa. Khi phong thức Start khởi đầu luồng này, phơng thức Start sẽ trả điều khiển cho luồng gọi ngay lập tức. Khi đó luồng gọi sẽ thi hành đồng thời luồng vừa phát động. Để biết đợc trạng thái của một luồng ta có thể sử dụng thuộc tính

Thread.ThreadSate hay thuộc tính Thread.isAlive public ThreadState ThreadState {get;} (**) public bool IsAlive {get;}) (*)

(*) - Thuộc tính này có giá trị là true khi mà luồng đã đợc gọi và cha chết ( tức là ph- ơng thức stop của nó cha đợc gọi và phơng thức kiểm soát run của nó cha hoàn tất nhiệm vụ) ngợc lại nó có giá trị false khi luồng đã kết thúc .

(**)- Giá trị của thuộc tính này trả về là trạng thái hiện hành của luồn và khởi đầu với giá trị unstarted.

Khi tạo một luồng ta có thể gán tên cho luồng đó để nhấn dạng tên luồng đang chạy bằng cách sử dụng thuộc tính Thread.Name để gán thuộc tên cho luồng. Điều cần lu ý khi sử dụng thuộc tính này là đây là thuộc tính chỉ ghi và một luồng chỉ đợc dặt tên cho một lần duy nhất mà thôi nếu không sẽ gây ra một lỗi ngoại lệ InvalidOperationException (thiết đặt một yêu cầu mà tên đó đã đợc đặt rồi).

II.2.2. Nhập luồng ( join thread )

Khi ta cần dừng xử lý một luồng và chờ đợi cho luồng thứ hai kết thúc gọi là luồng thứ nhất gia nhập luồng thứ hai hay nói cách khác là gắn đầu luồng một vào đuôi luồng thứ hai. C# cung cấp cho chúng ta ba phơng thức :

public void Join() ;(*) public bool Join(int);(**)

public bool Join(TimeSpan);(***)

(*) Khi dùng phơng thức này thì nó sẽ đóng luồng cho đến khi luồng thứ hai bị ngắt. Tuy nhiên sự chờ đợi không hạn định này có thể gây ra vấn đề nghiêm trọng đó là bế tắc hoàn toàn (deadlock) và trì hoãn vô hạn (infinitive postponement). Phơng thức này tung ra hai lỗi ngoại lệ là ThreadSateException khi mà lời gọi cố gắng gia nhập luồng trong khi đang ở trạng thai ThreadState.Unstarted và ngoại lệ thứ hai xảy ra là ThreadInterruptException tức là luồng bị ngắt trong khi đang chờ đợi.

(**) Khi dùng phơng thức này thì luồng sẽ bị đóng cho đến khi luồng thứ hai ngắt hay thời gian chỉ định hết. Phơng thức này trả lại giá trị true nếu luồng hai kết thúc hoặc trả lại false khi hết thời gian mà luồng hai cha kết thúc. Phơng thức này cũng sinh ra hai ngoai lệ là ThreadSateException tức luồng cha bắt đầu hoặc do thời gian là âm ArgumentOutOfRangeException.

Khi thực hiện các phơng thức này thì chúng đều làm thay đổi trạng thái của luồng gọi tới ThreadState.WaitSleepJoin và ta cần chú ý là không gọi phơng thức này khi luồng đang ở trạng thái ThreadState.Unstarted.

Ví dụ:

using System;

using System.Thread; class test

{ public static void thuyet() {

Console.WriteLine(“Trangthai:”+Second.CurrentState.ThreadSat e);

/*sử dụng thuộc tính CurrentThread để trả về trạng thái của luồng của luồng hiện thời */

}

public static void Main()

{ Console.WriteLine(“Luong chinh”);

ThreadStart my=new ThreadStart(thuyet); Thread second=new Thread(my);

second.Start(); second.Join();

Console.WriteLine(“Ket thuc:”+second.ThreadState); } (adsbygoogle = window.adsbygoogle || []).push({});

}

Trong chơng trình trên khi thực hiện nếu ta bỏ đi dòng lệnh second.Join() thì ch- ơng trình cho ta đầu ra :

Luong chinh. Ketthuc:unstarted Trangthai:Running.

Tuy nhiên khi ta cho dòng lệnh vào thì khi đó luồng chơng trình chính sẽ đi vào trạng thái Thread.WaitSleepJoin và chờ cho luồng vừa tạo ra kết thúc mới tiếp tục làm tiếp do đó đầu ra của chơng trình trên:

Luong chinh. Trangthai:Running Ketthuc:unstarted

II.2.3. Dừng một luồng

Để dùng một luồng trong C# ta có thể sử dụng phơng thức Thread.Susppend [ public void suspend();]

Tuy nhiên khi ta gọi phơng thức này, hệ thống không thực hiện ngay hành động này ngay lập tức. Thay vào đó, nó ghi nhận một luồng đình chỉ vừa yêu cầu và chờ đợi cho đến khi luồng đạt đến một điểm an toàn (safe point) trớc khi thực sự đình chỉ luồng đó hoạt động. Một điểm an toàn cho một luồng là một điểm an toàn cho gom rác. Ph- ơng thức này chỉ sẽ không hiệu quả khi chúng ta gọi phơng thức này khi mà luồng đã bị dừng và để khôi phục luồng bị đình chỉ ta sử dụng phơng thức Thread.Resume() để khôi phục lại hoạt động của luồng.

Lớp Thread còn đa ra cho ta một phơng thức Thread.Sleep cũng dùng cho mục đích nh trên. Phơng thức này lấy tham số là thời gian chỉ định luồng sẽ dừng trong bao lâu sau đó khôi phục thực hiện. Đây không phải là sự định thời khóa biểu cho luồng thực hiện và khi thực hiện thì phơng thức này đa luồng vào trạng thái WaitSleepJoin. Để gọi một luồng ra khỏi trạng thái WaitSleepJoin chúng ta có thể sử dụng phơng thức Thread.Interrupt() để làm luồng trở lại hoạt động.

II.2.4. Hủy một luồng

Phơng thức Thread .Abort để làm ngng hẳn một luồng đang hoạt động, có hai phơng thức cho hoạt động này: một là không đối số hai là có đối số tuy nhiên chúng ta

Một phần của tài liệu Lập trình C# (Trang 28 - 42)