簡體   English   中英

如何像中止線程一樣中止任務(Thread.Abort 方法)?

[英]How to abort a Task like aborting a Thread (Thread.Abort method)?

我們可以像這樣中止Thread

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();

但是我可以用同樣的方式而不是通過取消機制來中止Task (in.Net 4.0)嗎? 我想立即終止任務。

關於不使用線程中止的指導是有爭議的。 我認為它仍有一席之地,但在特殊情況下。 但是,您應該始終嘗試圍繞它進行設計並將其視為最后的手段。

例子;

您有一個連接到阻塞同步 Web 服務的簡單 Windows 窗體應用程序。 它在並行循環中對 Web 服務執行一個函數。

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    Thread.Sleep(120000); // pretend web service call

});

在這個例子中,阻塞調用需要 2 分鍾才能完成。 現在我將 MaxDegreeOfParallelism 設置為 ProcessorCount。 iListOfItems 中有 1000 個要處理的項目。

用戶單擊進程按鈕並開始循環,我們有“最多”20 個線程針對 iListOfItems 集合中的 1000 個項目執行。 每次迭代都在自己的線程上執行。 每個線程在由 Parallel.ForEach 創建時將使用一個前台線程。 這意味着無論主應用程序關閉,應用程序域都將保持活動狀態,直到所有線程完成。

但是用戶出於某種原因需要關閉應用程序,比如他們關閉表單。 這 20 個線程將繼續執行,直到處理完所有 1000 個項目。 這在這種情況下並不理想,因為應用程序不會像用戶期望的那樣退出,而是會繼續在幕后運行,這可以通過查看任務管理器看出。

假設用戶再次嘗試重建應用程序 (VS 2010),它報告 exe 被鎖定,然后他們將不得不進入任務管理器來殺死它,或者只是等到所有 1000 個項目都被處理。

我不會怪你說,但當然! 我應該使用CancellationTokenSource對象取消這些線程並調用 Cancel ...但是從 .net 4.0 開始,這存在一些問題。 首先,這仍然永遠不會導致線程中止,這將提供中止異常,然后線程終止,因此應用程序域將需要等待線程正常完成,這意味着等待最后一個阻塞調用,這將是最終調用po.CancellationToken.ThrowIfCancellationRequested最后一個運行迭代(線程)。 在示例中,這意味着應用程序域仍然可以保持活動長達 2 分鍾,即使表單已關閉並調用了取消。

請注意,在 CancellationTokenSource 上調用 Cancel 不會在處理線程上引發異常,這確實會中斷阻塞調用,類似於線程中止並停止執行。 當所有其他線程(並發迭代)最終完成並返回時,異常被緩存准備好,異常將在啟動線程(聲明循環的地方)中拋出。

我選擇不在CancellationTokenSource 對象上使用Cancel選項。 這是一種浪費,並且可以說違反了眾所周知的通過異常控制代碼流的反模式。

相反,實現一個簡單的線程安全屬性(即 Bool stopExecuting)可以說是“更好的”。 然后在循環中,檢查 stopExecuting 的值,如果該值被外部影響設置為 true,我們可以采取替代路徑優雅地關閉。 由於我們不應該調用取消,這排除了檢查CancellationTokenSource.IsCancellationRequested 的可能性,否則這將是另一種選擇。

類似下面的 if 條件在循環中是合適的;

if (loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting) {loopState.Stop(); 返回;}

迭代現在將以“受控”方式退出並終止進一步的迭代,但正如我所說,這對於我們必須等待每次迭代中進行的長時間運行和阻塞調用的問題幾乎沒有作用(並行循環線程),因為這些必須在每個線程可以選擇檢查它是否應該停止之前完成。

總之,當用戶關閉表單時,將通過 stopExecuting 通知 20 個線程停止,但只有當它們完成執行長時間運行的函數調用時才會停止。

對於應用程序域將始終保持活動狀態並且僅在所有前台線程完成后才被釋放的事實,我們無能為力。 這意味着等待循環內進行的任何阻塞調用完成時會出現延遲。

只有真正的線程中止才能中斷阻塞調用,並且您必須在中止線程的異常處理程序中盡最大努力避免使系統處於不穩定/未定義狀態,這是毫無疑問的。 這是否合適由程序員決定,基於他們選擇維護的資源句柄以及在線程的 finally 塊中關閉它們的容易程度。 您可以使用令牌注冊以在取消時終止作為半解決方法,即

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }

});

但這仍然會導致聲明線程中出現異常。

考慮到所有因素,使用 parallel.loop 構造的中斷阻塞調用可能是選項上的一種方法,避免使用庫中更晦澀的部分。 但是為什么在聲明方法中沒有取消和避免拋出異常的選項讓我覺得可能是疏忽。

但是我可以以相同的方式中止任務(在 .Net 4.0 中)而不是通過取消機制。 我想立即殺死任務

其他回答者告訴你不要這樣做。 但是,是的,你可以做到。 您可以提供Thread.Abort()作為由任務的取消機制調用的委托。 您可以通過以下方式進行配置:

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }

  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;

    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();

    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }

  public void Abort()
  {
    Canceller.Cancel();
  }

}

免責聲明:不要這樣做。

以下是不該做什么的示例:

 var doNotDoThis = new HardAborter();

 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });


 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);

 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);

 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);

 Console.ReadKey();

示例代碼的輸出

  1. 你不應該使用 Thread.Abort()
  2. 任務可以取消但不能中止。

Thread.Abort()方法(嚴重)已棄用。

線程和任務在停止時都應該合作,否則您將面臨使系統處於不穩定/未定義狀態的風險。

如果您確實需要運行一個進程並從外部終止它,唯一安全的選擇是在單獨的 AppDomain 中運行它。


這個答案是關於 .net 3.5 及更早版本的。

從那時起,線程中止處理得到了改進,ao 通過改變 finally 塊的工作方式。

但是 Thread.Abort 仍然是您應該始終盡量避免的可疑解決方案。

每個人都知道(希望)終止線程是不好的。 問題是當您不擁有要調用的一段代碼時。 如果這段代碼在某個 do/while 無限循環中運行,它本身調用了一些本機函數等,那么你基本上就被卡住了。 當這種情況發生在您自己的代碼終止、停止或 Dispose 調用中時,可以開始射擊壞人(這樣您自己就不會成為壞人)。

因此,就其價值而言,我編寫了這兩個阻塞函數,它們使用自己的本機線程,而不是池中的線程或 CLR 創建的某個線程。 如果發生超時,他們將停止線程:

// returns true if the call went to completion successfully, false otherwise
public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
public static bool RunWithAbort(this Action action, TimeSpan delay)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));

    var source = new CancellationTokenSource(delay);
    var success = false;
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            action();
            success = true;
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return success;
}

// returns what's the function should return if the call went to completion successfully, default(T) otherwise
public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
{
    if (func == null)
        throw new ArgumentNullException(nameof(func));

    var source = new CancellationTokenSource(delay);
    var item = default(T);
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            item = func();
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return item;
}

[DllImport("kernel32")]
private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);

[DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);

[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32")]
private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);

雖然可以中止線程,但實際上這樣做幾乎總是一個非常糟糕的主意。 中止線程意味着線程沒有機會自行清理,從而使資源未被刪除,以及處於未知狀態的事物。

在實踐中,如果您中止一個線程,您應該只與殺死該進程一起這樣做。 可悲的是,太多人認為 ThreadAbort 是停止某些事情並繼續進行的可行方法,但事實並非如此。

由於任務作為線程運行,您可以對它們調用 ThreadAbort,但與通用線程一樣,您幾乎從不想這樣做,除非作為最后的手段。

我遇到了與 Excel 的Application.Workbooks類似的問題。

如果應用程序很忙,該方法將永遠掛起。 我的方法只是嘗試將它放入任務中並等待,如果它花費的時間太長,我就把任務留在原地,go 離開( “在這種情況下”沒有害處,Excel 將在用戶完成任何操作時解凍繁忙)。

在這種情況下,不可能使用取消令牌。 優點是我不需要過多的代碼、中止線程等。

public static List<Workbook> GetAllOpenWorkbooks()
{
    //gets all open Excel applications
    List<Application> applications = GetAllOpenApplications();

    //this is what we want to get from the third party library that may freeze
    List<Workbook> books = null;

    //as Excel may freeze here due to being busy, we try to get the workbooks asynchronously
    Task task = Task.Run(() =>
    {
        try 
        { 
            books = applications
                .SelectMany(app => app.Workbooks.OfType<Workbook>()).ToList();
        }
        catch { }
    });
    
    //wait for task completion
    task.Wait(5000);
    return books; //handle outside if books is null
}

這是我對@Simon-Mourier 提出的一個想法的實現,使用 do.net 線程,簡短的代碼:

    public static bool RunWithAbort(this Action action, int milliseconds)
    {
         if (action == null) throw new ArgumentNullException(nameof(action));

        var success = false;
        var thread = new Thread(() =>
        {
            action();
            success = true;
        });
        thread.IsBackground = true;
        thread.Start();
        thread.Join(milliseconds);          
        thread.Abort();

        return success;
    }

您可以通過在您控制的線程上運行並中止該線程來“中止”任務。 這會導致任務在出現ThreadAbortException的錯誤狀態下完成。 您可以使用自定義任務調度程序控制線程創建,如本答案中所述 請注意, 有關中止線程警告適用。

(如果您不確保任務是在其自己的線程上創建的,則中止它會中止線程池線程或啟動任務的線程,您通常都不想這樣做。)

using System;
using System.Threading;
using System.Threading.Tasks;

...

var cts = new CancellationTokenSource();
var task = Task.Run(() => { while (true) { } });
Parallel.Invoke(() =>
{
    task.Wait(cts.Token);
}, () =>
{
    Thread.Sleep(1000);
    cts.Cancel();
});

這是使用CancellationTokenSource中止永無止境的任務的簡單片段。

如果您具有Task構造函數,則可以從Task中提取Thread,然后調用thread.abort。

Thread th = null;

Task.Factory.StartNew(() =>
{
    th = Thread.CurrentThread;

    while (true)
    {
        Console.WriteLine(DateTime.UtcNow);
    }
});

Thread.Sleep(2000);
th.Abort();
Console.ReadKey();

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM