11 Chủ đề trong C # - Phần 2: Tạo và bắt đầu chuỗi

(Post 05/10/2007) Các luồng được tạo bằng cách sử dụng phương thức khởi tạo của lớp Thread, truyền vào một ủy nhiệm ThreadStart – chỉ ra phương thức bắt đầu thực thi. Đây là cách đại biểu ThreadStart được xác định:

đại biểu công cộng void ThreadStart ();

Gọi Bắt đầu trên chuỗi sau đó đặt nó chạy. Luồng tiếp tục cho đến khi phương thức của nó trả về, lúc này luồng kết thúc. Đây là một ví dụ, sử dụng cú pháp C # mở rộng để tạo đại biểu TheadStart :

class ThreadTest {
static void Main () {
Thread t = new Thread (new ThreadStart (Go));
t.Start (); // Chạy Go () trên luồng mới.
Đi(); // Chạy đồng thời Go () trong luồng chính.
}
static void Go () {Console.WriteLine (“xin chào!”); }

Trong ví dụ này, luồng t thực hiện Go () – at (nhiều) cùng lúc luồng chính gọi Go () . Kết quả là hai hellos gần như ngay lập tức :

Một chuỗi có thể được tạo thuận tiện hơn bằng cách sử dụng cú pháp phím tắt của C # để khởi tạo các đại biểu:

static void Main () {
Thread t = new Thread (Go); // Không cần sử dụng ThreadStart
t.Start ();

}
static void Go () {…}

Trong trường hợp này, một đại biểu ThreadStart được trình biên dịch suy ra tự động. Một phím tắt khác là sử dụng một phương thức ẩn danh để bắt đầu chuỗi:

static void Main () {
Thread t = new Thread (Delegate () {Console.WriteLine (“Xin chào!”);});
t.Start ();
}

Một luồng có thuộc tính IsAlive trả về true sau khi phương thức Start () của nó được gọi, cho đến khi luồng kết thúc.

Một chuỗi, sau khi kết thúc, không thể bắt đầu lại.

Truyền dữ liệu đến ThreadStart

Giả sử, trong ví dụ trên, chúng tôi muốn phân biệt tốt hơn đầu ra từ mỗi luồng, có lẽ bằng cách để một trong các luồng viết bằng chữ hoa. Chúng tôi có thể đạt được điều này bằng cách chuyển một cờ cho phương thức Go : nhưng sau đó chúng tôi không thể sử dụng đại biểu ThreadStart vì nó không chấp nhận các đối số. May mắn thay, khung công tác .NET xác định một phiên bản khác của ủy nhiệm có tên là ParameterizedThreadStart , chấp nhận một đối số đối tượng như sau:

đại biểu công cộng void ParameterizedThreadStart (đối tượng obj);

Ví dụ trước trông giống như sau:

class ThreadTest {
static void Main () {
Thread t = new Thread (Go);
t.Start (true); // == Go (true)
Go (false);
}
static void Go (object upperCase) {
bool upper = (bool) upperCase;
Console.WriteLine (upper? “HELLO!”: “Xin chào!”);

}

Trong ví dụ này, trình biên dịch tự động suy ra đại biểu ParameterizedThreadStart vì phương thức Go chấp nhận một đối số đối tượng. Chúng tôi cũng có thể đã viết:

Thread t = new Thread (new ParameterizedThreadStart (Go));
t.Start (true);

Một đặc điểm của việc sử dụng ParameterizedThreadStart là chúng ta phải ép kiểu đối tượng sang kiểu mong muốn (trong trường hợp này là bool ) trước khi sử dụng. Ngoài ra, chỉ có một phiên bản đối số duy nhất của đại biểu này.

Một phương pháp thay thế là sử dụng một phương thức ẩn danh để gọi một phương thức thông thường như sau:

static void Main () {
Thread t = new Thread (Delegate () {WriteText (“Xin chào”);});
t.Start ();
}
static void WriteText (string text) {Console.WriteLine (text); }

Ưu điểm là phương thức đích (trong trường hợp này là WriteText ) có thể chấp nhận bất kỳ số lượng đối số nào và không cần ép kiểu . Tuy nhiên, người ta phải tính đến ngữ nghĩa biến bên ngoài của các phương thức ẩn danh, như rõ ràng trong ví dụ sau:

static void Main () {
string text = “Trước”;
Thread t = new Thread (ủy nhiệm () {WriteText (text);});
text = “Sau”;
t.Start ();
}
static void WriteText (string text) {Console.WriteLine (text); }

Các phương thức ẩn danh mở ra khả năng kỳ cục về tương tác không mong muốn thông qua các biến bên ngoài nếu chúng được sửa đổi bởi một trong hai bên sau khi bắt đầu chuỗi. Tương tác có chủ đích (thường thông qua các trường) thường được coi là quá đủ! Các biến bên ngoài được xử lý tốt nhất là chỉ sẵn sàng khi quá trình thực thi luồng đã bắt đầu – trừ khi một người sẵn sàng triển khai ngữ nghĩa khóa thích hợp ở cả hai phía.

Một hệ thống phổ biến khác để truyền dữ liệu đến một luồng là cung cấp cho Thread một phương thức instance chứ không phải là một phương thức tĩnh. Các thuộc tính của đối tượng thể hiện sau đó có thể cho luồng biết phải làm gì, như trong phần viết lại của ví dụ ban đầu sau đây:

class ThreadTest {
bool upper;

static void Main () {
ThreadTest instance1 = new ThreadTest ();
instance1.upper = true;
Thread t = new Thread (instance1.Go);
t.Start ();
ThreadTest instance2 = new ThreadTest ();
instance2.Go (); // Luồng chính – chạy với upper = false
}

void Go () {Console.WriteLine (upper? “HELLO!”: “Hello!”); }

Đặt tên chủ đề

Một luồng có thể được đặt tên thông qua thuộc tính Name của nó . Điều này mang lại lợi ích lớn trong việc gỡ lỗi: cũng như có thể Console.WriteLine tên của một chuỗi, Microsoft Visual Studio chọn tên của một chuỗi và hiển thị tên đó trong thanh công cụ Debug Location. Tên của một chuỗi có thể được đặt bất kỳ lúc nào – nhưng chỉ một lần – cố gắng thay đổi sau đó nó sẽ tạo ra một ngoại lệ.

Luồng chính của ứng dụng cũng có thể được gán tên – trong ví dụ sau, luồng chính được truy cập thông qua thuộc tính tĩnh CurrentThread :

class ThreadNaming {
static void Main () {
Thread.CurrentThread.Name = “main”;
Thread worker = new Thread (Go);
worker.Name = “worker”;
worker.Start ();
Đi();
}
static void Go () {
Console.WriteLine (“Xin chào từ” + Thread.CurrentThread.Name);
}
}

Chủ đề nền trước và nền

Theo mặc định, các luồng là các luồng nền trước, có nghĩa là chúng giữ cho ứng dụng tồn tại miễn là bất kỳ một trong số chúng đang chạy. C # cũng hỗ trợ các luồng nền, không giữ cho ứng dụng tự hoạt động – chấm dứt ngay sau khi tất cả các luồng nền trước đã kết thúc.

Thay đổi một luồng từ nền trước sang nền không thay đổi mức độ ưu tiên hoặc trạng thái của nó trong bộ lập lịch CPU theo bất kỳ cách nào.

Thuộc tính IsBackground của một luồng kiểm soát trạng thái nền của nó, như trong ví dụ sau:

class PriorityTest {
static void Main (string [] args) {
Thread worker = new Thread (Delegate () {Console.ReadLine ();});
if (args.Length> 0) worker.IsBackground = true;
worker.Start ();
}
}

Nếu chương trình được gọi mà không có đối số, chuỗi công nhân sẽ chạy ở chế độ nền trước mặc định của nó và sẽ đợi trên câu lệnh ReadLine , đợi người dùng nhấn Enter . Trong khi đó, luồng chính thoát ra, nhưng ứng dụng vẫn tiếp tục chạy vì một luồng nền trước vẫn còn sống.

Mặt khác, nếu một đối số được chuyển đến Main () , worker sẽ được gán trạng thái nền và chương trình sẽ thoát gần như ngay lập tức khi luồng chính kết thúc – chấm dứt ReadLine .

Khi một luồng nền kết thúc theo cách này, bất kỳ khối cuối cùng nào cũng bị phá vỡ. Vì việc vượt qua mã cuối cùng thường không được mong muốn, nên thực tế tốt là bạn nên đợi rõ ràng mọi luồng của nhân viên nền hoàn thành trước khi thoát ứng dụng – có thể là có thời gian chờ (điều này đạt được bằng cách gọi Thread.Join ). Nếu vì lý do nào đó mà một luồng công nhân phản bội không bao giờ kết thúc, thì người ta có thể cố gắng hủy bỏ nó , và nếu thất bại, hãy từ bỏ luồng đó, cho phép nó chết theo quy trình (ghi lại câu hỏi hóc búa ở giai đoạn này cũng có ý nghĩa!)

Sau đó, có các luồng công nhân làm luồng nền có thể có lợi, vì lý do đó luôn có thể có tiếng nói cuối cùng khi kết thúc ứng dụng. Cân nhắc phương án thay thế – luồng nền trước sẽ không chết – ngăn ứng dụng thoát. Một luồng nhân viên nền trước bị bỏ rơi đặc biệt nguy hiểm với ứng dụng Windows Forms, vì ứng dụng sẽ thoát ra khi luồng chính kết thúc (ít nhất là với người dùng) nhưng quy trình của nó sẽ vẫn chạy. Trong Trình quản lý Tác vụ Windows, nó sẽ biến mất khỏi tab Ứng dụng, mặc dù tên tệp thi hành của nó vẫn hiển thị trong tab Quy trình. Trừ khi người dùng định vị và kết thúc nhiệm vụ một cách rõ ràng,

Nguyên nhân phổ biến khiến ứng dụng không thoát đúng cách là sự hiện diện của các chủ đề nền trước bị “quên”.

Ưu tiên hàng đầu

Thuộc tính Ưu tiên của một luồng xác định lượng thời gian thực thi mà nó nhận được so với các luồng đang hoạt động khác trong cùng một quy trình, trên quy mô sau:

enum ThreadPeaker {Thấp nhất, Dưới đây Bình thường, Bình thường, Trên Bình thường, Cao nhất}

Điều này chỉ trở nên phù hợp khi nhiều luồng hoạt động đồng thời.

Đặt mức độ ưu tiên của luồng thành cao không có nghĩa là nó có thể thực hiện công việc theo thời gian thực, vì nó vẫn bị giới hạn bởi mức độ ưu tiên xử lý của ứng dụng. Để thực hiện công việc trong thời gian thực, lớp Process trong System.Diagnostics cũng phải được sử dụng để nâng cao mức độ ưu tiên của quy trình như sau (tôi không cho bạn biết cách làm điều này):

Process.GetCurrentProcess (). PriorityClass = ProcessPainstClass.High;

ProcessPinentClass. Cao thực sự thiếu một bậc so với mức ưu tiên quy trình cao nhất: Thời gian thực . Đặt mức độ ưu tiên quy trình của một người thành Thời gian thực sẽ hướng dẫn hệ điều hành rằng bạn không bao giờ muốn quy trình của mình bị ưu tiên. Nếu chương trình của bạn vô tình đi vào một vòng lặp vô hạn, bạn có thể mong đợi ngay cả hệ điều hành cũng bị khóa. Không có gì thiếu nút nguồn sẽ giải cứu bạn! Vì lý do này, Cao thường được coi là mức ưu tiên quy trình có thể sử dụng cao nhất.

Nếu ứng dụng thời gian thực có giao diện người dùng, có thể không mong muốn tăng mức độ ưu tiên của quy trình vì các bản cập nhật màn hình sẽ có quá nhiều thời gian CPU – làm chậm toàn bộ máy tính, đặc biệt nếu giao diện người dùng phức tạp. (Mặc dù tại thời điểm viết bài, chương trình điện thoại Internet Skype không còn làm được điều này nữa, có lẽ vì giao diện người dùng của nó khá đơn giản). Giảm mức độ ưu tiên của luồng chính – cùng với việc tăng mức độ ưu tiên của quy trình – đảm bảo luồng thời gian thực không bị ảnh hưởng bởi các bản vẽ lại màn hình, nhưng không ngăn máy tính chạy chậm lại, bởi vì hệ điều hành vẫn sẽ phân bổ CPU quá mức cho quy trình nói chung. Giải pháp lý tưởng là có giao diện người dùng và công việc thời gian thực trong các quy trình riêng biệt (với các mức độ ưu tiên khác nhau), giao tiếp thông qua Điều khiển từ xa hoặc bộ nhớ dùng chung. Bộ nhớ dùng chung yêu cầu P / Gọi API Win32 (tìm kiếm web CreateFileMapping và MapViewOfFile).

Xử lý ngoại lệ

Mọi khối thử / bắt / cuối cùng trong phạm vi khi luồng được tạo đều không liên quan khi luồng bắt đầu thực thi. Hãy xem xét chương trình sau:

public static void Main () {
try {
new Thread (Go) .Start ();
}
catch (Exception ex) {
// Chúng tôi sẽ không bao giờ đến được đây!
Console.WriteLine (“Ngoại lệ!”);
}

static void Go () {ném null; }
}

Câu lệnh try / catch trong ví dụ này thực sự vô dụng và luồng mới được tạo sẽ bị cản trở bởi một NullReferenceException không được xử lý . Hành vi này có ý nghĩa khi bạn coi một luồng có một đường dẫn thực thi độc lập. Biện pháp khắc phục là các phương thức nhập luồng có các trình xử lý ngoại lệ của riêng chúng:

public static void Main () {
new Thread (Go) .Start ();
}

static void Go () {
try {

ném null; // ngoại lệ này sẽ bị bắt bên dưới

}
catch (Exception ex) {
Thông thường ghi lại ngoại lệ và / hoặc báo hiệu một chuỗi khác
rằng chúng tôi đã gỡ bỏ

}

Từ .NET 2.0 trở đi, một ngoại lệ không được xử lý trên bất kỳ luồng nào sẽ tắt toàn bộ ứng dụng, có nghĩa là bỏ qua ngoại lệ thường không phải là một tùy chọn. Do đó, khối try / catch được yêu cầu trong mọi phương thức nhập luồng – ít nhất là trong các ứng dụng sản xuất – để tránh ứng dụng bị tắt không mong muốn trong trường hợp ngoại lệ không được xử lý. Điều này có thể hơi phức tạp – đặc biệt đối với các lập trình viên Windows Forms, những người thường sử dụng trình xử lý ngoại lệ “toàn cầu”, như sau:

sử dụng Hệ thống;
sử dụng System.Threading;
sử dụng System.Windows.Forms;

static class Program {
static void Main () {
Application.ThreadException + = HandleError;
Application.Run (MainForm mới ());
}

static void HandleError (object sender, ThreadExceptionEventArgs e) {
Log ngoại lệ, sau đó thoát ứng dụng hoặc tiếp tục …
}
}

Sự kiện Application.ThreadException kích hoạt khi một ngoại lệ được ném ra từ mã cuối cùng được gọi là kết quả của thông báo Windows (ví dụ: bàn phím, chuột hoặc thông báo “paint”) – nói ngắn gọn, gần như tất cả mã trong Windows Forms điển hình ứng dụng. Trong khi điều này hoạt động hoàn hảo, nó ru người ta vào một cảm giác an toàn sai lầm – rằng tất cả các ngoại lệ sẽ bị bắt bởi trình xử lý ngoại lệ trung tâm. Các ngoại lệ được ném trên các luồng worker là một ví dụ điển hình về các ngoại lệ không bị Application.ThreadException bắt (mã bên trong phương thức Main là một phương thức khác – bao gồm phương thức khởi tạo của biểu mẫu chính, thực thi trước khi vòng lặp thông báo Windows bắt đầu).

Khuôn khổ .NET cung cấp một sự kiện cấp thấp hơn để xử lý ngoại lệ chung: AppDomain.UnhandledException . Sự kiện này kích hoạt khi có một ngoại lệ chưa được xử lý trong bất kỳ chuỗi nào và trong bất kỳ loại ứng dụng nào (có hoặc không có giao diện người dùng). Tuy nhiên, mặc dù nó cung cấp một cơ chế cuối cùng tốt để ghi nhật ký các ngoại lệ chưa được khai thác, nhưng nó không cung cấp phương tiện ngăn ứng dụng tắt – và không có cách nào để ngăn chặn hộp thoại ngoại lệ không xử lý .NET.

(Sưu tầm)

FPT Aptech – Hệ Thống Đào Tạo Lập Trình Viên Quốc Tế

FPT Aptech trực thuộc Tổ chức Giáo dục FPT có hơn 25 năm kinh nghiệm đào tạo lập trình viên quốc tế tại Việt Nam, và luôn là sự lựa chọn ưu tiên của các sinh viên và nhà tuyển dụng.
0981578920
icons8-exercise-96