以任務為基礎的異步程式設計
工作平行連結庫 (TPL) 是以代表異步操作之 工作的概念為基礎。 在某些方面,工作類似於線程或 ThreadPool 工作專案,但抽象層級較高。 工作平行處理原則一詞 是指同時執行的一或多個獨立工作。 工作提供兩個主要優點:
更有效率且更可調整的系統資源使用方式。
在幕後,工作會被排入 ThreadPool佇列,其中其已透過能夠確定和調整執行緒數量的演算法進行強化。 這些演算法提供負載平衡,以將輸送量最大化。 此過程會讓工作變得相對輕量,而且您可以建立許多工作,以啟用細粒度的平行處理。
比線程或工作項目更具程式控制。
工作和其周圍建置的架構提供一組豐富的 API,可支援等候、取消、接續、健全的例外狀況處理、詳細狀態、自定義排程等等。
基於這兩個原因,TPL 是用來在 .NET 中撰寫多線程、異步和平行程式代碼的慣用 API。
以隱式方式建立和執行任務
Parallel.Invoke 方法提供方便的方式,同時執行任意數目的語句。 只要針對每個工作專案傳入 Action 委派即可。 建立這些委派最簡單的方式是使用 Lambda 運算式。 Lambda 運算式可以呼叫具名方法,或直接內嵌程式碼。 下列範例示範基本 Invoke 呼叫,該呼叫會建立並啟動兩個同時執行的工作。 第一個工作是由呼叫名為 DoSomeWork
之方法的 Lambda 運算式表示,而第二個工作則由呼叫名為 DoSomeOtherWork
之方法的 Lambda 表達式表示。
注意
此文件使用 lambda 運算式在 TPL 中定義委派。 如果您不熟悉 C# 或 Visual Basic 中的 Lambda 運算式,請參閱在 PLINQ 和 TPL 中Lambda 運算式。
Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())
如需詳細資訊,請參閱 如何:使用 Parallel.Invoke 執行平行作業。
若要進一步控制工作執行或從工作傳回值,您必須更明確地使用 Task 物件。
明確建立和執行工作
不會傳回值的工作是由 System.Threading.Tasks.Task 類別表示。 傳回值的工作是由繼承自 Task的 System.Threading.Tasks.Task<TResult> 類別表示。 工作物件會處理基礎結構詳細數據,並提供在工作整個存留期內,從呼叫線程存取的方法和屬性。 例如,您可以隨時存取工作的 Status 屬性,以判斷它是否已開始執行、執行至完成、已取消或擲回例外狀況。 狀態是由 TaskStatus 列舉表示。
當您建立工作時,您會為它提供一個使用者委派,以封裝工作將執行的程序代碼。 委派可以表示為具名委派、匿名方法或 Lambda 表達式。 Lambda 表達式可以包含對具名方法的呼叫,如下列範例所示。 此範例包含對 Task.Wait 方法的呼叫,以確保工作會在主控台模式應用程式結束之前完成執行。
using System;
using System.Threading;
using System.Threading.Tasks;
public class Lambda
{
public static void Main()
{
Thread.CurrentThread.Name = "Main";
// Create a task and supply a user delegate by using a lambda expression.
Task taskA = new Task( () => Console.WriteLine("Hello from taskA."));
// Start the task.
taskA.Start();
// Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name);
taskA.Wait();
}
}
// The example displays output as follows:
// Hello from thread 'Main'.
// Hello from taskA.
// or
// Hello from taskA.
// Hello from thread 'Main'.
Imports System.Threading
Namespace Lambda
Module Example
Public Sub Main()
Thread.CurrentThread.Name = "Main"
' Create a task and supply a user delegate by using a lambda expression.
Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
' Start the task.
taskA.Start()
' Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name)
taskA.Wait()
End Sub
End Module
' The example displays output like the following:
' Hello from thread 'Main'.
' Hello from taskA.
End Namespace
您也可以使用 Task.Run 方法來在一個作業中建立和啟動工作。 若要管理工作,Run 方法會使用預設工作排程器,不論哪個工作排程器與目前線程相關聯。 當不需要對工作的建立和排程進行更多控制時,Run 方法是建立和啟動工作的慣用方法。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Run;
public class Example
{
public static void Main()
{
Thread.CurrentThread.Name = "Main";
// Define and run the task.
Task taskA = Task.Run( () => Console.WriteLine("Hello from taskA."));
// Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name);
taskA.Wait();
}
}
// The example displays output as follows:
// Hello from thread 'Main'.
// Hello from taskA.
// or
// Hello from taskA.
// Hello from thread 'Main'.
Imports System.Threading
Namespace Run
Module Example
Public Sub Main()
Thread.CurrentThread.Name = "Main"
Dim taskA As Task = Task.Run(Sub() Console.WriteLine("Hello from taskA."))
' Output a message from the calling thread.
Console.WriteLine("Hello from thread '{0}'.",
Thread.CurrentThread.Name)
taskA.Wait()
End Sub
End Module
' The example displays output like the following:
' Hello from thread 'Main'.
' Hello from taskA.
End Namespace
您也可以使用 TaskFactory.StartNew 方法來在一個作業中建立和啟動工作。 如下列範例所示,您可以在下列範例中使用此方法:
建立與排程不必分開,您也可以需要更多的任務創建選項或使用特定的排程工具。
您必須將額外的狀態傳遞至工作,然後可透過其 Task.AsyncState 屬性擷取。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskIntro;
class CustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
public class AsyncState
{
public static void Main()
{
Task[] taskArray = new Task[10];
for (int i = 0; i < taskArray.Length; i++)
{
taskArray[i] = Task.Factory.StartNew((Object obj) =>
{
CustomData data = obj as CustomData;
if (data == null) return;
data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
},
new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
}
Task.WaitAll(taskArray);
foreach (var task in taskArray)
{
var data = task.AsyncState as CustomData;
if (data != null)
Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum);
}
}
}
// The example displays output like the following:
// Task #0 created at 635116412924597583, ran on thread #3.
// Task #1 created at 635116412924607584, ran on thread #4.
// Task #2 created at 635116412924607584, ran on thread #4.
// Task #3 created at 635116412924607584, ran on thread #4.
// Task #4 created at 635116412924607584, ran on thread #3.
// Task #5 created at 635116412924607584, ran on thread #3.
// Task #6 created at 635116412924607584, ran on thread #4.
// Task #7 created at 635116412924607584, ran on thread #4.
// Task #8 created at 635116412924607584, ran on thread #3.
// Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading
Namespace AsyncState
Class CustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Module Example
Public Sub Main()
Dim taskArray(9) As Task
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
Dim data As CustomData = TryCast(obj, CustomData)
If data Is Nothing Then Return
data.ThreadNum = Environment.CurrentManagedThreadId
End Sub,
New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
Next
Task.WaitAll(taskArray)
For Each task In taskArray
Dim data = TryCast(task.AsyncState, CustomData)
If data IsNot Nothing Then
Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum)
End If
Next
End Sub
End Module
' The example displays output like the following:
' Task #0 created at 635116412924597583, ran on thread #3.
' Task #1 created at 635116412924607584, ran on thread #4.
' Task #2 created at 635116412924607584, ran on thread #4.
' Task #3 created at 635116412924607584, ran on thread #4.
' Task #4 created at 635116412924607584, ran on thread #3.
' Task #5 created at 635116412924607584, ran on thread #3.
' Task #6 created at 635116412924607584, ran on thread #4.
' Task #7 created at 635116412924607584, ran on thread #4.
' Task #8 created at 635116412924607584, ran on thread #3.
' Task #9 created at 635116412924607584, ran on thread #4.
End Namespace
Task 和 Task<TResult> 都會公開靜態 Factory 屬性,以傳回預設的 TaskFactory實例,讓您可以將 方法呼叫為 Task.Factory.StartNew()
。 此外,在下列範例中,由於工作的類型為 System.Threading.Tasks.Task<TResult>,因此每個工作都有包含計算結果的公用 Task<TResult>.Result 屬性。 工作會以異步方式執行,而且可能會依任何順序完成。 如果在計算完成之前存取 Result 屬性,屬性會封鎖呼叫線程,直到有值可用為止。
using System;
using System.Threading.Tasks;
public class Result
{
public static void Main()
{
Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };
var results = new Double[taskArray.Length];
Double sum = 0;
for (int i = 0; i < taskArray.Length; i++) {
results[i] = taskArray[i].Result;
Console.Write("{0:N1} {1}", results[i],
i == taskArray.Length - 1 ? "= " : "+ ");
sum += results[i];
}
Console.WriteLine("{0:N1}", sum);
}
private static Double DoComputation(Double start)
{
Double sum = 0;
for (var value = start; value <= start + 10; value += .1)
sum += value;
return sum;
}
}
// The example displays the following output:
// 606.0 + 10,605.0 + 100,495.0 = 111,706.0
Namespace Result
Module Example
Public Sub Main()
Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation(1.0)),
Task(Of Double).Factory.StartNew(Function() DoComputation(100.0)),
Task(Of Double).Factory.StartNew(Function() DoComputation(1000.0))}
Dim results(taskArray.Length - 1) As Double
Dim sum As Double
For i As Integer = 0 To taskArray.Length - 1
results(i) = taskArray(i).Result
Console.Write("{0:N1} {1}", results(i),
If(i = taskArray.Length - 1, "= ", "+ "))
sum += results(i)
Next
Console.WriteLine("{0:N1}", sum)
End Sub
Private Function DoComputation(start As Double) As Double
Dim sum As Double
For value As Double = start To start + 10 Step .1
sum += value
Next
Return sum
End Function
End Module
' The example displays the following output:
' 606.0 + 10,605.0 + 100,495.0 = 111,706.0
End Namespace
如需詳細資訊,請參閱 如何:從工作傳回值。
當您使用 Lambda 運算式來建立委派時,您可以存取原始程式碼中該點可見的所有變數。 不過,在某些情況下,最值得注意的是在迴圈內,Lambda 不會如預期般擷取變數。 它只會擷取變數的參考,而不是它的值,因為變數在每次迭代後都會改變。 下列範例說明問題。 它會將迴圈計數器傳遞至 Lambda 運算式,以具現化 CustomData
物件,並使用迴圈計數器做為物件的識別碼。 如範例所示的輸出,每個 CustomData
物件都有相同的標識碼。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Example.Iterations;
class CustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
public class IterationTwo
{
public static void Main()
{
// Create the task object by using an Action(Of Object) to pass in the loop
// counter. This produces an unexpected result.
Task[] taskArray = new Task[10];
for (int i = 0; i < taskArray.Length; i++) {
taskArray[i] = Task.Factory.StartNew( (Object obj) => {
var data = new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks};
data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum);
},
i );
}
Task.WaitAll(taskArray);
}
}
// The example displays output like the following:
// Task #10 created at 635116418427727841 on thread #4.
// Task #10 created at 635116418427737842 on thread #4.
// Task #10 created at 635116418427737842 on thread #4.
// Task #10 created at 635116418427737842 on thread #4.
// Task #10 created at 635116418427737842 on thread #4.
// Task #10 created at 635116418427737842 on thread #4.
// Task #10 created at 635116418427727841 on thread #3.
// Task #10 created at 635116418427747843 on thread #3.
// Task #10 created at 635116418427747843 on thread #3.
// Task #10 created at 635116418427737842 on thread #4.
Imports System.Threading
Namespace IterationsTwo
Class CustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Module Example
Public Sub Main()
' Create the task object by using an Action(Of Object) to pass in the loop
' counter. This produces an unexpected result.
Dim taskArray(9) As Task
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
Dim data As New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks}
data.ThreadNum = Environment.CurrentManagedThreadId
Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum)
End Sub,
i)
Next
Task.WaitAll(taskArray)
End Sub
End Module
' The example displays output like the following:
' Task #10 created at 635116418427727841 on thread #4.
' Task #10 created at 635116418427737842 on thread #4.
' Task #10 created at 635116418427737842 on thread #4.
' Task #10 created at 635116418427737842 on thread #4.
' Task #10 created at 635116418427737842 on thread #4.
' Task #10 created at 635116418427737842 on thread #4.
' Task #10 created at 635116418427727841 on thread #3.
' Task #10 created at 635116418427747843 on thread #3.
' Task #10 created at 635116418427747843 on thread #3.
' Task #10 created at 635116418427737842 on thread #4.
End Namespace
您可以透過建構函式將狀態物件提供給任務,以在每次迭代中存取值。 下列範例會在建立 CustomData
物件時,使用循環計數器修改先前的範例,接著會傳遞至 Lambda 表達式。 如範例所示的輸出,每個 CustomData
對象現在都會根據物件具現化時的迴圈計數器值,具有唯一標識符。
using System;
using System.Threading;
using System.Threading.Tasks;
class CustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
public class IterationOne
{
public static void Main()
{
// Create the task object by using an Action(Of Object) to pass in custom data
// to the Task constructor. This is useful when you need to capture outer variables
// from within a loop.
Task[] taskArray = new Task[10];
for (int i = 0; i < taskArray.Length; i++) {
taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
CustomData data = obj as CustomData;
if (data == null)
return;
data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum);
},
new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
}
Task.WaitAll(taskArray);
}
}
// The example displays output like the following:
// Task #0 created at 635116412924597583 on thread #3.
// Task #1 created at 635116412924607584 on thread #4.
// Task #3 created at 635116412924607584 on thread #4.
// Task #4 created at 635116412924607584 on thread #4.
// Task #2 created at 635116412924607584 on thread #3.
// Task #6 created at 635116412924607584 on thread #3.
// Task #5 created at 635116412924607584 on thread #4.
// Task #8 created at 635116412924607584 on thread #4.
// Task #7 created at 635116412924607584 on thread #3.
// Task #9 created at 635116412924607584 on thread #4.
Imports System.Threading
Namespace IterationsOne
Class CustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Module Example
Public Sub Main()
' Create the task object by using an Action(Of Object) to pass in custom data
' to the Task constructor. This is useful when you need to capture outer variables
' from within a loop.
Dim taskArray(9) As Task
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
Dim data As CustomData = TryCast(obj, CustomData)
If data Is Nothing Then Return
data.ThreadNum = Environment.CurrentManagedThreadId
Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum)
End Sub,
New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
Next
Task.WaitAll(taskArray)
End Sub
End Module
' The example displays output like the following:
' Task #0 created at 635116412924597583 on thread #3.
' Task #1 created at 635116412924607584 on thread #4.
' Task #3 created at 635116412924607584 on thread #4.
' Task #4 created at 635116412924607584 on thread #4.
' Task #2 created at 635116412924607584 on thread #3.
' Task #6 created at 635116412924607584 on thread #3.
' Task #5 created at 635116412924607584 on thread #4.
' Task #8 created at 635116412924607584 on thread #4.
' Task #7 created at 635116412924607584 on thread #3.
' Task #9 created at 635116412924607584 on thread #4.
End Namespace
此狀態會當做自變數傳遞至工作委派,而且可以使用 Task.AsyncState 屬性從工作物件存取它。 下列範例是上一個範例的變化。 它會使用 AsyncState 屬性來顯示傳遞至 Lambda 表達式之 CustomData
對象的相關信息。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TaskIntro;
class CustomData
{
public long CreationTime;
public int Name;
public int ThreadNum;
}
public class AsyncState
{
public static void Main()
{
Task[] taskArray = new Task[10];
for (int i = 0; i < taskArray.Length; i++)
{
taskArray[i] = Task.Factory.StartNew((Object obj) =>
{
CustomData data = obj as CustomData;
if (data == null) return;
data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
},
new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
}
Task.WaitAll(taskArray);
foreach (var task in taskArray)
{
var data = task.AsyncState as CustomData;
if (data != null)
Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum);
}
}
}
// The example displays output like the following:
// Task #0 created at 635116412924597583, ran on thread #3.
// Task #1 created at 635116412924607584, ran on thread #4.
// Task #2 created at 635116412924607584, ran on thread #4.
// Task #3 created at 635116412924607584, ran on thread #4.
// Task #4 created at 635116412924607584, ran on thread #3.
// Task #5 created at 635116412924607584, ran on thread #3.
// Task #6 created at 635116412924607584, ran on thread #4.
// Task #7 created at 635116412924607584, ran on thread #4.
// Task #8 created at 635116412924607584, ran on thread #3.
// Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading
Namespace AsyncState
Class CustomData
Public CreationTime As Long
Public Name As Integer
Public ThreadNum As Integer
End Class
Module Example
Public Sub Main()
Dim taskArray(9) As Task
For i As Integer = 0 To taskArray.Length - 1
taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
Dim data As CustomData = TryCast(obj, CustomData)
If data Is Nothing Then Return
data.ThreadNum = Environment.CurrentManagedThreadId
End Sub,
New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
Next
Task.WaitAll(taskArray)
For Each task In taskArray
Dim data = TryCast(task.AsyncState, CustomData)
If data IsNot Nothing Then
Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
data.Name, data.CreationTime, data.ThreadNum)
End If
Next
End Sub
End Module
' The example displays output like the following:
' Task #0 created at 635116412924597583, ran on thread #3.
' Task #1 created at 635116412924607584, ran on thread #4.
' Task #2 created at 635116412924607584, ran on thread #4.
' Task #3 created at 635116412924607584, ran on thread #4.
' Task #4 created at 635116412924607584, ran on thread #3.
' Task #5 created at 635116412924607584, ran on thread #3.
' Task #6 created at 635116412924607584, ran on thread #4.
' Task #7 created at 635116412924607584, ran on thread #4.
' Task #8 created at 635116412924607584, ran on thread #3.
' Task #9 created at 635116412924607584, ran on thread #4.
End Namespace
工作標識碼
每個工作都會收到一個整數 ID,可在應用程式域中唯一識別,並且可以使用 Task.Id 屬性來存取。 識別碼適用於在 Visual Studio 偵錯工具中檢視工作資訊,平行堆疊 和 工作 視窗。 識別碼是延遲建立的,表示只有在需要時才會建立。 因此,每次執行程式時,任務可能會有不同的 ID。 如需如何在調試程式中檢視工作 ID 的詳細資訊,請參閱 使用工作視窗 和 使用平行堆疊視窗。
工作建立選項
建立工作的大部分 API 都會提供接受 TaskCreationOptions 參數的重載。 藉由指定其中一或多個選項,您可以告訴工作排程器如何在線程集區上排程工作。 選項可以使用位 OR 作業來合併。
下列範例顯示具有 LongRunning 和 PreferFairness 選項的工作:
var task3 = new Task(() => MyLongRunningMethod(),
TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();
Dim task3 = New Task(Sub() MyLongRunningMethod(),
TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()
工作、執行緒和文化
每個線程都有相關聯的文化特性和UI文化特性,分別由 Thread.CurrentCulture 和 Thread.CurrentUICulture 屬性定義。 線程的文化特性用於格式化、剖析、排序和字串比較作業等作業。 線程的UI文化用於資源查找。
系統文化特性會定義線程的預設文化特性和UI文化特性。 不過,您可以使用 CultureInfo.DefaultThreadCurrentCulture 和 CultureInfo.DefaultThreadCurrentUICulture 屬性,為應用程式域中的所有線程指定預設文化特性。 如果您明確設定線程的文化特性並啟動新的線程,則新線程不會繼承呼叫線程的文化特性;相反地,其文化特性是預設系統文化特性。 不過,在工作型程序設計中,工作會使用呼叫線程的文化特性,即使工作在不同的線程上以異步方式執行也一樣。
下列範例提供簡單的圖例。 它會將應用程式的目前文化特性變更為法文(法國)。 如果法文(法國)已經是目前的文化,它就會改變英語(美國)。 然後,它會叫用名為 formatDelegate
的委派,以傳回新文化特性中格式化為貨幣值的一些數字。 無論委派是由工作以同步或異步方式叫用,工作都會使用呼叫線程的文化特性。
using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
public class Example
{
public static void Main()
{
decimal[] values = { 163025412.32m, 18905365.59m };
string formatString = "C2";
Func<String> formatDelegate = () => { string output = String.Format("Formatting using the {0} culture on thread {1}.\n",
CultureInfo.CurrentCulture.Name,
Thread.CurrentThread.ManagedThreadId);
foreach (var value in values)
output += String.Format("{0} ", value.ToString(formatString));
output += Environment.NewLine;
return output;
};
Console.WriteLine("The example is running on thread {0}",
Thread.CurrentThread.ManagedThreadId);
// Make the current culture different from the system culture.
Console.WriteLine("The current culture is {0}",
CultureInfo.CurrentCulture.Name);
if (CultureInfo.CurrentCulture.Name == "fr-FR")
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
else
Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");
Console.WriteLine("Changed the current culture to {0}.\n",
CultureInfo.CurrentCulture.Name);
// Execute the delegate synchronously.
Console.WriteLine("Executing the delegate synchronously:");
Console.WriteLine(formatDelegate());
// Call an async delegate to format the values using one format string.
Console.WriteLine("Executing a task asynchronously:");
var t1 = Task.Run(formatDelegate);
Console.WriteLine(t1.Result);
Console.WriteLine("Executing a task synchronously:");
var t2 = new Task<String>(formatDelegate);
t2.RunSynchronously();
Console.WriteLine(t2.Result);
}
}
// The example displays the following output:
// The example is running on thread 1
// The current culture is en-US
// Changed the current culture to fr-FR.
//
// Executing the delegate synchronously:
// Formatting using the fr-FR culture on thread 1.
// 163 025 412,32 € 18 905 365,59 €
//
// Executing a task asynchronously:
// Formatting using the fr-FR culture on thread 3.
// 163 025 412,32 € 18 905 365,59 €
//
// Executing a task synchronously:
// Formatting using the fr-FR culture on thread 1.
// 163 025 412,32 € 18 905 365,59 €
Imports System.Globalization
Imports System.Threading
Module Example
Public Sub Main()
Dim values() As Decimal = {163025412.32D, 18905365.59D}
Dim formatString As String = "C2"
Dim formatDelegate As Func(Of String) = Function()
Dim output As String = String.Format("Formatting using the {0} culture on thread {1}.",
CultureInfo.CurrentCulture.Name,
Thread.CurrentThread.ManagedThreadId)
output += Environment.NewLine
For Each value In values
output += String.Format("{0} ", value.ToString(formatString))
Next
output += Environment.NewLine
Return output
End Function
Console.WriteLine("The example is running on thread {0}",
Thread.CurrentThread.ManagedThreadId)
' Make the current culture different from the system culture.
Console.WriteLine("The current culture is {0}",
CultureInfo.CurrentCulture.Name)
If CultureInfo.CurrentCulture.Name = "fr-FR" Then
Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
Else
Thread.CurrentThread.CurrentCulture = New CultureInfo("fr-FR")
End If
Console.WriteLine("Changed the current culture to {0}.",
CultureInfo.CurrentCulture.Name)
Console.WriteLine()
' Execute the delegate synchronously.
Console.WriteLine("Executing the delegate synchronously:")
Console.WriteLine(formatDelegate())
' Call an async delegate to format the values using one format string.
Console.WriteLine("Executing a task asynchronously:")
Dim t1 = Task.Run(formatDelegate)
Console.WriteLine(t1.Result)
Console.WriteLine("Executing a task synchronously:")
Dim t2 = New Task(Of String)(formatDelegate)
t2.RunSynchronously()
Console.WriteLine(t2.Result)
End Sub
End Module
' The example displays the following output:
'
' The example is running on thread 1
' The current culture is en-US
' Changed the current culture to fr-FR.
'
' Executing the delegate synchronously:
' Formatting Imports the fr-FR culture on thread 1.
' 163 025 412,32 € 18 905 365,59 €
'
' Executing a task asynchronously:
' Formatting Imports the fr-FR culture on thread 3.
' 163 025 412,32 € 18 905 365,59 €
'
' Executing a task synchronously:
' Formatting Imports the fr-FR culture on thread 1.
' 163 025 412,32 € 18 905 365,59 €
注意
在 .NET Framework 4.6 之前的 .NET Framework 版本中,工作的文化特性取決於執行線程的文化特性,而不是呼叫線程的文化特性。 針對異步工作,工作所使用的文化特性可能會與呼叫線程的文化特性不同。
如需了解更多有關異步任務和文化的資訊,請參閱 CultureInfo 文章中的“文化與異步任務操作”一節。
建立工作接續
Task.ContinueWith 和 Task<TResult>.ContinueWith 方法可讓您指定要在 前項工作 完成時啟動的工作。 延續工作的委託會收到前一項工作的參考,以便檢查前項工作的狀態。 藉由擷取 Task<TResult>.Result 屬性的值,您可以使用前項的輸出做為接續的輸入。
在下列範例中,getData
工作是由呼叫 TaskFactory.StartNew<TResult>(Func<TResult>) 方法所啟動。
processData
工作會在 getData
完成時自動啟動,displayData
會在 processData
完成時啟動。
getData
會產生一個整數陣列,該陣列可以透過 getData
任務的 Task<TResult>.Result 屬性由 processData
任務進行存取。
processData
工作會處理陣列並傳回結果,其類型是從傳遞至 Task<TResult>.ContinueWith<TNewResult>(Func<Task<TResult>,TNewResult>) 方法之 Lambda 表達式的傳回型別推斷而來。
displayData
工作會在 processData
完成時自動執行,而且 processData
Lambda 表達式所傳回 Tuple<T1,T2,T3> 物件可透過 processData
工作的 Task<TResult>.Result 屬性存取 displayData
工作。
displayData
任務將接收 processData
任務的結果。 它會產生一個結果,其型別是以類似的方式推斷,而且可在 Result 屬性中提供給程式使用。
using System;
using System.Threading.Tasks;
public class ContinuationOne
{
public static void Main()
{
var getData = Task.Factory.StartNew(() => {
Random rnd = new Random();
int[] values = new int[100];
for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
values[ctr] = rnd.Next();
return values;
} );
var processData = getData.ContinueWith((x) => {
int n = x.Result.Length;
long sum = 0;
double mean;
for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
sum += x.Result[ctr];
mean = sum / (double) n;
return Tuple.Create(n, sum, mean);
} );
var displayData = processData.ContinueWith((x) => {
return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
x.Result.Item1, x.Result.Item2,
x.Result.Item3);
} );
Console.WriteLine(displayData.Result);
}
}
// The example displays output similar to the following:
// N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
Namespace ContinuationsOne
Module Example
Public Sub Main()
Dim getData = Task.Factory.StartNew(Function()
Dim rnd As New Random()
Dim values(99) As Integer
For ctr = 0 To values.GetUpperBound(0)
values(ctr) = rnd.Next()
Next
Return values
End Function)
Dim processData = getData.ContinueWith(Function(x)
Dim n As Integer = x.Result.Length
Dim sum As Long
Dim mean As Double
For ctr = 0 To x.Result.GetUpperBound(0)
sum += x.Result(ctr)
Next
mean = sum / n
Return Tuple.Create(n, sum, mean)
End Function)
Dim displayData = processData.ContinueWith(Function(x)
Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
x.Result.Item1, x.Result.Item2,
x.Result.Item3)
End Function)
Console.WriteLine(displayData.Result)
End Sub
End Module
' The example displays output like the following:
' N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace
因為 Task.ContinueWith 是實例方法,因此您可以將方法呼叫鏈結在一起,而不是為每個前項工作具現化 Task<TResult> 物件。 下列範例的功能與上一個範例相同,不同之處在於它會將呼叫鏈結至 Task.ContinueWith 方法。 方法呼叫鏈結所傳回的 Task<TResult> 物件是最終的後續工作。
using System;
using System.Threading.Tasks;
public class ContinuationTwo
{
public static void Main()
{
var displayData = Task.Factory.StartNew(() => {
Random rnd = new Random();
int[] values = new int[100];
for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
values[ctr] = rnd.Next();
return values;
} ).
ContinueWith((x) => {
int n = x.Result.Length;
long sum = 0;
double mean;
for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
sum += x.Result[ctr];
mean = sum / (double) n;
return Tuple.Create(n, sum, mean);
} ).
ContinueWith((x) => {
return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
x.Result.Item1, x.Result.Item2,
x.Result.Item3);
} );
Console.WriteLine(displayData.Result);
}
}
// The example displays output similar to the following:
// N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
Namespace ContinuationsTwo
Module Example
Public Sub Main()
Dim displayData = Task.Factory.StartNew(Function()
Dim rnd As New Random()
Dim values(99) As Integer
For ctr = 0 To values.GetUpperBound(0)
values(ctr) = rnd.Next()
Next
Return values
End Function). _
ContinueWith(Function(x)
Dim n As Integer = x.Result.Length
Dim sum As Long
Dim mean As Double
For ctr = 0 To x.Result.GetUpperBound(0)
sum += x.Result(ctr)
Next
mean = sum / n
Return Tuple.Create(n, sum, mean)
End Function). _
ContinueWith(Function(x)
Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
x.Result.Item1, x.Result.Item2,
x.Result.Item3)
End Function)
Console.WriteLine(displayData.Result)
End Sub
End Module
' The example displays output like the following:
' N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace
ContinueWhenAll 和 ContinueWhenAny 方法可讓您繼續執行多個工作。
建立獨立子工作
在執行中的工作中運行的使用者代碼如果建立了一個新工作,且未指定 [AttachedToParent] 選項,這個新工作就不會以任何特殊的方式與父工作進行同步。 這種類型的非同步處理工作稱為 中斷連結的巢狀工作 或 中斷連結的子工作。 下列範例顯示建立一個獨立子工作的工作:
var outer = Task.Factory.StartNew(() =>
{
Console.WriteLine("Outer task beginning.");
var child = Task.Factory.StartNew(() =>
{
Thread.SpinWait(5000000);
Console.WriteLine("Detached task completed.");
});
});
outer.Wait();
Console.WriteLine("Outer task completed.");
// The example displays the following output:
// Outer task beginning.
// Outer task completed.
// Detached task completed.
Dim outer = Task.Factory.StartNew(Sub()
Console.WriteLine("Outer task beginning.")
Dim child = Task.Factory.StartNew(Sub()
Thread.SpinWait(5000000)
Console.WriteLine("Detached task completed.")
End Sub)
End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
' The example displays the following output:
' Outer task beginning.
' Outer task completed.
' Detached child completed.
注意
父任務不會等待分離的子任務完成。
建立子任務
在執行中的使用者程式代碼於工作中建立帶有 [AttachedToParent] 選項的新工作時,這個新工作被稱為父工作 的附屬子工作。 您可以使用 [AttachedToParent] 選項來表示結構化任務平行處理,因為父任務會隱式等待所有附加的子任務完成。 下列範例顯示一個建立10個附屬子工作的父任務。 此範例會呼叫 Task.Wait 方法來等候父工作完成。 它不需要明確等待附加的子任務完成。
using System;
using System.Threading;
using System.Threading.Tasks;
public class Child
{
public static void Main()
{
var parent = Task.Factory.StartNew(() => {
Console.WriteLine("Parent task beginning.");
for (int ctr = 0; ctr < 10; ctr++) {
int taskNo = ctr;
Task.Factory.StartNew((x) => {
Thread.SpinWait(5000000);
Console.WriteLine("Attached child #{0} completed.",
x);
},
taskNo, TaskCreationOptions.AttachedToParent);
}
});
parent.Wait();
Console.WriteLine("Parent task completed.");
}
}
// The example displays output like the following:
// Parent task beginning.
// Attached child #9 completed.
// Attached child #0 completed.
// Attached child #8 completed.
// Attached child #1 completed.
// Attached child #7 completed.
// Attached child #2 completed.
// Attached child #6 completed.
// Attached child #3 completed.
// Attached child #5 completed.
// Attached child #4 completed.
// Parent task completed.
Imports System.Threading
Namespace Child
Module Example
Public Sub Main()
Dim parent = Task.Factory.StartNew(Sub()
Console.WriteLine("Parent task beginning.")
For ctr As Integer = 0 To 9
Dim taskNo As Integer = ctr
Task.Factory.StartNew(Sub(x)
Thread.SpinWait(5000000)
Console.WriteLine("Attached child #{0} completed.",
x)
End Sub,
taskNo, TaskCreationOptions.AttachedToParent)
Next
End Sub)
parent.Wait()
Console.WriteLine("Parent task completed.")
End Sub
End Module
' The example displays output like the following:
' Parent task beginning.
' Attached child #9 completed.
' Attached child #0 completed.
' Attached child #8 completed.
' Attached child #1 completed.
' Attached child #7 completed.
' Attached child #2 completed.
' Attached child #6 completed.
' Attached child #3 completed.
' Attached child #5 completed.
' Attached child #4 completed.
' Parent task completed.
End Namespace
父工作可以使用 [TaskCreationOptions.DenyChildAttach] 選項來防止其他工作附加至父工作。 如需詳細資訊,請參閱 附加和分離的子工作。
等候工作完成
System.Threading.Tasks.Task 和 System.Threading.Tasks.Task<TResult> 類型提供數個多載的 Task.Wait 方法,可讓您等候工作完成。 此外,靜態 Task.WaitAll 和 Task.WaitAny 方法的重載允許您等候任務陣列中的任何或所有任務完成。
一般而言,您會等候工作,因為下列其中一個原因:
主線程取決於工作計算的最終結果。
您必須處理可能從任務擲回的例外狀況。
應用程式可能會在所有工作完成執行之前終止。 例如,主控台應用程式會在執行
Main
中的所有同步程式代碼之後終止。
下列範例顯示不涉及例外狀況處理的基本模式:
Task[] tasks = new Task[3]
{
Task.Factory.StartNew(() => MethodA()),
Task.Factory.StartNew(() => MethodB()),
Task.Factory.StartNew(() => MethodC())
};
//Block until all tasks complete.
Task.WaitAll(tasks);
// Continue on this thread...
Dim tasks() =
{
Task.Factory.StartNew(Sub() MethodA()),
Task.Factory.StartNew(Sub() MethodB()),
Task.Factory.StartNew(Sub() MethodC())
}
' Block until all tasks complete.
Task.WaitAll(tasks)
' Continue on this thread...
如需顯示例外狀況處理的範例,請參閱 例外狀況處理。
某些多載允許您指定逾時,而另外一些則接受額外的 CancellationToken 作為輸入參數,以便可以通過程式設計方式或回應使用者輸入來取消等候。
當您等待一個工作時,您也會自動地等待透過 [TaskCreationOptions.AttachedToParent] 選項建立的該工作之所有子工作。 如果工作已完成,Task.Wait 會立即傳回 。 即使工作完成之後呼叫 Task.Wait 方法,Task.Wait 方法也會擲回工作引發的任何例外狀況。
編寫任務
Task 和 Task<TResult> 類別提供數種方法來協助您撰寫多個工作。 這些方法會實作常見的模式,並充分利用 C#、Visual Basic 和 F# 所提供的異步語言功能。 本節說明 WhenAll、WhenAny、Delay和 FromResult 方法。
Task.WhenAll
Task.WhenAll 方法會以異步方式等候多個 Task 或 Task<TResult> 物件完成。 它提供多載版本,可讓您等候非統一的工作集。 例如,您可以通過一次方法呼叫來等候多個 Task 和 Task<TResult> 物件完成。
Task.WhenAny
Task.WhenAny 方法會以異步方式等候多個 Task 或 Task<TResult> 物件的其中一個完成。 如同在 Task.WhenAll 方法中,此方法會提供多載版本,讓您等候非統一的工作集。 WhenAny 方法在下列案例中特別有用:
備援作業:請考慮可透過多種方式執行的演算法或作業。 您可以使用 WhenAny 方法來選取先完成的作業,然後取消其餘作業。
交錯作業:您可以啟動多個必須完成的作業,並使用 WhenAny 方法來處理每個作業完成時的結果。 一項作業完成後,您可以啟動一或多個工作。
節流作業:您可以使用 WhenAny 方法來限制並行作業數目來擴充先前的案例。
過期作業:您可以使用 WhenAny 方法來選取一或多個工作,以及在特定時間之後完成的工作,例如 Delay 方法傳回的工作。 下一節將說明 Delay 方法。
Task.Delay(任務延遲)
Task.Delay 方法會產生在指定時間之後完成的 Task 物件。 您可以使用此方法來建置循環來輪詢數據、指定逾時、延遲處理使用者輸入等等。
任務(T)。FromResult
藉由使用 Task.FromResult 方法,您可以建立保存預先計算結果的 Task<TResult> 物件。 當您執行會傳回 Task<TResult> 物件的異步操作,且已經計算該 Task<TResult> 對象的結果時,這個方法很有用。 如需使用 FromResult 擷取快取中保留之異步下載作業結果的範例,請參閱 如何:建立預先計算的工作。
處理工作中例外狀況
當工作拋出一或多個例外狀況時,這些例外狀況會被包裝在 AggregateException 例外狀況中。 該例外狀況會傳播回與任務連接的執行緒。 一般而言,這是等候工作完成的線程,或是存取 Result 屬性的線程。 此行為會強制執行 .NET Framework 原則,預設所有未處理的例外狀況都應該終止進程。 呼叫程式代碼可以使用 try
/catch
區塊中的任何一項來處理例外狀況:
聯結線程也可以藉由在垃圾收集工作之前存取 Exception 屬性來處理例外狀況。 藉由存取這個屬性,您可以防止未處理的例外狀況觸發在物件完成時終止進程的例外狀況傳播行為。
如需例外狀況和工作的詳細資訊,請參閱 例外狀況處理。
取消任務
Task 類別支援合作式取消,並與 .NET Framework 4 中引進的 System.Threading.CancellationTokenSource 和 System.Threading.CancellationToken 類別完全整合。 System.Threading.Tasks.Task 類別中的許多建構函式會採用 CancellationToken 對象作為輸入參數。 許多 StartNew 和 Run 多載也包含 CancellationToken 參數。
您可以使用 CancellationTokenSource 類別,在稍後建立令牌併發出取消要求。 將令牌傳遞至 Task 做為自變數,同時參考使用者委派中的相同令牌,這會執行回應取消要求的工作。
如需詳細資訊,請參閱 工作取消 和 如何取消工作及其子任務。
TaskFactory 類別
TaskFactory 類別提供靜態方法,封裝用來建立和啟動工作和接續工作的常見模式。
最常見的模式是 StartNew,它會在一個語句中建立和啟動工作。
當您從多個前項建立延續任務時,請在 Task<TResult> 類別中使用 ContinueWhenAll 方法或 ContinueWhenAny 方法或其等效方法。 如需詳細資訊,請參閱使用接續工作鏈結工作。
若要在 Task 或 Task<TResult> 實例中封裝異步程序設計模型
BeginX
和EndX
方法,請使用 FromAsync 方法。 如需詳細資訊,請參閱 TPL 和傳統 .NET Framework 異步程式設計。
默認 TaskFactory 可作為 Task 類別或 Task<TResult> 類別上的靜態屬性存取。 您也可以直接具現化 TaskFactory,並指定各種選項,包括 CancellationToken、TaskCreationOptions 選項、TaskContinuationOptions 選項或 TaskScheduler。 當您建立工作處理站時所指定的任何選項都會套用至它建立的所有工作,除非使用 TaskCreationOptions 列舉建立 Task,在此情況下,工作的選項會覆寫工作處理站的選項。
沒有委派的工作
在某些情況下,您可能想要使用 Task 來封裝外部元件所執行的一些異步操作,而不是您的使用者委派。 如果作業是以異步程序設計模型開始/結束模式為基礎,您可以使用 FromAsync 方法。 如果不是這種情況,您可以使用 TaskCompletionSource<TResult> 物件將操作封裝在任務中,從而獲得 Task 程式設計的某些優點。 例如,支援例外狀況的傳遞和延續。 如需詳細資訊,請參閱 TaskCompletionSource<TResult>。
自定義排程器
大部分的應用程式或函式庫開發人員並不關心工作在哪個處理器上執行,如何與其他工作同步處理,或如何在 System.Threading.ThreadPool上排程。 它們只需要在主計算機上盡可能有效率地執行。 如果您需要更精細地控制排程詳細數據,TPL 可讓您在預設工作排程器上設定一些設定,甚至可讓您提供自定義排程器。 如需詳細資訊,請參閱 TaskScheduler。
相關數據結構
TPL 有數個新的公用類型,適用於平行和循序案例。 這些類別包括 System.Collections.Concurrent 命名空間中的數個安全線程、快速且可調整的集合類別,以及數個新的同步處理類型。 例如,System.Threading.Semaphore 和 System.Threading.ManualResetEventSlim,相較於其前置專案,其對於特定種類的工作負載更有效率。 .NET Framework 4 中的其他新類型,例如,System.Threading.Barrier 和 System.Threading.SpinLock,提供舊版中無法使用的功能。 如需詳細資訊,請參閱 平行程式設計的數據結構。
自訂工作類型
我們建議您不要繼承自 System.Threading.Tasks.Task 或 System.Threading.Tasks.Task<TResult>。 相反地,我們建議您使用 AsyncState 屬性,將其他數據或狀態與 Task 或 Task<TResult> 對象產生關聯。 您也可以使用擴充方法來擴充 Task 和 Task<TResult> 類別的功能。 如需擴充方法的詳細資訊,請參閱 擴充方法 和 擴充方法。
如果您必須繼承自 Task 或 Task<TResult>,則無法使用 Run 或 System.Threading.Tasks.TaskFactory、System.Threading.Tasks.TaskFactory<TResult>或 System.Threading.Tasks.TaskCompletionSource<TResult> 類別來建立自定義工作類型的實例。 您無法使用這些類別,因為這些類別只會建立 Task 和 Task<TResult> 物件。 此外,您無法使用 Task、Task<TResult>、TaskFactory和 TaskFactory<TResult> 提供的工作接續機制來建立自定義工作類型的實例。 您無法使用這些類別,因為這些類別也只會建立 Task 和 Task<TResult> 物件。
相關區段
標題 | 描述 |
---|---|
使用接續工作串聯任務 | 描述接續的運作方式。 |
附加和獨立的子工作 | 描述附加和獨立的子工作之間的差異。 |
工作取消 | 描述內建於 Task 物件的取消支援。 |
例外狀況處理 | 描述如何處理並行線程上的例外狀況。 |
如何:使用 Parallel.Invoke 執行平行作業 | 描述如何使用 Invoke。 |
如何從任務中傳回值 | 描述如何從任務傳回值。 |
如何:取消任務及其子任務 | 描述如何取消工作。 |
如何:建立預先計算的工作 | 描述如何使用 Task.FromResult 方法來擷取快取中保留的異步下載作業結果。 |
如何:以平行運算遍歷二元樹 | 描述如何使用任務來遍歷二進位樹狀結構。 |
如何:展開巢狀任務 | 示範如何使用 Unwrap 擴充方法。 |
數據平行處理原則 | 描述如何使用 For 和 ForEach 來建立數據的平行迴圈。 |
平行程序設計 | .NET Framework 平行程序設計的最上層節點。 |