C#之非同步

JoeSnail發表於2017-08-06

C#之非同步

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

非同步是相對於同步而言。跟多執行緒不能同一而論。

非同步程式設計採用future或callback機制,以避免產生不必要的執行緒。(一個future代表一個將要完成的工作。)非同步程式設計核心就是:啟動了的操作將在一段時間後完成。這個操作正在執行時,不會阻塞原來的執行緒。啟動了這個操作的執行緒,可以繼續執行其他任務。當操作完成時,會通知它的future或者回撥函式,以便讓程式知道操作已經結束。

為什麼要使用非同步:

面向終端使用者的GUI程式:非同步程式設計提高了相應能力。可以使程式在執行任務時仍能相應使用者的輸入。
伺服器端應用:實現了可擴充套件性。伺服器應用可以利用執行緒池滿足其可擴充套件性。


非同步和同步的區別:

如果以同步方式執行某個任務時,需要等待該任務完成,然後才能再繼續執行另一個任務。而用非同步執行某個任務時,可以在該任務完成之前執行另一個任務。非同步最重要的體現就是不排隊,不阻塞

圖:單執行緒同步
C#之非同步

圖:多執行緒同步
C#之非同步


非同步跟多執行緒

非同步可以在單個執行緒上實現,也可以在多個執行緒上實現,還可以不需要執行緒(一些IO操作)。

圖:單執行緒非同步
C#之非同步

圖:多執行緒非同步
C#之非同步


非同步是否建立執行緒

非同步可以分為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執行線上程池執行緒中。


參考:

相關文章