C#之異步


C#之異步

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

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

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

為什么要使用異步:

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


異步和同步的區別:

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

圖:單線程同步

圖:多線程同步


異步跟多線程

異步可以在單個線程上實現,也可以在多個線程上實現,還可以不需要線程(一些IO操作)。

圖:單線程異步

圖:多線程異步


異步是否創建線程

異步可以分為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執行在線程池線程中。


參考:


免責聲明!

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



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