C#之非同步
在計算機中,一個執行緒就是一系列的命令,一個工作單元。作業系統可以管理多個執行緒,給每個執行緒分配cpu執行的時間片,然後切換不同的執行緒在這個cpu上執行。這種單核的處理器一次只能做一件事,不能同時做兩件以上的事情,只是通過時間的分配來實現多個執行緒的執行。但是在多核處理器上,可以實現同時執行多個執行緒。作業系統可以將時間分配給第一個處理器上的執行緒,然後在另一個處理器上分配時間給另一個執行緒。
非同步是相對於同步而言。跟多執行緒不能同一而論。
非同步程式設計採用future或callback機制,以避免產生不必要的執行緒。(一個future代表一個將要完成的工作。)非同步程式設計核心就是:啟動了的操作將在一段時間後完成。這個操作正在執行時,不會阻塞原來的執行緒。啟動了這個操作的執行緒,可以繼續執行其他任務。當操作完成時,會通知它的future或者回撥函式,以便讓程式知道操作已經結束。
為什麼要使用非同步:
面向終端使用者的GUI程式:非同步程式設計提高了相應能力。可以使程式在執行任務時仍能相應使用者的輸入。
伺服器端應用:實現了可擴充套件性。伺服器應用可以利用執行緒池滿足其可擴充套件性。
非同步和同步的區別:
如果以同步方式執行某個任務時,需要等待該任務完成,然後才能再繼續執行另一個任務。而用非同步執行某個任務時,可以在該任務完成之前執行另一個任務。非同步最重要的體現就是不排隊,不阻塞。
圖:單執行緒同步
圖:多執行緒同步
非同步跟多執行緒
非同步可以在單個執行緒上實現,也可以在多個執行緒上實現,還可以不需要執行緒(一些IO操作)。
圖:單執行緒非同步
圖:多執行緒非同步
非同步是否建立執行緒
非同步可以分為CPU非同步和IO非同步。非同步在CPU操作中是必須要跑線上程上的,一般情況下這時我們都會新開一個執行緒執行這個非同步操作。但在IO操作中是不需要執行緒的,硬體直接和記憶體操作。
但是是否建立執行緒取決於你的非同步的實現方式。比如在非同步你用ThreadPool,Task.Run()等方法是建立了一個執行緒池的執行緒,那麼該非同步是在另一個執行緒上執行。
C#實現非同步的四種方式:
- 非同步模式BeginXXX,EndXXX
- 事件非同步xxxAsync,xxxCompleted
- 基於任務
Task
的非同步 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
事件。
如: backgroundworker
和progressbar
結合
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>
修飾。
非同步方法執行流程:
- 主執行緒呼叫MethodAsync方法,並等待方法執行結束
- 非同步方法開始執行,輸出“非同步執行”
- 非同步方法執行到await關鍵字,此時MethodAsync方法掛起,等待await表示式執行完畢,同時將控制權返回給呼叫方主執行緒,主執行緒繼續執行。
- 執行Task.Delay方法,同時主執行緒繼續執行之後的方法。
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執行線上程池執行緒中。
參考: