(Post 14/12/2007) Gọi Abort trên chủ đề của chính mình là một trường hợp mà Abort hoàn toàn an toàn. Khác là khi bạn có thể chắc chắn luồng bạn đang hủy nằm trong một phần mã cụ thể, thường là nhờ cơ chế đồng bộ hóa như Wait Handle hoặc Monitor.Wait. Một trường hợp thứ ba trong đó việc gọi Abort là an toàn là khi sau đó bạn hủy bỏ miền ứng dụng hoặc quy trình của chuỗi.
- Phần 1: Tổng quan và khái niệm
- Phần 2: Tạo và bắt đầu chủ đề
- Phần 3: Cơ bản về đồng bộ hóa
- Phần 4: Khóa và an toàn chỉ
- Phần 5: Ngắt và hủy bỏ
- Phần 6: Trạng thái luồng
- Phần 7: Xử lý Chờ
- Phần 8: Bối cảnh đồng bộ hóa
- Phần 9: Căn hộ và Hình thức Windows
- Phần 10: BackgroundWorker
- Phần 11: ReaderWriterLock
- Phần 12: Tổng hợp luồng
- Phần 13: Đại biểu không đồng bộ
- Phần 14: Bộ hẹn giờ
- Phần 15: Lưu trữ cục bộ
- Phần 16: Đồng bộ hóa không chặn
- Phần 17: Chờ và Xung (1)
- Phần 17: Chờ và Xung (2)
- Phần 18: Tạm dừng và tiếp tục
Một chuỗi có thể được kết thúc cưỡng bức thông qua phương thức Abort:
class Abort {
static void Main () {
Thread t = new Thread (Delegate () {while (true);}); // Quay mãi mãi
t.Start ();
Thread.Sleep (1000); // Để nó chạy trong giây …
t.Abort (); // sau đó hủy bỏ nó.
}
}
Luồng sau khi bị hủy bỏ sẽ ngay lập tức chuyển sang trạng thái AbortRequested. Nếu sau đó nó kết thúc như mong đợi, nó sẽ chuyển sang trạng thái Stopped. Người gọi có thể đợi điều này xảy ra bằng cách gọi Tham gia:
class Abort {
static void Main () {
Thread t = new Thread (Delegate () {while (true);});
Console.WriteLine (t.ThreadState); //
Chưa khởi động t.Start ();
Thread.Sleep (1000);
Console.WriteLine (t.ThreadState); // Chạy
t.Abort ();
Console.WriteLine (t.ThreadState); // AbortRequested
t.Join ();
Console.WriteLine (t.ThreadState); // Đã dừng
}
}
Huỷ bỏ khiến một ThreadAbortException được ném vào luồng đích, trong hầu hết các trường hợp, ngay tại nơi mà luồng đang thực thi tại thời điểm đó. Luồng bị hủy bỏ có thể chọn xử lý ngoại lệ, nhưng ngoại lệ sau đó sẽ tự động được ném lại vào cuối khối bắt (để giúp đảm bảo luồng, thực sự, kết thúc như mong đợi). Tuy nhiên, có thể ngăn việc tự động ném lại bằng cách gọi Thread.ResetAbort trong khối bắt. Sau đó, luồng sau đó vào lại trạng thái Đang chạy (từ đó nó có thể bị hủy bỏ một lần nữa). Trong ví dụ sau, chuỗi công nhân quay trở lại từ trạng thái chết mỗi khi cố gắng Hủy bỏ:
class Terminator {
static void Main () {
Thread t = new Thread (Work);
t.Start ();
Thread.Sleep (1000); t.Abort ();
Thread.Sleep (1000); t.Abort ();
Thread.Sleep (1000); t.Abort ();
}
static void Work () {
while (true) {
try {while (true); }
catch (ThreadAbortException) {Thread.ResetAbort (); }
Console.WriteLine (“Tôi sẽ không chết!”);
}
}
}
ThreadAbortException được xử lý đặc biệt bởi thời gian chạy, trong đó nó không khiến toàn bộ ứng dụng chấm dứt nếu được xử lý, không giống như tất cả các loại ngoại lệ khác.
Abort sẽ hoạt động trên một chuỗi ở hầu hết mọi trạng thái – đang chạy, bị chặn, bị treo hoặc đã dừng. Tuy nhiên, nếu một chuỗi bị treo bị hủy bỏ, một ThreadStateException sẽ được ném ra – lần này là trên chuỗi đang gọi – và việc hủy bỏ sẽ không bắt đầu cho đến khi chuỗi đó được tiếp tục lại sau đó. Đây là cách hủy bỏ một chuỗi bị treo:
hãy thử {secureThread.Abort (); }
catch (ThreadStateException) {secureThread.Resume (); }
// Bây giờ, Suspements sẽ hủy bỏ.
Các biến chứng với Thread.Abort
Giả sử một chuỗi bị hủy bỏ không gọi ResetAbort, người ta có thể mong đợi nó kết thúc khá nhanh. Nhưng khi nó xảy ra, với một luật sư giỏi, chủ đề có thể vẫn còn trên tử tù trong một thời gian dài! Dưới đây là một số yếu tố có thể giữ cho nó tồn tại ở trạng thái AbortRequested:
- Các hàm tạo lớp tĩnh không bao giờ bị hủy bỏ từng phần (để không có khả năng làm nhiễm độc lớp trong thời gian còn lại của miền ứng dụng)
- Tất cả các khối bắt / cuối cùng đều được tôn trọng và không bao giờ bị hủy bỏ giữa luồng
- Nếu luồng đang thực thi mã không được quản lý khi bị hủy bỏ, quá trình thực thi sẽ tiếp tục cho đến khi đạt được câu lệnh mã được quản lý tiếp theo
Yếu tố cuối cùng có thể đặc biệt rắc rối, đó là bản thân .NET framework thường gọi mã không được quản lý, đôi khi vẫn ở đó trong thời gian dài. Một ví dụ có thể là khi sử dụng lớp mạng hoặc cơ sở dữ liệu. Nếu tài nguyên mạng hoặc máy chủ cơ sở dữ liệu bị chết hoặc phản hồi chậm, có thể việc thực thi có thể nằm hoàn toàn trong mã không được quản lý, có lẽ trong vài phút, tùy thuộc vào việc triển khai lớp. Trong những trường hợp này, chắc chắn một người sẽ không muốn Tham gia chuỗi bị hủy bỏ – ít nhất là không có thời gian chờ!
Việc hủy bỏ mã .NET thuần túy ít có vấn đề hơn, miễn là các khối try / last hoặc các câu lệnh sử dụng được kết hợp để đảm bảo quá trình dọn dẹp diễn ra đúng cách nếu ThreadAbortException được ném ra. Tuy nhiên, ngay cả khi đó, một người vẫn có thể dễ bị tổn thương bởi những bất ngờ khó chịu. Ví dụ, hãy xem xét những điều sau:
using (StreamWriter w = File.CreateText (“myfile.txt”))
w.Write (“Abort-Safe?”);
Câu lệnh using của C # chỉ đơn giản là một lối tắt cú pháp, trong trường hợp này mở rộng thành như sau:
StreamWriter w;
w = File.CreateText (“myfile.txt”);
thử {w.Write (“Hủy bỏ-An toàn”); }
cuối cùng {w.Dispose (); }
Abort có thể kích hoạt sau khi StreamWriter được tạo, nhưng trước khi khối thử bắt đầu. Trên thực tế, bằng cách đào sâu vào IL, người ta có thể thấy rằng nó cũng có thể kích hoạt giữa StreamWriter được tạo và gán cho w:
IL_0001: ldstr “myfile.txt”
IL_0006: gọi lớp [mscorlib] System.IO.StreamWriter
[mscorlib] System.IO.File :: CreateText (string)
IL_000b: stloc.0
.try
{
…
Dù bằng cách nào, phương thức Dispose trong khối cuối cùng cũng bị phá vỡ, dẫn đến việc xử lý tệp mở bị bỏ rơi – ngăn cản mọi nỗ lực tiếp theo để tạo myfile.txt cho đến khi miền ứng dụng kết thúc.
Trong thực tế, tình huống trong ví dụ này vẫn còn tồi tệ hơn, bởi vì rất có thể xảy ra Hủy bỏ trong quá trình triển khai File.CreateText. Đây được gọi là mã không rõ ràng – mà chúng tôi không có nguồn. May mắn thay, mã .NET không bao giờ thực sự rõ ràng: chúng ta có thể quay lại trong ILDASM, hoặc tốt hơn nữa là Reflector của Lutz Roeder – và nhìn vào các assembly của framework, thấy rằng nó gọi hàm tạo của StreamWriter, có logic sau:
public StreamWriter (string path, bool append, …)
{
…
…
Stream stream1 = StreamWriter.CreateFile (path, append);
this.Init (stream1, …);
}
Không nơi nào trong phương thức khởi tạo này có khối try / catch, có nghĩa là nếu Abort kích hoạt bất kỳ đâu trong phương thức Init (không tầm thường), luồng mới được tạo sẽ bị hủy bỏ, không có cách nào để đóng xử lý tệp bên dưới.
Bởi vì việc tách rời mọi lệnh gọi CLR được yêu cầu rõ ràng là không thực tế, điều này đặt ra câu hỏi về cách người ta nên viết một phương thức thân thiện với hủy bỏ. Cách giải quyết phổ biến nhất là không hủy bỏ một luồng khác – mà là thêm một trường boolean tùy chỉnh vào lớp của worker, báo hiệu rằng nó nên hủy bỏ. Công nhân kiểm tra cờ định kỳ, thoát ra nếu đúng. Trớ trêu thay, lối thoát duyên dáng nhất cho nhân viên là gọi Abort trên chính chuỗi của nó – mặc dù việc ném một ngoại lệ rõ ràng cũng hoạt động tốt. Điều này đảm bảo luồng được hỗ trợ ngay trong khi thực hiện bất kỳ khối bắt / cuối cùng nào – giống như gọi Abort từ một luồng khác, ngoại trừ ngoại lệ chỉ được ném từ những nơi được chỉ định:
class ProLife {
public static void Main () {
RulyWorker w = new RulyWorker ();
Thread t = new Thread (w.Work);
t.Start ();
Thread.Sleep (500);
w.Abort ();
}
public class RulyWorker {
// Từ khóa dễ bay hơi đảm bảo abort không được lưu vào bộ nhớ cache bởi một luồng
bool abort dễ bay hơi;
public void Abort () {abort = true; }
public void Work () {
while (true) {
CheckAbort ();
// Làm công cụ …
thử {OtherMethod (); }
cuối cùng {/ * mọi yêu cầu dọn dẹp * /}
}
}
void OtherMethod () {
// Thực hiện công việc …
CheckAbort ();
}
void CheckAbort () {if (hủy bỏ) Thread.CurrentThread.Abort (); }
}
}
Gọi Abort trên chuỗi của riêng mình là một trong những trường hợp mà Abort hoàn toàn an toàn. Khác là khi bạn có thể chắc chắn luồng bạn đang hủy nằm trong một phần mã cụ thể, thường là nhờ cơ chế đồng bộ hóa như Wait Handle hoặc Monitor.Wait. Một trường hợp thứ ba trong đó việc gọi Abort là an toàn là khi sau đó bạn hủy bỏ miền ứng dụng hoặc quy trình của chuỗi. |
Miền ứng dụng kết thúc
Một cách khác để triển khai một worker thân thiện với hủy bỏ là để luồng của nó chạy trong miền ứng dụng của chính nó. Sau khi gọi Abort, người ta chỉ cần loại bỏ miền ứng dụng, do đó giải phóng bất kỳ tài nguyên nào bị xử lý không đúng cách.
Nói một cách chính xác, bước đầu tiên – hủy bỏ luồng – là không cần thiết, vì khi miền ứng dụng được dỡ bỏ, tất cả các luồng thực thi mã trong miền đó sẽ tự động bị hủy bỏ. Tuy nhiên, nhược điểm của việc dựa vào hành vi này là nếu các luồng bị hủy bỏ không thoát kịp thời (có thể do mã trong các khối cuối cùng hoặc vì các lý do khác đã được thảo luận trước đó) thì miền ứng dụng sẽ không tải và CannotUnloadAppDomainException sẽ được ném vào người gọi. Vì lý do này, tốt hơn nên hủy bỏ chuỗi công nhân một cách rõ ràng, sau đó gọi Tham gia với một số thời gian chờ (mà bạn có quyền kiểm soát) trước khi dỡ bỏ miền ứng dụng.
Trong ví dụ sau, worker đi vào một vòng lặp vô hạn, tạo và đóng tệp bằng phương thức File.CreateText hủy bỏ không an toàn. Sau đó, chuỗi chính liên tục khởi động và hủy bỏ các công nhân. Nó thường không thành công trong vòng một hoặc hai lần lặp lại, với CreateText bị hủy bỏ một phần thông qua triển khai nội bộ của nó, để lại một xử lý tệp mở bị bỏ rơi:
sử dụng Hệ thống;
sử dụng System.IO;
sử dụng System.Threading;
class Program {
static void Main () {
while (true) {
Thread t = new Thread (Work);
t.Start ();
Thread.Sleep (100);
t.Abort ();
Console.WriteLine (“Đã hủy bỏ”);
}
}
static void Work () {
while (true)
using (StreamWriter w = File.CreateText (“myfile.txt”)) {}
}
}
Aborted |
Đây là chương trình tương tự đã được sửa đổi để luồng công nhân chạy trong miền ứng dụng của riêng nó, miền này sẽ được tải xuống sau khi luồng bị hủy bỏ. Nó chạy vĩnh viễn mà không có lỗi, vì việc tải miền ứng dụng sẽ giải phóng tệp xử lý bị bỏ rơi:
class Program {
static void Main (string [] args) {
while (true) {
AppDomain ad = AppDomain.CreateDomain (“worker”);
Thread t = new Thread (ủy nhiệm () {ad.DoCallBack (Work);});
t.Start ();
Thread.Sleep (100);
t.Abort ();
if (! t.Join (2000)) {
// Chuỗi sẽ không kết thúc – đây là nơi chúng tôi có thể thực hiện thêm hành động,
// nếu, thực sự, chúng tôi có thể làm bất cứ điều gì. May mắn thay trong
// trường hợp này, chúng ta có thể mong đợi chuỗi * always * kết thúc.
}
AppDomain.Unload (quảng cáo); // Xé bỏ miền ô nhiễm!
Console.WriteLine (“Đã hủy bỏ”);
}
}
static void Work () {
while (true)
using (StreamWriter w = File.CreateText (“myfile.txt”)) {}
}
}
Bị hủy bỏ Đã hủy bỏ Bị hủy bỏ |
Việc tạo và hủy một miền ứng dụng được xếp vào loại tương đối tốn thời gian trong thế giới của các hoạt động phân luồng (mất vài mili giây), vì vậy nó có lợi cho việc thực hiện không thường xuyên thay vì lặp lại! Ngoài ra, sự phân tách được giới thiệu bởi miền ứng dụng giới thiệu một phần tử khác có thể có lợi hoặc có hại, tùy thuộc vào những gì chương trình đa luồng đang đặt ra để đạt được. Ví dụ, trong bối cảnh kiểm thử đơn vị, việc chạy các luồng trên các miền ứng dụng riêng biệt có thể mang lại lợi ích lớn.
Quá trình kết thúc
Một cách khác mà một luồng có thể kết thúc là khi tiến trình mẹ kết thúc. Một ví dụ về điều này là khi thuộc tính IsBackground của một luồng worker được đặt thành true và luồng chính kết thúc trong khi worker vẫn đang chạy. Luồng nền không thể giữ cho ứng dụng tồn tại và do đó quá trình kết thúc, mang theo luồng nền với nó.
Khi một luồng kết thúc do tiến trình mẹ của nó, nó sẽ ngừng chết và không có khối cuối cùng nào được thực thi.
Tình huống tương tự xảy ra khi người dùng chấm dứt một ứng dụng không phản hồi thông qua Trình quản lý tác vụ Windows hoặc một quy trình bị giết theo chương trình thông qua Process.Kill.
(Sưu tầm)
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. |