基于任务的异步编程

任务并行库(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())

注释

Task 在后台创建的 Invoke 实例数不一定与所提供的委托数相等。 TPL 可能会使用各种优化,特别是对于大量的委托。

有关详细信息,请参阅 如何:使用 Parallel.Invoke 执行并行操作

若要更好地控制任务执行或从任务返回值,必须更显式地处理 Task 对象。

显式创建和运行任务

不返回值的任务由 System.Threading.Tasks.Task 类表示。 返回值的任务由 System.Threading.Tasks.Task<TResult> 继承自 Task的类表示。 任务对象处理基础结构详细信息,并提供在任务整个生存期内从调用线程访问的方法和属性。 例如,可以随时访问 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 '{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 '{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

TaskTask<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($"{sum:N1}");
   }

   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 #{data.Name} created at {data.CreationTime} on thread #{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 #{data.Name} created at {data.CreationTime} on thread #{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,该 ID 可以唯一标识在应用程序域中它,并且可以通过 Task.Id 属性进行访问。 此 ID 可用于在 Visual Studio 调试器 并行堆栈任务 窗口中查看任务信息。 该 ID 是以惰性方式创建的,这意味着请求该 ID 时才会创建该 ID。 因此,每次运行程序时,任务可能具有不同的 ID。 有关如何在调试器中查看任务 ID 的详细信息,请参阅 “使用任务窗口 ”和 “使用并行堆栈窗口”。

任务创建选项

创建任务的大多数 API 提供接受 TaskCreationOptions 参数的重载。 通过指定一个或多个选项,您可以告知任务调度程序如何在线程池上调度任务。 可以使用按位 OR 操作来组合选项。

以下示例显示了具有 LongRunningPreferFairness 选项的任务:

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.CurrentCultureThread.CurrentUICulture 属性定义。 线程的区域性用在格式设置、分析、排序和字符串比较等操作中。 线程的 UI 区域性用于查找资源。

系统区域性定义线程的默认区域性和 UI 区域性。 但你可以使用 CultureInfo.DefaultThreadCurrentCultureCultureInfo.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 {Thread.CurrentThread.ManagedThreadId}");
       // Make the current culture different from the system culture.
       Console.WriteLine($"The current culture is {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 {CultureInfo.CurrentCulture.Name}.\n");

       // 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.ContinueWithTask<TResult>.ContinueWith 方法,可以指定要在 前面的任务 完成时启动的任务。 延续任务的委托被传递给对先行任务的引用,以便它查看先行任务的状态。 通过检索属性 Task<TResult>.Result 的值,可以将前序操作的输出用作后续操作的输入。

在以下示例中,任务 getData 通过调用 TaskFactory.StartNew<TResult>(Func<TResult>) 方法启动。 processData 任务会在 getData 完成后自动启动,而 displayData 任务则会在 processData 完成后启动。 getData生成一个整数数组,该数组可通过processData任务的getData属性被Task<TResult>.Result任务访问。 任务 processData 处理该数组并返回一个结果,其类型是从传递给 Task<TResult>.ContinueWith<TNewResult>(Func<Task<TResult>,TNewResult>) 方法的 lambda 表达式的返回类型推断出来的。 displayData任务会在processData完成后自动执行,由Tuple<T1,T2,T3> lambda 表达式返回的processData对象可以通过displayData任务的processData属性被Task<TResult>.Result任务访问。 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

使用 ContinueWhenAllContinueWhenAny 方法可以继续执行多个任务。

有关详细信息,请参阅使用延续任务链接任务

创建分离的子任务

在任务中运行的用户代码会创建新任务且未指定 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 #{x} completed.");
                                               },
                                               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.TaskSystem.Threading.Tasks.Task<TResult> 类型提供了 Task.Wait 方法的若干重载,以便能够等待任务完成。 此外,静态 Task.WaitAllTask.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 方法是在该任务完成之后调用的。

组合任务

这些 TaskTask<TResult> 类提供了多种方法来帮助你撰写多个任务。 这些方法实现常见模式,并更好地利用 C#、Visual Basic 和 F# 提供的异步语言功能。 本部分介绍WhenAllWhenAnyDelayFromResult方法。

Task.WhenAll

该方法 Task.WhenAll 异步等待多个 TaskTask<TResult> 对象完成。 通过它提供的重载版本可以等待非均匀任务组。 例如,您可以通过一次方法调用来等待多个 TaskTask<TResult> 对象完成。

Task.WhenAny

该方法 Task.WhenAny 异步等待多个 TaskTask<TResult> 对象之一完成。 与在 Task.WhenAll 方法中一样,该方法提供重载版本,让你能等待非均匀任务组。 此方法 WhenAny 在以下方案中特别有用:

  • 冗余操作:考虑可通过多种方式执行的算法或操作。 可以使用WhenAny 方法选择首先完成的操作,然后取消剩余的操作。

  • 交错操作:您可以启动多个必须完成的操作,并使用 WhenAny 方法在每个操作完成时处理结果。 一个操作完成后,可以启动一个或多个任务。

  • 受限操作:可以使用该方法通过限制并发操作的数量来扩展前面的方案。

  • 过期操作:可以使用 WhenAny 方法在一个或多个任务之间以及在特定时间后完成的任务之间进行选择,例如由 Delay 方法返回的任务。 以下部分介绍了该方法 Delay

Task.Delay

Task.Delay 方法将生成在指定时间后完成的 Task 对象。 可以使用此方法生成轮询数据的循环、指定超时、延迟用户输入的处理等。

任务(T)。FromResult

通过使用该方法 Task.FromResult ,可以创建一个 Task<TResult> 包含预计算结果的对象。 当您执行返回 Task<TResult> 对象的异步操作,并且该 Task<TResult> 对象的结果已计算时,此方法非常有用。 有关使用 FromResult 检索缓存中保存的异步下载结果的示例,请参阅 How to: Create Pre-Computed Tasks

处理任务中的异常

当任务引发一个或多个异常时,这些异常将被包装在一个AggregateException 异常中。 该异常会传播回与任务联接的线程。 通常,它是等待任务完成的线程或正在访问Result属性的线程。 此行为强制实施默认情况下所有未经处理的异常应终止进程的 .NET Framework 策略。 调用代码可以通过在 try/catch 块中使用任意方法来处理异常:

联接线程还可以通过在垃圾回收任务之前访问 Exception 属性来处理异常。 通过访问此属性,可以阻止未经处理的异常触发在完成对象时终止进程的异常传播行为。

有关异常和任务的详细信息,请参阅 异常处理

取消任务

Task 类支持协作取消,并与在 .NET Framework 4 中引入的 System.Threading.CancellationTokenSourceSystem.Threading.CancellationToken 类完全集成。 类中的 System.Threading.Tasks.Task 许多构造函数采用对象 CancellationToken 作为输入参数。 StartNewRun的许多重载还包括一个CancellationToken参数。

可以使用 CancellationTokenSource 类来创建令牌,并在稍后的时间发出取消请求。 可以将该标记作为参数传递给 Task,还可以在执行响应取消请求的工作的用户委托中引用同一标记。

有关详细信息,请参阅 任务取消如何:取消任务及其子任务

TaskFactory 类

TaskFactory 类提供静态方法,这些方法封装用于创建和启动任务和延续任务的常见模式。

TaskFactory默认值可以作为Task类或Task<TResult>类上的静态属性进行访问。 你还可以直接实例化一个TaskFactory,并指定各种选项,包括一个CancellationToken选项、一个TaskCreationOptions选项、一个TaskContinuationOptions选项或一个TaskScheduler选项。 创建任务工厂时指定的任何选项都将应用于它创建的所有任务,除非该任务是通过使用 Task 枚举创建的,在这种情况下,任务的选项将替代任务工厂的选项。

无委托的任务

在某些情况下,你可能会想使用Task来封装由外部组件而非用户委托执行的一些异步操作。 如果操作是基于异步编程模型的 Begin/End 模式,则可以使用FromAsync 方法。 如果不是这种情况,您可以使用TaskCompletionSource<TResult>对象将操作包装到任务中,从而获得一些可编程性的优点Task。 例如对异常传播和延续的支持。 有关详细信息,请参阅 TaskCompletionSource<TResult>

自定义调度器

大多数应用程序或库开发人员并不关心任务在哪个处理器上运行、如何与其他任务同步工作,也不关心任务如何在System.Threading.ThreadPool上被调度。 它们仅要求它在主计算机上尽可能高效地执行。 如果需要对计划详细信息进行更精细的控制,TPL 允许你在默认任务计划程序上配置某些设置,甚至可以提供自定义计划程序。 有关详细信息,请参阅 TaskScheduler

TPL 具有几种新的公共类型,这些类型在并行和顺序方案中很有用。 在System.Collections.Concurrent命名空间中,包括几个线程安全、快速且可扩展的集合类,以及几种新的同步类型。 例如,System.Threading.SemaphoreSystem.Threading.ManualResetEventSlim在处理特定类型的工作负荷时,比它们的前身更加高效。 在 .NET Framework 4 中,还有其他新的类型,例如 System.Threading.BarrierSystem.Threading.SpinLock,它们提供了早期版本中不可用的功能。 有关详细信息,请参阅 用于并行编程的数据结构

自定义任务类型

建议不要从System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult>继承。 相反,建议使用 AsyncState 属性将其他数据或状态与 Task 某个或 Task<TResult> 对象相关联。 还可以使用扩展方法来扩展TaskTask<TResult>类的功能。 有关扩展方法的详细信息,请参阅扩展方法和扩展方法

如果必须从TaskTask<TResult>继承,则不能使用RunSystem.Threading.Tasks.TaskFactorySystem.Threading.Tasks.TaskFactory<TResult>System.Threading.Tasks.TaskCompletionSource<TResult>类来创建自定义任务类型的实例。 因为这些类仅创建 TaskTask<TResult> 对象,你不能使用它们。 此外,不能使用TaskTask<TResult>TaskFactoryTaskFactory<TResult>提供的任务延续机制来创建自定义任务类型的实例。 不能使用这些类,因为这些类也仅创建TaskTask<TResult>对象。

标题 DESCRIPTION
通过链式任务实现任务的延续 描述延续任务的工作方式。
附加和分离的子任务 描述附加子任务和分离子任务之间的差异。
任务取消 描述该对象 Task 内置的取消支持。
异常处理 描述如何处理并发线程上的异常。
如何使用 Parallel.Invoke 执行并行操作 介绍如何使用 Invoke
如何:从任务返回值 描述如何从任务中返回值。
如何:取消任务及其子级 介绍如何取消任务。
如何创建预先计算任务 介绍如何使用 Task.FromResult 该方法检索缓存中保存的异步下载作的结果。
如何:遍历具有并行任务的二进制树 描述如何使用任务遍历二叉树。
如何:解包嵌套任务 演示如何使用 Unwrap 扩展方法。
数据并行度 介绍如何使用 ForForEach 创建对数据的并行循环。
并行编程 .NET Framework 并行编程的顶级节点。

另请参阅