C#之異步

C#之異步

在計算機中,一個線程就是一系列的命令,一個工做單元。操做系統能夠管理多個線程,給每一個線程分配cpu執行的時間片,而後切換不一樣的線程在這個cpu上執行。這種單核的處理器一次只能作一件事,不能同時作兩件以上的事情,只是經過時間的分配來實現多個線程的執行。可是在多核處理器上,能夠實現同時執行多個線程。操做系統能夠將時間分配給第一個處理器上的線程,而後在另外一個處理器上分配時間給另外一個線程。編程

異步是相對於同步而言。跟多線程不能同一而論。服務器

異步編程採用future或callback機制,以免產生沒必要要的線程。(一個future表明一個將要完成的工做。)異步編程核心就是:啓動了的操做將在一段時間後完成。這個操做正在執行時,不會阻塞原來的線程。啓動了這個操做的線程,能夠繼續執行其餘任務。當操做完成時,會通知它的future或者回調函數,以便讓程序知道操做已經結束。多線程

爲何要使用異步:異步

面向終端用戶的GUI程序:異步編程提升了相應能力。可使程序在執行任務時仍能相應用戶的輸入。
服務器端應用:實現了可擴展性。服務器應用能夠利用線程池知足其可擴展性。async


異步和同步的區別:

若是以同步方式執行某個任務時,須要等待該任務完成,而後才能再繼續執行另外一個任務。而用異步執行某個任務時,能夠在該任務完成以前執行另外一個任務。異步最重要的體現就是不排隊,不阻塞異步編程

圖:單線程同步
函數

圖:多線程同步
操作系統


異步跟多線程

異步能夠在單個線程上實現,也能夠在多個線程上實現,還能夠不須要線程(一些IO操做)。線程

圖:單線程異步
3d

圖:多線程異步


異步是否建立線程

異步能夠分爲CPU異步和IO異步。異步在CPU操做中是必需要跑在線程上的,通常狀況下這時咱們都會新開一個線程執行這個異步操做。但在IO操做中是不須要線程的,硬件直接和內存操做。
可是是否建立線程取決於你的異步的實現方式。好比在異步你用ThreadPool,Task.Run()等方法是建立了一個線程池的線程,那麼該異步是在另外一個線程上執行。


C#實現異步的四種方式:

  1. 異步模式BeginXXX,EndXXX
  2. 事件異步xxxAsync,xxxCompleted
  3. 基於任務Task的異步
  4. async,await關鍵字異步

異步模式

異步模式是調用Beginxxx方法,返回一個IAsyncResult類型的值,在回調函數裏調用Endxxxx(IAsyncResult)獲取結果值。

異步模式中最多見的是委託的異步。

如:聲明一個string類型輸入參數和string類型返回值的委託。調用委託的BeginInvoke方法,來異步執行該委託。

Func<string, string> func = (string str) =>
             {
                 Console.WriteLine(str);
                 return str + " end";
             };
            func.BeginInvoke("hello",IAsyncResult ar =>
            {
                Console.WriteLine(func.EndInvoke(ar));
            }, null);
//輸出:
//hello
//hello end

BeginInvoke方法的第一個參數表示委託的輸入參數。

第二個參數表示IAsyncResult類型輸入參數的回調函數,其實也是個委託。

第三個參數是個狀態值。


事件異步

事件異步有一個xxxAsync方法,和對應該方法的 xxxCompleted事件。

如: backgroundworkerprogressbar結合

public partial class MainWindow : Window
    {
        private BackgroundWorker bworker = new BackgroundWorker();
        public MainWindow()
        {
            InitializeComponent();
            //支持報告進度
            bworker.WorkerReportsProgress = true;
            //執行具體的方法
            bworker.DoWork += Bworker_DoWork;
            //進度變化時觸發的事件
            bworker.ProgressChanged += Bworker_ProgressChanged;
            //異步結束時觸發的事件
            bworker.RunWorkerCompleted += Bworker_RunWorkerCompleted;
            Loaded += MainWindow_Loaded;
        }

        private void MainWindow_Loaded(object sender, RoutedEventArgs e)
        {
            //開始異步執行
            bworker.RunWorkerAsync();
        }

        private void Bworker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            //異步完成時觸發的事件
            progressBar.value=100;
        }

        private void Bworker_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            //獲取進度值複製給progressBar
            progressBar.Value = e.ProgressPercentage;
        }

        private void Bworker_DoWork(object sender, DoWorkEventArgs e)
        {
            for (int j = 0; j <= 100; j++)
            {
                //調用進度變化方法,觸發進度變化事件
                bworker.ReportProgress(j);
                Thread.Sleep(100);
            }
        }
    }

Task模式的異步

Task是在Framework4.0提出來的新概念。Task自己就表示一個異步操做(Task默認是運行在線程池裏的線程上)。它比線程更輕量,能夠更高效的利用線程。而且任務提供了更多的控制操做。

  • 實現了控制任務執行順序
  • 實現父子任務
  • 實現了任務的取消操做
  • 實現了進度報告
  • 實現了返回值
  • 實現了隨時查看任務狀態

任務的執行默認是由任務調度器來實現的(任務調用器使這些任務並行執行)。任務的執行和線程不是一一對應的。有可能會是幾個任務在同一個線程上運行,充分利用了線程,避免一些短期的操做單獨跑在一個線程裏。因此任務更適合CPU密集型操做。

Task 啓動

任務能夠賦值當即運行,也能夠先由構造函數賦值,以後再調用。

//啓用線程池中的線程異步執行
 Task t1 = Task.Factory.StartNew(() =>
            {
                Console.WriteLine("Task啓動...");
            });
//啓用線程池中的線程異步執行
 Task t2 = Task.Run(() =>
            {
                Console.WriteLine("Task啓動...");
            });

 Task t3 = new Task(() =>
            {
                Console.WriteLine("Task啓動...");
            });
 t3.Start();//啓用線程池中的線程異步執行
 t3.RunSynchronously();//任務同步執行

Task 等待任務結果,處理結果

Task t1 = Task.Run(() =>
            {
                Console.WriteLine("Task啓動...");
            });
 Task t2 = Task.Run(() =>
            {
                Console.WriteLine("Task啓動...");
            });

 //調用WaitAll() ,會阻塞調用線程,等待任務執行完成 ,這時異步也沒有意義了          
 Task.WaitAll(new Task[] { t1, t2 });
 Console.WriteLine("Task完成...");

 //調用ContinueWith,等待任務完成,觸發下一個任務,這個任務可看成任務完成時觸發的回調函數。
 //爲了獲取結果,同時不阻塞調用線程,建議使用ContinueWith,在任務完成後,接着執行一個處理結果的任務。
t1.ContinueWith((t) =>
{
    Console.WriteLine("Task完成...");
});
t2.ContinueWith((t) =>
{
    Console.WriteLine("Task完成...");
});

//調用GetAwaiter()方法,獲取任務的等待者,調用OnCompleted事件,當任務完成時觸發
//調用OnCompleted事件也不會阻塞線程
t1.GetAwaiter().OnCompleted(() =>
{
    Console.WriteLine("Task完成...");
});
t2.GetAwaiter().OnCompleted(() =>
{
    Console.WriteLine("Task完成...");
});

Task 任務取消

//實例化一個取消實例
var source = new CancellationTokenSource();
var token = source.Token;

Task t1 = Task.Run(() =>
{
    Thread.Sleep(2000);
    //判斷是否任務取消
    if (token.IsCancellationRequested)
    {
        //token.ThrowIfCancellationRequested();
        Console.WriteLine("任務已取消");
    }
    Thread.Sleep(500);
    //token傳遞給任務
}, token);

Thread.Sleep(1000);
Console.WriteLine(t1.Status);
//取消該任務
source.Cancel();
Console.WriteLine(t1.Status);

Task 返回值

Task<string> t1 = Task.Run(() => TaskMethod("hello"));
t1.Wait();
Console.WriteLine(t1.Result);

public string TaskMethod(string str)
{
    return str + " from task method";
}

Task異步操做,須要注意的一點就是調用Waitxxx方法,會阻塞調用線程。


async await 異步

首先要明確一點的就是async await 不會建立線程。而且他們是一對關鍵字,必須成對的出現。

若是await的表達式沒有建立新的線程,那麼一個異步操做就是在調用線程的時間片上執行,不然就是在另外一個線程上執行。

async Task MethodAsync()
{
    Console.WriteLine("異步執行");
    await Task.Delay(4000); 
    Console.WriteLine("異步執行結束");
}

一個異步方法必須有async修飾,且方法名以Async結尾。異步方法體至少包含一個await表達式。await 能夠看做是一個掛起異步方法的一個點,且同時把控制權返回給調用者。異步方法的返回值必須是Task或者Task<T>。即若是方法沒有返回值那就用Task表示,若是有一個string類型的返回值,就用Task泛型Task<string>修飾。

異步方法執行流程:

  1. 主線程調用MethodAsync方法,並等待方法執行結束
  2. 異步方法開始執行,輸出「異步執行」
  3. 異步方法執行到await關鍵字,此時MethodAsync方法掛起,等待await表達式執行完畢,同時將控制權返回給調用方主線程,主線程繼續執行。
  4. 執行Task.Delay方法,同時主線程繼續執行以後的方法。
  5. Task.Delay結束,await表達式結束,MehtodAsync執行await表達式以後的語句,輸出「異步執行結束」。

和其餘方法同樣,async方法開始時以同步方式執行。在async內部,await關鍵字對它的參數執行一個異步等待。它首先檢查操做是否已經完成,若是完成了,就繼續運行(同步方式)。不然它會暫停async方法,並返回,留下一個未完成的Task。一段時間後,操做完成,async方法就恢復運行。

一個async方法是由多個同步執行的程序塊組成的,每一個同步程序塊之間由await語句分隔。第一個同步程序塊是在調用這個方法的線程中執行,但其餘同步程序塊在哪裏運行呢?狀況比較複雜。

最多見的狀況是用await語句等待一個任務完成,當該方法在await處暫停時,就能夠捕獲上下文(context)。若是當前SynchronizationContext不爲空,這個上下文就是當前SynchronizationContext。若是爲空,則這個上下文爲當前TaskScheduler。該方法會在這個上下文中繼續運行。通常來講,運行在UI線程時採用UI上下文,處理Asp.Net請求時採用Asp.Net請求上下文,其餘不少狀況下則採用線程池上下文。

由於,在上面的代碼中,每一個同步程序塊會試圖在原始的上下文中恢復運行。若是在UI線程調用async方法,該方法的每一個同步程序塊都將在此UI線程上運行。可是,若是在線程池中調用,每一個同步程序塊將在線程池上運行。

若是要避免這種行爲,能夠在await中使用configureAwait方法,將參數ContinueOnCapturedContext設置爲false。async方法中await以前的代碼會在調用的線程裏運行。在被await暫停後,await以後的代碼則會在線程池裏繼續運行。

async Task MethodAsync()
{
    Console.WriteLine("異步執行");//同步程序塊1
    await Task.Delay(4000).ConfigureAwait(false); 
    Console.WriteLine("異步執行結束");//同步程序塊2
}

咱們可能想固然的認爲Task.Delay會阻塞執行線程,就跟Thread.Sleep同樣。其實他們是不同的。Task.Delay建立一個將在設置時間後執行的任務。就至關於一個定時器,多少時間後再執行操做。不會阻塞執行線程。

當咱們在異步線程中調用Sleep的時候,只會阻塞異步線程。不會阻塞到主線程。

async Task Method2Async()
{
    Console.WriteLine("await執行前..."+Thread.CurrentThread.ManagedThreadId);
    await Task.Run(() =>
    {
        Console.WriteLine("await執行..." + Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(5000);
        Console.WriteLine("await執行結束..." + Thread.CurrentThread.ManagedThreadId);
        
    });
    Console.WriteLine("await以後執行..."+ Thread.CurrentThread.ManagedThreadId);
}

//輸出:
//await執行前...9
//await執行...12
//await以後執行...9
//await執行結束...12

上面的異步方法,Task建立了一個線程池線程,Thread.Sleep執行在線程池線程中。


參考:

相關文章
相關標籤/搜索