c# 異步編程


原文鏈接:https://blog.csdn.net/zuheyawen/article/details/99863588

前言
C#異步編程有幾種實現方式,異步方法就是其中的一種。異步方法是 C#5.0 才有的新特性,主要采用 async、await 關鍵字聲明為異步方法,完成對方法的異步調用。C#5.0 對應的 VS 版本是 VS2012,對應的 .NET Framework 版本是 v4.5,所以需要在此基礎上才支持。(否則可能報:找不到“async”修飾符所需的所有類型。目標框架版本是否不正確,或者缺少對程序集的引用?)

什么是異步方法
1. 異步方法,是指在執行當前方法的同時,可以異步的去調用其他方法(異步方法),並且不會阻塞當前方法的線程。
2. 使用了 async 修飾符的方法稱為異步方法,通常配合 await 運算符和 Task 異步任務一起使用。
1) 如果方法使用了 async 修飾符,則方法中需要包含一個以上 await 運算符,否則將以同步執行。
2) 反之,如果方法中包含一個以上 await 運算符,則必須聲明為一個異步方法,即使用 async 修飾符。

3. Task 分為兩種:
1) Task,表示可以執行一個異步操作,聲明如下:
public class Task : IAsyncResult, IDisposable { }
2) Task<TResult>,表示可以執行帶有返回值的異步操作,聲明如下:
public class Task<TResult> : Task { }

4. 異步方法的返回類型必須為 void、Task、Task<TResult> 中的其中一種。
1) void,表示無返回值,不關心異步方法執行后的結果,一般用於僅僅執行某一項任務,但是不關心結果的場景。
2) Task,表示異步方法將返回一個 Task 對象,該對象通常用於判斷異步任務是否已經完成,可以使用 taskObj.Wait() 方法等待,或者 taskObj.IsCompleted 判斷。
3) Task<TResult>,表示異步方法將返回一個 Task<TResult> 對象,該對象的 Result 屬性則是異步方法的執行結果,調用該屬性時將阻塞當前線程(異步方法未執行完成時)。

歸納一下:void 不關心結果;Task 只關心是否執行完成;Task<TResult> 不止關心是否執行完成,還要獲取執行結果。

下面通過幾個生活中比較形象的例子來理解異步方法的使用
1. 模擬扔垃圾(不關心結果,返回 void 類型)

/// <summary>
/// 扔垃圾
/// </summary>
public void DropLitter()
{
    Console.WriteLine("老婆開始打掃房間,線程Id為:{0}", GetThreadId());
    Console.WriteLine("垃圾滿了,快去扔垃圾");
    CommandDropLitter();
    Console.WriteLine("不管他繼續打掃,線程Id為:{0}", GetThreadId());
    Thread.Sleep(100);
    Console.WriteLine("老婆把房間打掃好了,線程Id為:{0}", GetThreadId());
}
 
/// <summary>
/// 通知我去扔垃圾
/// </summary>
public async void CommandDropLitter()
{
    Console.WriteLine("這時我准備去扔垃圾,線程Id為:{0}", GetThreadId());
    await Task.Run(() =>
    {
        Console.WriteLine("屁顛屁顛的去扔垃圾,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
    });
    Console.WriteLine("垃圾扔了還有啥吩咐,線程Id為:{0}", GetThreadId());
}

運行以上代碼:

以上代碼在 CommandDropLitter() 方法上加了 async 修飾符,並且使用 await 運算符開啟了一個新的 Task 去執行另一個任務。注意:當前線程遇到 await 時,則立刻跳回調用方法繼續往下執行。而 Task 執行完成之后將執行 await 之后的代碼,並且與 await 之前的線程不是同一個。

2.模擬打開電源開關(關心是否執行完成,返回 Task 類型)

/// <summary>
/// 打開電源開關
/// </summary>
public void OpenMainsSwitch()
{
    Console.WriteLine("我和老婆正在看電視,線程Id為:{0}", GetThreadId());
    Console.WriteLine("突然停電了,快去看下是不是跳閘了");
    Task task = CommandOpenMainsSwitch();
    Console.WriteLine("沒電了先玩會兒手機吧,線程Id為:{0}", GetThreadId());
    Thread.Sleep(100);
    Console.WriteLine("手機也沒電了只等電源打開,線程Id為:{0}", GetThreadId());
 
    //task.Wait();    //所以這里將被阻塞,直到任務完成
    //或者
    while (!task.IsCompleted) { Thread.Sleep(100); }
 
    Console.WriteLine("又有電了我們繼續看電視,線程Id為:{0}", GetThreadId());
}
 
/// <summary>
/// 通知我去打開電源開關
/// </summary>
public async Task CommandOpenMainsSwitch()
{
    Console.WriteLine("這時我准備去打開電源開關,線程Id為:{0}", GetThreadId());
    await Task.Run(() =>
    {
        Console.WriteLine("屁顛屁顛的去打開電源開關,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
    });
 
    Console.WriteLine("電源開關打開了,線程Id為:{0}", GetThreadId());
}

運行以上代碼:

1) 可見,調用 Wait() 方法后,當前線程被阻塞了,直到 Task 執行完成后,當前線程才繼續執行。
2) 注意:由於 CommandOpenMainsSwitch() 是一個異步方法,雖然返回類型為 Task 類型,但是在我們代碼中並沒有寫(也不能寫) return task 語句,這是為什么呢?可能是這種返回類型比較特殊,或者編譯器自動幫我們完成了吧!就算寫也只能寫 return 語句,后面不能跟對象表達式。

3. 模擬去買鹽(不止關心是否執行完成,還要獲取執行結果。返回 Task<TResult> 類型)

/// <summary>
/// 做飯
/// </summary>
public void CookDinner()
{
    Console.WriteLine("老婆開始做飯,線程Id為:{0}", GetThreadId());
    Console.WriteLine("哎呀,沒鹽了");
    Task<string> task = CommandBuySalt();
    Console.WriteLine("不管他繼續炒菜,線程Id為:{0}", GetThreadId());
    Thread.Sleep(100);
    string result = task.Result;    //必須要用鹽了,等我把鹽回來(停止炒菜(阻塞線程))
    Console.WriteLine("用了鹽炒的菜就是好吃【{0}】,線程Id為:{1}", result, GetThreadId());
    Console.WriteLine("老婆把飯做好了,線程Id為:{0}", GetThreadId());
}
 
/// <summary>
/// 通知我去買鹽
/// </summary>
public async Task<string> CommandBuySalt()
{
    Console.WriteLine("這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
 
    string result = await Task.Run(() =>
    {
        Console.WriteLine("屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
        return "鹽買回來了,順便我還買了一包煙";
 
    });
 
    Console.WriteLine("{0},線程Id為:{1}", result, GetThreadId());
 
    return result;
}

運行以上代碼:

1) 以上代碼 task.Result 會阻塞當前線程,與 task.Wait() 類似。
2) 注意:與前面返回類型為 Task 的 CommandOpenMainsSwitch() 方法一樣,雖然 CommandBuySalt() 方法返回類型為 Task<string>,但是我們的返回語句是 return 字符串。

其他示例
1. 在前面(模擬去買鹽)的示例中,異步方法中只開啟了一個 Task,如果開啟多個 Task 又是什么情況,看代碼:

public void AsyncTest()
{
    Console.WriteLine("AsyncTest() 方法開始執行,線程Id為:{0}", GetThreadId());
    Task task = Test1();
    Console.WriteLine("AsyncTest() 方法繼續執行,線程Id為:{0}", GetThreadId());
    task.Wait();
    Console.WriteLine("AsyncTest() 方法結束執行,線程Id為:{0}", GetThreadId());
}
 
public async Task Test1()
{
    Console.WriteLine("Test1() 方法開始執行,線程Id為:{0}", GetThreadId());
    await Task.Factory.StartNew((state) =>
    {
        Console.WriteLine("Test1() 方法中的 {0} 開始執行,線程Id為:{1}", state, GetThreadId());
        Thread.Sleep(1000);
        Console.WriteLine("Test1() 方法中的 {0} 結束執行,線程Id為:{1}", state, GetThreadId());
    }, "task1");
 
    await Task.Factory.StartNew((state) =>
    {
        Console.WriteLine("Test1() 方法中的 {0} 開始執行,線程Id為:{1}", state, GetThreadId());
        Thread.Sleep(3000);
        Console.WriteLine("Test1() 方法中的 {0} 結束執行,線程Id為:{1}", state, GetThreadId());
    }, "task2");
 
    Console.WriteLine("Test1() 方法結束執行,線程Id為:{0}", GetThreadId());
}

運行以上代碼:

當異步方法中有多個 await 時,會依次執行所有的 Task,只有當所有 Task 執行完成后才表示異步方法執行完成,當前線程才得以執行。

2. 同樣以前面(模擬去買鹽)的示例,如果發現其實家里還有鹽,這是就要告訴我不用買了(取消異步操作),怎么實現?這就要借助 System.Threading.CancellationTokenSource 和 System.Threading.Tasks.CancellationToken 對象來完成。

/// <summary>
/// 做飯(買鹽任務取消)
/// </summary>
public void CookDinner_CancelBuySalt()
{
    Console.WriteLine("老婆開始做飯,線程Id為:{0}", GetThreadId());
    Console.WriteLine("哎呀,沒鹽了");
    CancellationTokenSource source = new CancellationTokenSource();
    Task<string> task = CommandBuySalt_CancelBuySalt(source.Token);
    Console.WriteLine("不管他繼續炒菜,線程Id為:{0}", GetThreadId());
    Thread.Sleep(100);
 
    string result = "家里的鹽";
    if (!string.IsNullOrEmpty(result))
    {
        source.Cancel();    //傳達取消請求
        Console.WriteLine("家里還有鹽不用買啦,線程Id為:{0}", GetThreadId());
    }
    else
    {
        //如果已取消就不能再獲得結果了(否則將拋出 System.Threading.Tasks.TaskCanceledException 異常)
        //你都叫我不要買了,我拿什么給你?
        result = task.Result;
    }
 
    Console.WriteLine("既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", result, GetThreadId());
    Console.WriteLine("老婆把飯做好了,線程Id為:{0}", GetThreadId());
    Console.WriteLine("最終的任務狀態是:{0},已完成:{1},已取消:{2},已失敗:{3}",
        task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
}
 
/// <summary>
/// 通知我去買鹽(又告訴我不用買了)
/// </summary>
public async Task<string> CommandBuySalt_CancelBuySalt(CancellationToken token)
{
    Console.WriteLine("這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
 
    //已開始執行的任務不能被取消
    string result = await Task.Run(() =>
    {
        Console.WriteLine("屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
    }, token).ContinueWith((t) =>  //若沒有取消就繼續執行
    {
        Console.WriteLine("鹽已經買好了,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
 
        return "鹽買回來了,順便我還買了一包煙";
    }, token);
 
    Console.WriteLine("{0},線程Id為:{1}", result, GetThreadId());
 
    return result;
}

運行以上代碼:

1) 剛開始我以為調用 source.Cancel() 方法后會立即取消 Task 的執行,仔細一想也不太可能。如果需要在 Task 執行前或者執行期間完成取消操作,我們自己寫代碼判斷 cancellationToken.IsCancellationRequested 屬性是否為 true(該屬性在調用 source.Cancel() 后或者 source.CancelAfter() 方法到達指定時間后為 true),如果為 true 結束執行即可。
2) 這里所說的“傳達取消請求”的意思是,每個 Task 在執行之前都會檢查 cancellationToken.IsCancellationRequested 屬性是否為 true,如果為 true 則不執行 Task,並將設置 Status、IsCompleted、IsCanceled 等。
3) 所以,在 Task 的源碼中有這樣一段代碼

if (cancellationToken.IsCancellationRequested)
{
    // Fast path for an already-canceled cancellationToken
    this.InternalCancel(false);
}

3. 乘熱打鐵,我們再來看看多個 CancellationTokenSource 取消異步任務,以及注冊取消后的回調委托方法,繼續以(模擬去買鹽)為例:

/// <summary>
/// 做飯(多個消息傳達買鹽任務取消)
/// </summary>
public void CookDinner_MultiCancelBuySalt()
{
    Console.WriteLine("老婆開始做飯,線程Id為:{0}", GetThreadId());
    Console.WriteLine("哎呀,沒鹽了");
    CancellationTokenSource source1 = new CancellationTokenSource();    //因為存在而取消
    CancellationTokenSource source2 = new CancellationTokenSource();    //因為放棄而取消
 
    CancellationTokenSource source = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
 
    //注冊取消時的回調委托
    source1.Token.Register(() =>
    {
        Console.WriteLine("這是因為{0}所以取消,線程Id為:{1}", "家里還有鹽", GetThreadId());
    });
 
    source2.Token.Register((state) =>
    {
        Console.WriteLine("這是因為{0}所以取消,線程Id為:{1}", state, GetThreadId());
    }, "不做了出去吃");
 
    source.Token.Register((state) =>
    {
        Console.WriteLine("這是因為{0}所以取消,線程Id為:{1}", state, GetThreadId());
    }, "沒理由");
 
    //這里必須傳遞 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象
    Task<string> task = CommandBuySalt_MultiCancelBuySalt(source.Token);
 
    Console.WriteLine("等等,好像不用買了,線程Id為:{0}", GetThreadId());
    Thread.Sleep(100);
 
    string[] results = new string[] { "家里的鹽", "不做了出去吃", "沒理由" };
    Random r = new Random();
    switch (r.Next(1, 4))
    {
        case 1:
            source1.Cancel();           //傳達取消請求(家里有鹽)
            //source1.CancelAfter(3000);  //3s后才調用取消的回調方法
            Console.WriteLine("既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", results[0], GetThreadId());
            break;
        case 2:
            source2.Cancel();           //傳達取消請求(不做了出去吃)
            //source2.CancelAfter(3000);  //3s后才調用取消的回調方法
            Console.WriteLine("我們出去吃不用買啦【{0}】,線程Id為:{1}", results[1], GetThreadId());
            break;
        case 3:
            source.Cancel();            //傳達取消請求(沒理由)
            //source.CancelAfter(3000);   //3s后才調用取消的回調方法
            Console.WriteLine("沒理由就是不用買啦【{0}】,線程Id為:{1}", results[2], GetThreadId());
            break;
    }
 
    Console.WriteLine("最終的任務狀態是:{0},已完成:{1},已取消:{2},已失敗:{3}",
        task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
}
 
/// <summary>
/// 通知我去買鹽(又告訴我不用買了,各種理由)
/// </summary>
public async Task<string> CommandBuySalt_MultiCancelBuySalt(CancellationToken token)
{
    Console.WriteLine("這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
 
    //已開始執行的任務不能被取消
    string result = await Task.Run(() =>
    {
        Console.WriteLine("屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
    }, token).ContinueWith((t) =>  //若沒有取消就繼續執行
    {
        Console.WriteLine("鹽已經買好了,線程Id為:{0}", GetThreadId());
        Thread.Sleep(1000);
        return "鹽買回來了,順便我還買了一包煙";
    }, token);
 
    Console.WriteLine("{0},線程Id為:{1}", result, GetThreadId());
 
    return result;
}

運行以上代碼:

1)   當調用 source.Cancel() 方法后,會立即取消並調用 token 注冊的回調方法;而調用 existSource.CancelAfter() 方法則會等到達指定的毫秒數后才會取消。
2)   注意:傳遞給異步方法的 token 對象,必須是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象,否則取消將無效。
3)   回調的委托方法始終只有兩個,一個是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象的注冊委托,另一個是調用 Cancel()/CancelAfter() 方法的 Token 對象的注冊委托。
4)   如果以上代碼調用的是 CancelAfter(3000) 方法,運行結果如下:

原文鏈接: https://www.cnblogs.com/abeam/p/11227935.html

前言
C#異步編程有幾種實現方式,異步方法就是其中的一種。異步方法是 C#5.0 才有的新特性,主要采用 async、await 關鍵字聲明為異步方法,完成對方法的異步調用。C#5.0 對應的 VS 版本是 VS2012,對應的 .NET Framework 版本是 v4.5,所以需要在此基礎上才支持。(否則可能報:找不到“async”修飾符所需的所有類型。目標框架版本是否不正確,或者缺少對程序集的引用?)

什么是異步方法
1. 異步方法,是指在執行當前方法的同時,可以異步的去調用其他方法(異步方法),並且不會阻塞當前方法的線程。
2. 使用了 async 修飾符的方法稱為異步方法,通常配合 await 運算符和 Task 異步任務一起使用。
1) 如果方法使用了 async 修飾符,則方法中需要包含一個以上 await 運算符,否則將以同步執行。
2) 反之,如果方法中包含一個以上 await 運算符,則必須聲明為一個異步方法,即使用 async 修飾符。

3. Task 分為兩種:
1) Task,表示可以執行一個異步操作,聲明如下:
public class Task : IAsyncResult, IDisposable { }
2) Task<TResult>,表示可以執行帶有返回值的異步操作,聲明如下:
public class Task<TResult> : Task { }

4. 異步方法的返回類型必須為 void、Task、Task<TResult> 中的其中一種。
1) void,表示無返回值,不關心異步方法執行后的結果,一般用於僅僅執行某一項任務,但是不關心結果的場景。
2) Task,表示異步方法將返回一個 Task 對象,該對象通常用於判斷異步任務是否已經完成,可以使用 taskObj.Wait() 方法等待,或者 taskObj.IsCompleted 判斷。
3) Task<TResult>,表示異步方法將返回一個 Task<TResult> 對象,該對象的 Result 屬性則是異步方法的執行結果,調用該屬性時將阻塞當前線程(異步方法未執行完成時)。

歸納一下:void 不關心結果;Task 只關心是否執行完成;Task<TResult> 不止關心是否執行完成,還要獲取執行結果。

下面通過幾個生活中比較形象的例子來理解異步方法的使用
1. 模擬扔垃圾(不關心結果,返回 void 類型)

  1.  
    /// <summary>
  2.  
    /// 扔垃圾
  3.  
    /// </summary>
  4.  
    public void DropLitter()
  5.  
    {
  6.  
        Console.WriteLine( "老婆開始打掃房間,線程Id為:{0}", GetThreadId());
  7.  
        Console.WriteLine( "垃圾滿了,快去扔垃圾");
  8.  
        CommandDropLitter();
  9.  
        Console.WriteLine( "不管他繼續打掃,線程Id為:{0}", GetThreadId());
  10.  
        Thread.Sleep( 100);
  11.  
        Console.WriteLine( "老婆把房間打掃好了,線程Id為:{0}", GetThreadId());
  12.  
    }
  13.  
     
  14.  
    /// <summary>
  15.  
    /// 通知我去扔垃圾
  16.  
    /// </summary>
  17.  
    public async void CommandDropLitter()
  18.  
    {
  19.  
        Console.WriteLine( "這時我准備去扔垃圾,線程Id為:{0}", GetThreadId());
  20.  
         await Task.Run(() =>
  21.  
        {
  22.  
            Console.WriteLine( "屁顛屁顛的去扔垃圾,線程Id為:{0}", GetThreadId());
  23.  
            Thread.Sleep( 1000);
  24.  
        });
  25.  
        Console.WriteLine( "垃圾扔了還有啥吩咐,線程Id為:{0}", GetThreadId());
  26.  
    }

運行以上代碼:

以上代碼在 CommandDropLitter() 方法上加了 async 修飾符,並且使用 await 運算符開啟了一個新的 Task 去執行另一個任務。注意:當前線程遇到 await 時,則立刻跳回調用方法繼續往下執行。而 Task 執行完成之后將執行 await 之后的代碼,並且與 await 之前的線程不是同一個。

2.模擬打開電源開關(關心是否執行完成,返回 Task 類型)

  1.  
    /// <summary>
  2.  
    /// 打開電源開關
  3.  
    /// </summary>
  4.  
    public void OpenMainsSwitch()
  5.  
    {
  6.  
        Console.WriteLine( "我和老婆正在看電視,線程Id為:{0}", GetThreadId());
  7.  
        Console.WriteLine( "突然停電了,快去看下是不是跳閘了");
  8.  
        Task task = CommandOpenMainsSwitch();
  9.  
        Console.WriteLine( "沒電了先玩會兒手機吧,線程Id為:{0}", GetThreadId());
  10.  
        Thread.Sleep( 100);
  11.  
        Console.WriteLine( "手機也沒電了只等電源打開,線程Id為:{0}", GetThreadId());
  12.  
     
  13.  
         //task.Wait();    //所以這里將被阻塞,直到任務完成
  14.  
         //或者
  15.  
         while (!task.IsCompleted) { Thread.Sleep( 100); }
  16.  
     
  17.  
        Console.WriteLine( "又有電了我們繼續看電視,線程Id為:{0}", GetThreadId());
  18.  
    }
  19.  
     
  20.  
    /// <summary>
  21.  
    /// 通知我去打開電源開關
  22.  
    /// </summary>
  23.  
    public async Task CommandOpenMainsSwitch()
  24.  
    {
  25.  
        Console.WriteLine( "這時我准備去打開電源開關,線程Id為:{0}", GetThreadId());
  26.  
         await Task.Run(() =>
  27.  
        {
  28.  
            Console.WriteLine( "屁顛屁顛的去打開電源開關,線程Id為:{0}", GetThreadId());
  29.  
            Thread.Sleep( 1000);
  30.  
        });
  31.  
     
  32.  
        Console.WriteLine( "電源開關打開了,線程Id為:{0}", GetThreadId());
  33.  
    }

運行以上代碼:

1) 可見,調用 Wait() 方法后,當前線程被阻塞了,直到 Task 執行完成后,當前線程才繼續執行。
2) 注意:由於 CommandOpenMainsSwitch() 是一個異步方法,雖然返回類型為 Task 類型,但是在我們代碼中並沒有寫(也不能寫) return task 語句,這是為什么呢?可能是這種返回類型比較特殊,或者編譯器自動幫我們完成了吧!就算寫也只能寫 return 語句,后面不能跟對象表達式。

3. 模擬去買鹽(不止關心是否執行完成,還要獲取執行結果。返回 Task<TResult> 類型)

  1.  
    /// <summary>
  2.  
    /// 做飯
  3.  
    /// </summary>
  4.  
    public void CookDinner()
  5.  
    {
  6.  
        Console.WriteLine( "老婆開始做飯,線程Id為:{0}", GetThreadId());
  7.  
        Console.WriteLine( "哎呀,沒鹽了");
  8.  
        Task< string> task = CommandBuySalt();
  9.  
        Console.WriteLine( "不管他繼續炒菜,線程Id為:{0}", GetThreadId());
  10.  
        Thread.Sleep( 100);
  11.  
         string result = task.Result;     //必須要用鹽了,等我把鹽回來(停止炒菜(阻塞線程))
  12.  
        Console.WriteLine( "用了鹽炒的菜就是好吃【{0}】,線程Id為:{1}", result, GetThreadId());
  13.  
        Console.WriteLine( "老婆把飯做好了,線程Id為:{0}", GetThreadId());
  14.  
    }
  15.  
     
  16.  
    /// <summary>
  17.  
    /// 通知我去買鹽
  18.  
    /// </summary>
  19.  
    public async Task<string> CommandBuySalt()
  20.  
    {
  21.  
        Console.WriteLine( "這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
  22.  
     
  23.  
         string result = await Task.Run(() =>
  24.  
        {
  25.  
            Console.WriteLine( "屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
  26.  
            Thread.Sleep( 1000);
  27.  
             return "鹽買回來了,順便我還買了一包煙";
  28.  
     
  29.  
        });
  30.  
     
  31.  
        Console.WriteLine( "{0},線程Id為:{1}", result, GetThreadId());
  32.  
     
  33.  
         return result;
  34.  
    }

運行以上代碼:

1) 以上代碼 task.Result 會阻塞當前線程,與 task.Wait() 類似。
2) 注意:與前面返回類型為 Task 的 CommandOpenMainsSwitch() 方法一樣,雖然 CommandBuySalt() 方法返回類型為 Task<string>,但是我們的返回語句是 return 字符串。

其他示例
1. 在前面(模擬去買鹽)的示例中,異步方法中只開啟了一個 Task,如果開啟多個 Task 又是什么情況,看代碼:

  1.  
    public void AsyncTest()
  2.  
    {
  3.  
        Console.WriteLine( "AsyncTest() 方法開始執行,線程Id為:{0}", GetThreadId());
  4.  
        Task task = Test1();
  5.  
        Console.WriteLine( "AsyncTest() 方法繼續執行,線程Id為:{0}", GetThreadId());
  6.  
        task.Wait();
  7.  
        Console.WriteLine( "AsyncTest() 方法結束執行,線程Id為:{0}", GetThreadId());
  8.  
    }
  9.  
     
  10.  
    public async Task Test1()
  11.  
    {
  12.  
        Console.WriteLine( "Test1() 方法開始執行,線程Id為:{0}", GetThreadId());
  13.  
         await Task.Factory.StartNew((state) =>
  14.  
        {
  15.  
            Console.WriteLine( "Test1() 方法中的 {0} 開始執行,線程Id為:{1}", state, GetThreadId());
  16.  
            Thread.Sleep( 1000);
  17.  
            Console.WriteLine( "Test1() 方法中的 {0} 結束執行,線程Id為:{1}", state, GetThreadId());
  18.  
        }, "task1");
  19.  
     
  20.  
         await Task.Factory.StartNew((state) =>
  21.  
        {
  22.  
            Console.WriteLine( "Test1() 方法中的 {0} 開始執行,線程Id為:{1}", state, GetThreadId());
  23.  
            Thread.Sleep( 3000);
  24.  
            Console.WriteLine( "Test1() 方法中的 {0} 結束執行,線程Id為:{1}", state, GetThreadId());
  25.  
        }, "task2");
  26.  
     
  27.  
        Console.WriteLine( "Test1() 方法結束執行,線程Id為:{0}", GetThreadId());
  28.  
    }

運行以上代碼:

當異步方法中有多個 await 時,會依次執行所有的 Task,只有當所有 Task 執行完成后才表示異步方法執行完成,當前線程才得以執行。

2. 同樣以前面(模擬去買鹽)的示例,如果發現其實家里還有鹽,這是就要告訴我不用買了(取消異步操作),怎么實現?這就要借助 System.Threading.CancellationTokenSource 和 System.Threading.Tasks.CancellationToken 對象來完成。

  1.  
    /// <summary>
  2.  
    /// 做飯(買鹽任務取消)
  3.  
    /// </summary>
  4.  
    public void CookDinner_CancelBuySalt()
  5.  
    {
  6.  
        Console.WriteLine( "老婆開始做飯,線程Id為:{0}", GetThreadId());
  7.  
        Console.WriteLine( "哎呀,沒鹽了");
  8.  
        CancellationTokenSource source = new CancellationTokenSource();
  9.  
        Task< string> task = CommandBuySalt_CancelBuySalt(source.Token);
  10.  
        Console.WriteLine( "不管他繼續炒菜,線程Id為:{0}", GetThreadId());
  11.  
        Thread.Sleep( 100);
  12.  
     
  13.  
         string result = "家里的鹽";
  14.  
         if (! string.IsNullOrEmpty(result))
  15.  
        {
  16.  
            source.Cancel();     //傳達取消請求
  17.  
            Console.WriteLine( "家里還有鹽不用買啦,線程Id為:{0}", GetThreadId());
  18.  
        }
  19.  
         else
  20.  
        {
  21.  
             //如果已取消就不能再獲得結果了(否則將拋出 System.Threading.Tasks.TaskCanceledException 異常)
  22.  
             //你都叫我不要買了,我拿什么給你?
  23.  
            result = task.Result;
  24.  
        }
  25.  
     
  26.  
        Console.WriteLine( "既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", result, GetThreadId());
  27.  
        Console.WriteLine( "老婆把飯做好了,線程Id為:{0}", GetThreadId());
  28.  
        Console.WriteLine( "最終的任務狀態是:{0},已完成:{1},已取消:{2},已失敗:{3}",
  29.  
            task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
  30.  
    }
  31.  
     
  32.  
    /// <summary>
  33.  
    /// 通知我去買鹽(又告訴我不用買了)
  34.  
    /// </summary>
  35.  
    public async Task<string> CommandBuySalt_CancelBuySalt(CancellationToken token)
  36.  
    {
  37.  
        Console.WriteLine( "這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
  38.  
     
  39.  
         //已開始執行的任務不能被取消
  40.  
         string result = await Task.Run(() =>
  41.  
        {
  42.  
            Console.WriteLine( "屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
  43.  
            Thread.Sleep( 1000);
  44.  
        }, token).ContinueWith((t) =>   //若沒有取消就繼續執行
  45.  
        {
  46.  
            Console.WriteLine( "鹽已經買好了,線程Id為:{0}", GetThreadId());
  47.  
            Thread.Sleep( 1000);
  48.  
     
  49.  
             return "鹽買回來了,順便我還買了一包煙";
  50.  
        }, token);
  51.  
     
  52.  
        Console.WriteLine( "{0},線程Id為:{1}", result, GetThreadId());
  53.  
     
  54.  
         return result;
  55.  
    }

運行以上代碼:

1) 剛開始我以為調用 source.Cancel() 方法后會立即取消 Task 的執行,仔細一想也不太可能。如果需要在 Task 執行前或者執行期間完成取消操作,我們自己寫代碼判斷 cancellationToken.IsCancellationRequested 屬性是否為 true(該屬性在調用 source.Cancel() 后或者 source.CancelAfter() 方法到達指定時間后為 true),如果為 true 結束執行即可。
2) 這里所說的“傳達取消請求”的意思是,每個 Task 在執行之前都會檢查 cancellationToken.IsCancellationRequested 屬性是否為 true,如果為 true 則不執行 Task,並將設置 Status、IsCompleted、IsCanceled 等。
3) 所以,在 Task 的源碼中有這樣一段代碼

  1.  
    if (cancellationToken.IsCancellationRequested)
  2.  
    {
  3.  
        // Fast path for an already-canceled cancellationToken
  4.  
        this.InternalCancel( false);
  5.  
    }

3. 乘熱打鐵,我們再來看看多個 CancellationTokenSource 取消異步任務,以及注冊取消后的回調委托方法,繼續以(模擬去買鹽)為例:

  1.  
    /// <summary>
  2.  
    /// 做飯(多個消息傳達買鹽任務取消)
  3.  
    /// </summary>
  4.  
    public void CookDinner_MultiCancelBuySalt()
  5.  
    {
  6.  
        Console.WriteLine( "老婆開始做飯,線程Id為:{0}", GetThreadId());
  7.  
        Console.WriteLine( "哎呀,沒鹽了");
  8.  
        CancellationTokenSource source1 = new CancellationTokenSource();     //因為存在而取消
  9.  
        CancellationTokenSource source2 = new CancellationTokenSource();     //因為放棄而取消
  10.  
     
  11.  
        CancellationTokenSource source = CancellationTokenSource.CreateLinkedTokenSource(source1.Token, source2.Token);
  12.  
     
  13.  
         //注冊取消時的回調委托
  14.  
        source1.Token.Register(() =>
  15.  
        {
  16.  
            Console.WriteLine( "這是因為{0}所以取消,線程Id為:{1}", "家里還有鹽", GetThreadId());
  17.  
        });
  18.  
     
  19.  
        source2.Token.Register((state) =>
  20.  
        {
  21.  
            Console.WriteLine( "這是因為{0}所以取消,線程Id為:{1}", state, GetThreadId());
  22.  
        }, "不做了出去吃");
  23.  
     
  24.  
        source.Token.Register((state) =>
  25.  
        {
  26.  
            Console.WriteLine( "這是因為{0}所以取消,線程Id為:{1}", state, GetThreadId());
  27.  
        }, "沒理由");
  28.  
     
  29.  
         //這里必須傳遞 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象
  30.  
        Task< string> task = CommandBuySalt_MultiCancelBuySalt(source.Token);
  31.  
     
  32.  
        Console.WriteLine( "等等,好像不用買了,線程Id為:{0}", GetThreadId());
  33.  
        Thread.Sleep( 100);
  34.  
     
  35.  
         string[] results = new string[] { "家里的鹽", "不做了出去吃", "沒理由" };
  36.  
        Random r = new Random();
  37.  
         switch (r.Next( 1, 4))
  38.  
        {
  39.  
             case 1:
  40.  
                source1.Cancel();           //傳達取消請求(家里有鹽)
  41.  
                 //source1.CancelAfter(3000);  //3s后才調用取消的回調方法
  42.  
                Console.WriteLine( "既然有鹽我就繼續炒菜【{0}】,線程Id為:{1}", results[ 0], GetThreadId());
  43.  
                 break;
  44.  
             case 2:
  45.  
                source2.Cancel();           //傳達取消請求(不做了出去吃)
  46.  
                 //source2.CancelAfter(3000);  //3s后才調用取消的回調方法
  47.  
                Console.WriteLine( "我們出去吃不用買啦【{0}】,線程Id為:{1}", results[ 1], GetThreadId());
  48.  
                 break;
  49.  
             case 3:
  50.  
                source.Cancel();             //傳達取消請求(沒理由)
  51.  
                 //source.CancelAfter(3000);   //3s后才調用取消的回調方法
  52.  
                Console.WriteLine( "沒理由就是不用買啦【{0}】,線程Id為:{1}", results[ 2], GetThreadId());
  53.  
                 break;
  54.  
        }
  55.  
     
  56.  
        Console.WriteLine( "最終的任務狀態是:{0},已完成:{1},已取消:{2},已失敗:{3}",
  57.  
            task.Status, task.IsCompleted, task.IsCanceled, task.IsFaulted);
  58.  
    }
  59.  
     
  60.  
    /// <summary>
  61.  
    /// 通知我去買鹽(又告訴我不用買了,各種理由)
  62.  
    /// </summary>
  63.  
    public async Task<string> CommandBuySalt_MultiCancelBuySalt(CancellationToken token)
  64.  
    {
  65.  
        Console.WriteLine( "這時我准備去買鹽了,線程Id為:{0}", GetThreadId());
  66.  
     
  67.  
         //已開始執行的任務不能被取消
  68.  
         string result = await Task.Run(() =>
  69.  
        {
  70.  
            Console.WriteLine( "屁顛屁顛的去買鹽,線程Id為:{0}", GetThreadId());
  71.  
            Thread.Sleep( 1000);
  72.  
        }, token).ContinueWith((t) =>   //若沒有取消就繼續執行
  73.  
        {
  74.  
            Console.WriteLine( "鹽已經買好了,線程Id為:{0}", GetThreadId());
  75.  
            Thread.Sleep( 1000);
  76.  
             return "鹽買回來了,順便我還買了一包煙";
  77.  
        }, token);
  78.  
     
  79.  
        Console.WriteLine( "{0},線程Id為:{1}", result, GetThreadId());
  80.  
     
  81.  
         return result;
  82.  
    }

運行以上代碼:

1)   當調用 source.Cancel() 方法后,會立即取消並調用 token 注冊的回調方法;而調用 existSource.CancelAfter() 方法則會等到達指定的毫秒數后才會取消。
2)   注意:傳遞給異步方法的 token 對象,必須是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象,否則取消將無效。
3)   回調的委托方法始終只有兩個,一個是 CancellationTokenSource.CreateLinkedTokenSource() 方法返回的 Token 對象的注冊委托,另一個是調用 Cancel()/CancelAfter() 方法的 Token 對象的注冊委托。
4)   如果以上代碼調用的是 CancelAfter(3000) 方法,運行結果如下:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM