欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

C#如何優(yōu)雅地取消進(jìn)程的執(zhí)行之Cancellation詳解

 更新時(shí)間:2024年12月31日 08:49:15   作者:木林森先生  
本文介紹了.NET框架中的取消協(xié)作模型,包括CancellationToken的使用、取消請(qǐng)求的發(fā)送和接收、以及如何處理取消事件

概述

從.NET Framework 4開始,.NET使用統(tǒng)一的模型來協(xié)作取消異步或長(zhǎng)時(shí)間運(yùn)行的同步線程。該模型基于一個(gè)稱為CancellationToken的輕量級(jí)對(duì)象。這個(gè)對(duì)象在調(diào)用一個(gè)或多個(gè)取消線程時(shí)(例如通過創(chuàng)建新線程或任務(wù)),是通過將token傳遞給每個(gè)線程來完成的(通過鏈?zhǔn)降姆绞揭来蝹鬟f)。單個(gè)線程能夠依次地將token的副本傳遞給其他線程。

之后,在適當(dāng)?shù)哪硞€(gè)時(shí)機(jī),創(chuàng)建token的對(duì)象就可以使用token來請(qǐng)求線程停止。只有請(qǐng)求對(duì)象可以發(fā)出取消請(qǐng)求,每個(gè)監(jiān)聽器負(fù)責(zé)監(jiān)聽到請(qǐng)求并以適當(dāng)和及時(shí)的方式響應(yīng)取消請(qǐng)求。

實(shí)現(xiàn)協(xié)作取消模型的一般模式是:

  • 1、實(shí)例化一個(gè)CancellationTokenSource對(duì)象,該對(duì)象管理cancellation并將cancellation通知發(fā)送給單獨(dú)的cancellation token。
  • 2、CancellationTokenSource對(duì)象的Token屬性,可以返回一個(gè)Token對(duì)象,我們可以將該Token對(duì)象發(fā)送給每個(gè)監(jiān)聽該cancellation的進(jìn)程或Task。
  • 3、為每個(gè)任務(wù)或線程提供響應(yīng)取消的機(jī)制。
  • 4、調(diào)用 CancellationTokenSource.Cancel() 方法,來取消線程或者Task。

【tips】我們?cè)谑褂胏ancellation的token取消線程后,應(yīng)該確保調(diào)用CancellationTokenSource.Dispose()方法,以便于釋放它持有的任何非托管資源。。

下圖展示出了CancellationTokenSource對(duì)象里的Token屬性對(duì)象,是如何傳遞到其他的線程里的。

合作取消模型使創(chuàng)建取消感知的應(yīng)用程序和庫(kù)變得更容易,它支持以下功能:

  • 1、取消是合式的,不會(huì)強(qiáng)加給監(jiān)聽器。監(jiān)聽器確定如何優(yōu)雅地終止以響應(yīng)取消請(qǐng)求。
  • 2、請(qǐng)求不同于監(jiān)聽。調(diào)用可取消的線程的對(duì)象,可以控制何時(shí)(如果有的話)取消被請(qǐng)求。
  • 3、請(qǐng)求的對(duì)象,可以通過僅使用一個(gè)方法,即可發(fā)送取消請(qǐng)求到所有的token副本中。
  • 4、監(jiān)聽器可以通過將多個(gè)Token連接成一個(gè)linked Token,來同時(shí)監(jiān)聽多個(gè)Token。
  • 5、用戶代碼可以注意到并響應(yīng)library code的取消請(qǐng)求,而library code可以注意到并響應(yīng)用戶代碼的取消請(qǐng)求。
  • 6、可以通過輪詢、回調(diào)注冊(cè)或等待等待句柄的方式,來通知監(jiān)聽器執(zhí)行取消請(qǐng)求。

與取消線程相關(guān)的類型

取消框架是作為一組相關(guān)類型實(shí)現(xiàn)的,這些類型在下表中列出。

CancellationTokenSource該對(duì)象創(chuàng)建cancellation token,并向 cancellation token的所有副本分發(fā)取消請(qǐng)求。
CancellationToken傳遞給一個(gè)或多個(gè)監(jiān)聽器的輕量級(jí)的值類型,通常作為方法參數(shù)。偵聽器通過輪詢、回調(diào)或等待句柄監(jiān)視token的IsCancellationRequested屬性的值。
OperationCanceledException此異常構(gòu)造函數(shù)的重載,接受CancellationToken作為參數(shù)。偵聽器可以選擇性地拋出此異常以驗(yàn)證取消的來源,并通知其他已響應(yīng)取消請(qǐng)求監(jiān)聽器。

取消模型以幾種類型集成到.net中。

最重要的是System.Threading.Tasks.Parallel,System.Threading.Tasks.Task、System.Threading.Tasks.Task<TResult> 和 System.Linq.ParallelEnumerable。

建議使用所有新的庫(kù)和應(yīng)用代碼來實(shí)現(xiàn)合作市取消模式。

代碼舉例

在下面的示例中,請(qǐng)求對(duì)象創(chuàng)建一個(gè)CancellationTokenSource對(duì)象,然后將該對(duì)象的Token屬性傳遞給可取消的進(jìn)程。

接收請(qǐng)求的線程通過輪詢來監(jiān)視Token的IsCancellationRequested屬性的值。

當(dāng)該值變?yōu)閠rue時(shí),偵聽器可以以任何合適的方式終止。在本例中,方法只是退出,這是許多情況下所需要的全部?jī)?nèi)容。

using System;
using System.Threading;

public class Example
{
    public static void Main()
    {
        // Create the token source.
        CancellationTokenSource cts = new CancellationTokenSource();

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        Thread.Sleep(2500);

        // Request cancellation.
        cts.Cancel();
        Console.WriteLine("Cancellation set in token source...");
        Thread.Sleep(2500);
        // Cancellation should have happened, so call Dispose.
        cts.Dispose();
    }

    // Thread 2: The listener
    static void DoSomeWork(object? obj)
    {
        if (obj is null)
            return;

        CancellationToken token = (CancellationToken)obj;

        for (int i = 0; i < 100000; i++)
        {
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("In iteration {0}, cancellation has been requested...",
                                  i + 1);
                // Perform cleanup if necessary.
                //...
                // Terminate the operation.
                break;
            }
            // Simulate some work.
            Thread.SpinWait(500000);
        }
    }
}
// The example displays output like the following:
//       Cancellation set in token source...
//       In iteration 1430, cancellation has been requested...

操作取消vs對(duì)象取消

在協(xié)作取消框架中,取消指的是操作(線程中執(zhí)行的操作),而不是對(duì)象。取消請(qǐng)求意味著在執(zhí)行任何所需的清理后,操作應(yīng)盡快停止。一個(gè)cancellation token應(yīng)該指向一個(gè)“可取消的操作”,無論該操作如何在您的程序中實(shí)現(xiàn)。

在token的IsCancellationRequested屬性被設(shè)置為true之后,它不能被重置為false。因此,取消令牌在被取消后不能被重用。

如果您需要對(duì)象取消機(jī)制,您可以通過調(diào)用CancellationToken來基于操作取消機(jī)制。注冊(cè)方法,如下例所示。

using System;
using System.Threading;

class CancelableObject
{
    public string id;

    public CancelableObject(string id)
    {
        this.id = id;
    }

    public void Cancel()
    {
        Console.WriteLine("Object {0} Cancel callback", id);
        // Perform object cancellation here.
    }
}

public class Example1
{
    public static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();
        CancellationToken token = cts.Token;

        // User defined Class with its own method for cancellation
        var obj1 = new CancelableObject("1");
        var obj2 = new CancelableObject("2");
        var obj3 = new CancelableObject("3");

        // Register the object's cancel method with the token's
        // cancellation request.
        token.Register(() => obj1.Cancel());
        token.Register(() => obj2.Cancel());
        token.Register(() => obj3.Cancel());

        // Request cancellation on the token.
        cts.Cancel();
        // Call Dispose when we're done with the CancellationTokenSource.
        cts.Dispose();
    }
}
// The example displays the following output:
//       Object 3 Cancel callback
//       Object 2 Cancel callback
//       Object 1 Cancel callback

如果一個(gè)對(duì)象支持多個(gè)并發(fā)的可取消操作,則可以給每個(gè)不同的可取消操作各自傳入一個(gè)不同的token。這樣,一個(gè)操作可以被取消而不會(huì)影響到其他操作。

監(jiān)聽并響應(yīng)取消請(qǐng)求

在用戶委托中,可取消操作的實(shí)現(xiàn)者決定如何終止該操作以響應(yīng)取消請(qǐng)求。在許多情況下,用戶委托可以只執(zhí)行任何所需的清理,然后立即返回。

但是,在更復(fù)雜的情況下,可能需要用戶委托通知庫(kù)代碼已發(fā)生cancellation。在這種情況下,終止操作的正確方法是委托調(diào)用ThrowIfCancellationRequested方法,這將導(dǎo)致拋出OperationCanceledException異常。庫(kù)代碼可以在用戶委托線程上捕獲此異常,并檢查異常的token,以確定該異常是否表示協(xié)作取消或其他異常情況。

在這種情況下,終止操作的正確方法是委托調(diào)用ThrowIfCancellationRequested方法,這將導(dǎo)致拋出OperationCanceledException。庫(kù)代碼可以在用戶委托線程上捕獲此異常,并檢查異常的token,以確定該異常是否表示協(xié)作取消或其他異常情況。

輪詢監(jiān)聽

對(duì)于循環(huán)或遞歸的長(zhǎng)時(shí)間運(yùn)行的計(jì)算,可以通過定期輪詢CancellationToken.IsCancellationRequested的值來監(jiān)聽取消請(qǐng)求。如果它的值為true,則該方法應(yīng)該盡快清理并終止。輪詢的最佳頻率取決于應(yīng)用程序的類型。開發(fā)人員可以為任何給定的程序確定最佳輪詢頻率。輪詢本身不會(huì)顯著影響性能。

下面的程序案例展示了一種可能的輪詢方式。

static void NestedLoops(Rectangle rect, CancellationToken token)
{
   for (int col = 0; col < rect.columns && !token.IsCancellationRequested; col++) {
      // Assume that we know that the inner loop is very fast.
      // Therefore, polling once per column in the outer loop condition
      // is sufficient.
      for (int row = 0; row < rect.rows; row++) {
         // Simulating work.
         Thread.SpinWait(5_000);
         Console.Write("{0},{1} ", col, row);
      }
   }

   if (token.IsCancellationRequested) {
      // Cleanup or undo here if necessary...
      Console.WriteLine("\r\nOperation canceled");
      Console.WriteLine("Press any key to exit.");

      // If using Task:
      // token.ThrowIfCancellationRequested();
   }
}

下面的程序代碼是一個(gè)詳細(xì)的實(shí)現(xiàn):

using System;
using System.Threading;

public class ServerClass
{
   public static void StaticMethod(object obj)
   {
      CancellationToken ct = (CancellationToken)obj;
      Console.WriteLine("ServerClass.StaticMethod is running on another thread.");

      // Simulate work that can be canceled.
      while (!ct.IsCancellationRequested) {
         Thread.SpinWait(50000);
      }
      Console.WriteLine("The worker thread has been canceled. Press any key to exit.");
      Console.ReadKey(true);
   }
}

public class Simple
{
   public static void Main()
   {
      // The Simple class controls access to the token source.
      CancellationTokenSource cts = new CancellationTokenSource();

      Console.WriteLine("Press 'C' to terminate the application...\n");
      // Allow the UI thread to capture the token source, so that it
      // can issue the cancel command.
      Thread t1 = new Thread(() => { if (Console.ReadKey(true).KeyChar.ToString().ToUpperInvariant() == "C")
                                     cts.Cancel(); } );

      // ServerClass sees only the token, not the token source.
      Thread t2 = new Thread(new ParameterizedThreadStart(ServerClass.StaticMethod));
      // Start the UI thread.

      t1.Start();

      // Start the worker thread and pass it the token.
      t2.Start(cts.Token);

      t2.Join();
      cts.Dispose();
   }
}
// The example displays the following output:
//       Press 'C' to terminate the application...
//
//       ServerClass.StaticMethod is running on another thread.
//       The worker thread has been canceled. Press any key to exit.

通過回調(diào)注冊(cè)進(jìn)行監(jiān)聽

以這種方式進(jìn)行的某些操作可能會(huì)阻塞,從而無法及時(shí)檢查cancellation token的值。對(duì)于這些情況,您可以注冊(cè)一個(gè)回調(diào)方法,以便在收到取消請(qǐng)求時(shí)解除對(duì)該方法的阻塞。

Register方法返回一個(gè)專門用于此目的的CancellationTokenRegistration對(duì)象。下面的示例展示了如何使用Register方法來取消異步Web請(qǐng)求。

using System;
using System.Net;
using System.Threading;

class Example4
{
    static void Main()
    {
        CancellationTokenSource cts = new CancellationTokenSource();

        StartWebRequest(cts.Token);

        // cancellation will cause the web
        // request to be cancelled
        cts.Cancel();
    }

    static void StartWebRequest(CancellationToken token)
    {
        WebClient wc = new WebClient();
        wc.DownloadStringCompleted += (s, e) => Console.WriteLine("Request completed.");

        // Cancellation on the token will
        // call CancelAsync on the WebClient.
        token.Register(() =>
        {
            wc.CancelAsync();
            Console.WriteLine("Request cancelled!");
        });

        Console.WriteLine("Starting request.");
        wc.DownloadStringAsync(new Uri("http://www.contoso.com"));
    }
}

CancellationTokenRegistration對(duì)象管理線程同步,并確?;卣{(diào)將在精確的時(shí)間點(diǎn)停止執(zhí)行。

為了確保系統(tǒng)響應(yīng)性并避免死鎖,在注冊(cè)回調(diào)時(shí)必須遵循以下準(zhǔn)則:

1、回調(diào)方法應(yīng)該是快速的,因?yàn)樗峭秸{(diào)用的,因此對(duì)Cancel的調(diào)用在回調(diào)返回之前不會(huì)返回。

2、如果在回調(diào)運(yùn)行時(shí)調(diào)用Dispose,并且持有回調(diào)等待的鎖,則程序可能會(huì)死鎖。Dispose返回后,您可以釋放回調(diào)所需的任何資源。

3、Callbacks 不應(yīng)該執(zhí)行任何手動(dòng)線程或在回調(diào)中使用SynchronizationContext。如果回調(diào)必須在特定線程上運(yùn)行,則使用System.Threading.CancellationTokenRegistration構(gòu)造函數(shù),該構(gòu)造函數(shù)使您能夠指定目標(biāo)syncContext是活動(dòng)的SynchronizationContext.Current。在回調(diào)中執(zhí)行手動(dòng)線程會(huì)導(dǎo)致死鎖。

使用WaitHandle進(jìn)行偵聽

當(dāng)一個(gè)可取消的操作在等待一個(gè)同步原語(如System.Threading. manualresetevent或System.Threading. Semaphore)時(shí)可能會(huì)阻塞。

你可以使用CancellationToken.WaitHandle屬性,以使操作同時(shí)等待事件和取消請(qǐng)求。

CancellationToken的 等待句柄 將在響應(yīng)取消請(qǐng)求時(shí)發(fā)出信號(hào),該方法可以使用WaitAny()方法的返回值來確定發(fā)出信號(hào)的是否是cancellation token。然后操作可以直接退出,或者拋出OperationCanceledException異常。

// Wait on the event if it is not signaled.
int eventThatSignaledIndex =
       WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                          new TimeSpan(0, 0, 20));

System.Threading.ManualResetEventSlim和System.Threading.SemaphoreSlim都在它們的Wait()方法中支持取消框架。

您可以將CancellationToken傳遞給該方法,當(dāng)請(qǐng)求取消時(shí),事件將被喚醒并拋出OperationCanceledException。

try
{
    // mres is a ManualResetEventSlim
    mres.Wait(token);
}
catch (OperationCanceledException)
{
    // Throw immediately to be responsive. The
    // alternative is to do one more item of work,
    // and throw on next iteration, because
    // IsCancellationRequested will be true.
    Console.WriteLine("The wait operation was canceled.");
    throw;
}

Console.Write("Working...");
// Simulating work.
Thread.SpinWait(500000);

下面的示例使用ManualResetEvent來演示如何解除阻塞不支持統(tǒng)一取消的等待句柄。

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

class CancelOldStyleEvents
{
    // Old-style MRE that doesn't support unified cancellation.
    static ManualResetEvent mre = new ManualResetEvent(false);

    static void Main()
    {
        var cts = new CancellationTokenSource();

        // Pass the same token source to the delegate and to the task instance.
        Task.Run(() => DoWork(cts.Token), cts.Token);
        Console.WriteLine("Press s to start/restart, p to pause, or c to cancel.");
        Console.WriteLine("Or any other key to exit.");

        // Old-style UI thread.
        bool goAgain = true;
        while (goAgain)
        {
            char ch = Console.ReadKey(true).KeyChar;

            switch (ch)
            {
                case 'c':
                    cts.Cancel();
                    break;
                case 'p':
                    mre.Reset();
                    break;
                case 's':
                    mre.Set();
                    break;
                default:
                    goAgain = false;
                    break;
            }

            Thread.Sleep(100);
        }
        cts.Dispose();
    }

    static void DoWork(CancellationToken token)
    {
        while (true)
        {
            // Wait on the event if it is not signaled.
            int eventThatSignaledIndex =
                   WaitHandle.WaitAny(new WaitHandle[] { mre, token.WaitHandle },
                                      new TimeSpan(0, 0, 20));

            // Were we canceled while waiting?
            if (eventThatSignaledIndex == 1)
            {
                Console.WriteLine("The wait operation was canceled.");
                throw new OperationCanceledException(token);
            }
            // Were we canceled while running?
            else if (token.IsCancellationRequested)
            {
                Console.WriteLine("I was canceled while running.");
                token.ThrowIfCancellationRequested();
            }
            // Did we time out?
            else if (eventThatSignaledIndex == WaitHandle.WaitTimeout)
            {
                Console.WriteLine("I timed out.");
                break;
            }
            else
            {
                Console.Write("Working... ");
                // Simulating work.
                Thread.SpinWait(5000000);
            }
        }
    }
}

下面的示例使用ManualResetEventSlim來演示如何解除支持統(tǒng)一取消的協(xié)調(diào)原語的阻塞。同樣的方法也可以用于其他輕量級(jí)協(xié)調(diào)原語,如SemaphoreSlim和CountdownEvent。

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

class CancelNewStyleEvents
{
   // New-style MRESlim that supports unified cancellation
   // in its Wait methods.
   static ManualResetEventSlim mres = new ManualResetEventSlim(false);

   static void Main()
   {
      var cts = new CancellationTokenSource();

      // Pass the same token source to the delegate and to the task instance.
      Task.Run(() => DoWork(cts.Token), cts.Token);
      Console.WriteLine("Press c to cancel, p to pause, or s to start/restart,");
      Console.WriteLine("or any other key to exit.");

      // New-style UI thread.
         bool goAgain = true;
         while (goAgain)
         {
             char ch = Console.ReadKey(true).KeyChar;

             switch (ch)
             {
                 case 'c':
                     // Token can only be canceled once.
                     cts.Cancel();
                     break;
                 case 'p':
                     mres.Reset();
                     break;
                 case 's':
                     mres.Set();
                     break;
                 default:
                     goAgain = false;
                     break;
             }

             Thread.Sleep(100);
         }
         cts.Dispose();
     }

     static void DoWork(CancellationToken token)
     {

         while (true)
         {
             if (token.IsCancellationRequested)
             {
                 Console.WriteLine("Canceled while running.");
                 token.ThrowIfCancellationRequested();
             }

             // Wait on the event to be signaled
             // or the token to be canceled,
             // whichever comes first. The token
             // will throw an exception if it is canceled
             // while the thread is waiting on the event.
             try
             {
                 // mres is a ManualResetEventSlim
                 mres.Wait(token);
             }
             catch (OperationCanceledException)
             {
                 // Throw immediately to be responsive. The
                 // alternative is to do one more item of work,
                 // and throw on next iteration, because
                 // IsCancellationRequested will be true.
                 Console.WriteLine("The wait operation was canceled.");
                 throw;
             }

             Console.Write("Working...");
             // Simulating work.
             Thread.SpinWait(500000);
         }
     }
 }

同時(shí)監(jiān)聽多個(gè)令牌

在某些情況下,偵聽器必須同時(shí)偵聽多個(gè)cancellation token。

例如,一個(gè)可取消操作除了監(jiān)控通過方法形參傳入的外部token之外,還可能必須監(jiān)視內(nèi)部的cancellation token。為此,創(chuàng)建一個(gè)linked token源,它可以將兩個(gè)或多個(gè)token連接到一個(gè)token中,如下面的示例所示。

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

class LinkedTokenSourceDemo
{
    static void Main()
    {
        WorkerWithTimer worker = new WorkerWithTimer();
        CancellationTokenSource cts = new CancellationTokenSource();

        // Task for UI thread, so we can call Task.Wait wait on the main thread.
        Task.Run(() =>
        {
            Console.WriteLine("Press 'c' to cancel within 3 seconds after work begins.");
            Console.WriteLine("Or let the task time out by doing nothing.");
            if (Console.ReadKey(true).KeyChar == 'c')
                cts.Cancel();
        });

        // Let the user read the UI message.
        Thread.Sleep(1000);

        // Start the worker task.
        Task task = Task.Run(() => worker.DoWork(cts.Token), cts.Token);

        try
        {
            task.Wait(cts.Token);
        }
        catch (OperationCanceledException e)
        {
            if (e.CancellationToken == cts.Token)
                Console.WriteLine("Canceled from UI thread throwing OCE.");
        }
        catch (AggregateException ae)
        {
            Console.WriteLine("AggregateException caught: " + ae.InnerException);
            foreach (var inner in ae.InnerExceptions)
            {
                Console.WriteLine(inner.Message + inner.Source);
            }
        }

        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
        cts.Dispose();
    }
}

class WorkerWithTimer
{
    CancellationTokenSource internalTokenSource = new CancellationTokenSource();
    CancellationToken internalToken;
    CancellationToken externalToken;
    Timer timer;

    public WorkerWithTimer()
    {
        // A toy cancellation trigger that times out after 3 seconds
        // if the user does not press 'c'.
        timer = new Timer(new TimerCallback(CancelAfterTimeout), null, 3000, 3000);
    }

    public void DoWork(CancellationToken externalToken)
    {
        // Create a new token that combines the internal and external tokens.
        this.internalToken = internalTokenSource.Token;
        this.externalToken = externalToken;

        using (CancellationTokenSource linkedCts =
                CancellationTokenSource.CreateLinkedTokenSource(internalToken, externalToken))
        {
            try
            {
                DoWorkInternal(linkedCts.Token);
            }
            catch (OperationCanceledException)
            {
                if (internalToken.IsCancellationRequested)
                {
                    Console.WriteLine("Operation timed out.");
                }
                else if (externalToken.IsCancellationRequested)
                {
                    Console.WriteLine("Cancelling per user request.");
                    externalToken.ThrowIfCancellationRequested();
                }
            }
        }
    }

    private void DoWorkInternal(CancellationToken token)
    {
        for (int i = 0; i < 1000; i++)
        {
            if (token.IsCancellationRequested)
            {
                // We need to dispose the timer if cancellation
                // was requested by the external token.
                timer.Dispose();

                // Throw the exception.
                token.ThrowIfCancellationRequested();
            }

            // Simulating work.
            Thread.SpinWait(7500000);
            Console.Write("working... ");
        }
    }

    public void CancelAfterTimeout(object? state)
    {
        Console.WriteLine("\r\nTimer fired.");
        internalTokenSource.Cancel();
        timer.Dispose();
    }
}

注意,當(dāng)您完成對(duì)鏈接的令牌源的處理后,必須對(duì)它調(diào)用Dispose。

當(dāng)linked token拋出一個(gè)操作消連時(shí),傳遞給異常的token就是linked token,而不是前任token。為了確定token的哪個(gè)被取消,請(qǐng)直接檢查前任token的狀態(tài)。

在本例中,AggregateException不應(yīng)該被拋出,但這里會(huì)捕獲它,因?yàn)樵趯?shí)際場(chǎng)景中,除了從任務(wù)委托拋出的OperationCanceledException之外,任何其他異常都被包裝在AggregateException中。

總結(jié)

以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論