65.9K
CodeProject 正在变化。 阅读更多。
Home

C# (.NET 4.5) 中的异步编程和线程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (73投票s)

2015年6月2日

CPOL

10分钟阅读

viewsIcon

213078

C# (.NET 4.5) 中异步编程和线程的介绍

异步编程和线程是并发或并行编程中非常重要的特性。异步编程可能使用也可能不使用线程。如果我们将它们放在一起看,我认为我们可能会对这两个特性有更好的理解。

本文涵盖的主题

  1. 异步编程
  2. 是否需要线程
  3. 基于任务的异步编程
  4. 并行编程
  5. 结论

异步编程

异步操作意味着该操作独立于主流程或其他流程运行。通常,C# 程序从 Main 方法开始执行,并在 Main 方法返回时结束。在此期间,所有操作都按顺序一个接一个地运行。一个操作必须等待其前一个操作完成。让我们看以下代码

static void Main(string[] args)
        {
           DoTaskOne();
           DoTaskTwo();
        }

方法“DoTaskTwo”只有在“DoTaskOne”完成之后才会启动。换句话说,方法“DoTaskOne”会阻塞执行,直到它完成。

在异步编程中,调用一个在后台运行的方法,并且调用线程不会被阻塞。调用方法后,执行流立即返回到调用线程并执行其他任务。通常它使用 Thread 或 Task(我们稍后会详细讨论 Thread 和 Task)。

在我们的例子中,如果异步运行“DoTaskOne”,在调用“DoTaskOne”方法后,执行流立即返回到 Main 方法并启动“DoTaskTwo”。

我们可以使用 Thread 类创建自己的线程,或者使用 .NET 提供的异步模式来执行异步编程。.NET 中有三种不同的异步模式

  1. 异步编程模型 (APM) 模式
  2. 基于事件的异步模式 (EAP)

以上两种模型均不被 Microsoft 推荐,因此我们不讨论它们。如果您感兴趣,可以从以下 msdn 链接了解更多信息

https://msdn.microsoft.com/en-us/library/ms228963(v=vs.110).aspx

https://msdn.microsoft.com/en-us/library/ms228969(v=vs.110).aspx

  1. 基于任务的异步模式 (TAP):推荐此模型,因此我们将详细讨论它

是否需要线程

如果我们使用 .NET 4.5 中引入的异步编程模式,在大多数情况下我们不需要手动创建线程。编译器完成了开发人员过去常做的困难工作。

创建新线程成本很高,需要时间。除非我们需要控制一个线程,否则“基于任务的异步模式 (TAP)”和“任务并行库 (TPL)”足以用于异步和并行编程。TAP 和 TPL 使用 Task(我们稍后会讨论什么是 Task)。通常 Task 使用线程池中的线程(*线程池*是由 .NET 框架已经创建和维护的线程集合。如果我们使用 Task,大多数情况下我们不需要直接使用线程池。如果您想了解更多关于线程池的信息,请访问此链接:https://msdn.microsoft.com/en-us/library/h4732ks0.aspx

但 Task 可以运行

  1. 在当前线程中
  2. 在新线程中
  3. 在线程池中的线程中
  4. 甚至不需要任何线程

但如果我们使用 Task,作为开发人员,我们无需担心线程的创建或使用,.NET 框架为我们处理内部的困难。

无论如何,如果我们需要对线程进行一些控制,例如,

  1. 我们想为线程设置名称
  2. 我们想为线程设置优先级
  3. 我们想让我们的线程成为前台或后台

那么我们可能需要使用 thread 类创建自己的线程。

使用 Thread 类创建线程

Thread 类的构造函数接受一个委托参数,类型为

  1. ThreadStart:此委托定义一个返回类型为 void 且无参数的方法。
  2. ParameterizedThreadStart:此委托定义一个返回类型为 void 且带有一个对象类型参数的方法。

以下是使用 Start 方法启动新线程的简单示例

static void Main(string[] args)
        {
            Thread thread = new Thread(DoTask);
            thread.Start();// Start DoTask method in a new thread
            //Do other tasks in main thread
        }
        static public void DoTask() {
            //do something in a new thread       
        }

我们可以使用 lambda 表达式代替命名方法

static void Main(string[] args)
        {
            Thread thread = new Thread(() => {
                //do something in a new thread
            });
            
            thread.Start();// Start a new thread
            //Do other tasks in main thread
        }

如果不需要变量引用,我们甚至可以直接启动线程,例如

static void Main(string[] args)
        {
            new Thread(() => {
                //do something in a new thread
            }).Start();// Start a new thread

            //Do other tasks in main thread
        }

但是,如果要在创建线程对象后对其进行控制,则需要变量引用。我们可以为对象的不同属性分配不同的值,例如

static void Main(string[] args)
        {
            Thread thread = new Thread(DoTask);
           
            thread.Name = "My new thread";// Asigning name to the thread
            thread.IsBackground = false;// Made the thread forground
            thread.Priority = ThreadPriority.AboveNormal;// Setting thread priority
            thread.Start();// Start DoTask method in a new thread
            //Do other task in main thread
        }

通过引用变量,我们可以执行一些功能,例如中止线程或通过调用 join 方法等待线程完成。如果我们在线程上调用 join,则主线程会阻塞,直到调用线程完成。

如果我们要将一些数据传递给方法,可以将其作为 Start 方法的参数传递。由于方法参数是对象类型,我们需要对其进行适当的类型转换。

static void Main(string[] args)
        {
         Thread thread = new Thread(DoTaskWithParm);
         thread.Start("Passing string");// Start DoTaskWithParm method in a new thread
         //Do other task in main thread
        }
        static public void DoTaskWithParm(object data)
        {

            //we need to cast the data to appropriate object

        }

“async”和“await”关键字

.NET 框架引入了两个新的关键字来执行异步编程:“async”和“await”。要在方法内使用“await”关键字,我们需要使用“async”修饰符声明该方法。“await”关键字在调用异步方法之前使用。“await”关键字会暂停方法的进一步执行,并将控制权返回给调用线程。请看示例

private async static void CallerWithAsync()// async modifier is used 
{
string result = await GetSomethingAsync();// await is used before a method call. It suspends //execution of CallerWithAsync() method and control returs to the calling thread that can //perform other task.

Console.WriteLine(result);// this line would not be executed before  GetSomethingAsync() //method completes
}

async”修饰符只能用于返回 Task 或 void 的方法。它不能用于程序的入口点 Main 方法。

我们不能在所有方法之前使用 await 关键字。要使用“await”,该方法必须返回“可等待”类型。以下是“可等待”的类型

  1. 任务
  2. Task<T>
  3. 自定义“可等待”类型。使用自定义类型是一种罕见且高级的场景;我们在此不讨论它。

基于任务的异步模式

首先,我们需要一个返回 Task 或 Task<T> 的异步方法。我们可以通过以下方式创建 Task

  1. Task.Factory.StartNew 方法:在 .NET 4.5 之前(在 .NET 4 中),这是创建和调度任务的主要方法。
  2. Task.RunTask.Run<T> 方法:从 .NET 4.5 开始应使用此方法。此方法足以满足大多数常见情况。
  3. Task.FromResult 方法:如果结果已计算,我们可以使用此方法创建任务。

Task.Factory.StartNew 在高级场景中仍然有一些重要的用途。请参阅此链接了解更多信息:http://blogs.msdn.com/b/pfxteam/archive/2011/10/24/10229468.aspx

以下链接展示了一些创建 Task 的方法:http://dotnetcodr.com/2014/01/01/5-ways-to-start-a-task-in-net-c/

创建并等待任务

我们将使用 Task.Run<T> 方法创建我们的任务。此方法将指定的工作排队到线程池上运行,并返回该工作的任务句柄。从同步方法创建异步任务需要以下步骤

  1. 假设我们有一个同步方法,但完成需要一些时间
       static string Greeting(string name)
        {
        Thread.Sleep(3000);

        return string.Format("Hello, {0}", name);

       }
  1. 要异步访问此方法,我们必须将其包装在一个异步方法中。假设名称是“GreetingAsync”。约定是在异步方法名称后添加“Async”后缀。
                      static Task<string> GreetingAsync(string name)
                         {
                               return Task.Run<string>(() =>
                                     {
                                        return Greeting(name);
                                      });
                     }
  1. 现在我们可以使用 await 关键字调用异步方法 GreetingAsync
                    private async static void CallWithAsync()
                        {
                             //some other tasks
                                string result = await GreetingAsync("Bulbul");
                                //We can add  multiple “await” in same “async” method
                               //string result1 = await GreetingAsync(“Ahmed”);
                              //string result2 = await GreetingAsync(“Every Body”);
                              Console.WriteLine(result);
                     }

当调用“CallWithAsync”方法时,它会像常规同步方法一样开始执行,直到遇到“await”关键字。当它遇到“await”关键字时,它会暂停方法的执行,并开始等待“GreetingAsync("Bulbul")”方法完成。与此同时,控制权返回给“CallWithAsync”方法的调用者,调用者可以像往常一样执行其他任务。

当“GreetingAsync("Bulbul")”方法完成后,“CallWithAsync”方法会在“await”关键字之后恢复其其他任务。在这种情况下,它会执行代码“Console.WriteLine(result)

  1. 任务延续:“ContinueWith”是 Task 类的方法,它定义了任务完成时应调用的代码。
                           private static void CallWithContinuationTask()
                                 {
                                    Task<string> t1 = GreetingAsync("Bulbul");
                                   t1.ContinueWith(t =>

                                        {

                                             string result = t.Result;

                                             Console.WriteLine(result);
                                       });
                               }

如果使用“ContinueWith”方法,我们不需要使用“await”关键字,编译器会将其放置在适当的位置。

等待多个异步方法

让我们看以下代码

                   private async static void CallWithAsync()
                        {
                             string result = await GreetingAsync("Bulbul");
                             string result1 = await GreetingAsync(“Ahmed”);
                             Console.WriteLine(result);
                            Console.WriteLine(result1);
                   }

这里我们正在顺序等待两次调用。第二次调用“GreetingAsync(“Ahmed”)”将在第一次调用“GreetingAsync("Bulbul")”完成后开始。如果上述代码中的“result”和“result1”不依赖,那么顺序“awaiting”不是一个好的做法。

在这种情况下,我们可以简单地不带“await”关键字调用方法,并通过组合器在单个位置“await”它们。在这种情况下,两个方法调用可以并行执行。

                      private async static void MultipleAsyncMethodsWithCombinators()
                        {

                           Task<string> t1 = GreetingAsync("Bulbul");

                           Task<string> t2 = GreetingAsync("Ahmed");

                           await Task.WhenAll(t1, t2);

                         Console.WriteLine("Finished both methods.\n " +

                        "Result 1: {0}\n Result 2: {1}", t1.Result, t2.Result);
                      }

这里我们使用 Task.WhenAll 组合器。Task.WhenAll 创建一个任务,该任务将在所有提供的任务完成后完成。Task 类还有另一个组合器 Task.WhenAny,它将在任何提供的任务完成后完成。

处理异常

我们必须将“await”代码块放在 try 块内以处理方法的异常。

                   private async static void CallWithAsync()
                      {
                          try
                          {
                              string result = await GreetingAsync("Bulbul");
                          }
                        catch (Exception ex)
                         {
                        Console.WriteLine(“handled {0}”, ex.Message);
                        }
             }

如果 try 块内有多个“await”,则只会处理第一个异常,并且不会到达下一个“await”。如果即使某些方法抛出异常,我们也希望所有方法都被调用,我们必须不带“await”关键字调用它们,并使用 Task.WhenAll 方法等待所有任务。

        private async static void CallWithAsync()
              {
                 try
                   {

                       Task<string> t1 = GreetingAsync("Bulbul");

                      Task<string> t2 = GreetingAsync("Ahmed");

                      await Task.WhenAll(t1, t2);
                  }
               catch (Exception ex)
               {
               Console.WriteLine(“handled {0}”, ex.Message);
             }
       }

尽管所有任务都将完成,但我们只能从第一个任务中看到异常。这不是第一个抛出异常的任务,而是列表中的第一个任务。

获取所有任务错误的另一种方法是在 try 块之外声明它们,以便可以从异常块访问它们,然后检查任务的“IsFaulted”属性。如果它有异常,则“IsFaulted”属性将为 true。然后我们可以通过任务实例的内部异常获取异常。

但还有另一种更好的方法,像这样

        static async void ShowAggregatedException()
           {
              Task taskResult = null;
              try 
             {
                  Task<string> t1 = GreetingAsync("Bulbul");
                  Task<string> t2 = GreetingAsync("Ahmed");
                  await (taskResult = Task.WhenAll(t1, t2));
           }
          catch (Exception ex)
          {
             Console.WriteLine("handled {0}", ex.Message);
             foreach (var innerEx in taskResult.Exception.InnerExceptions)
            {
               Console.WriteLine("inner exception {0}", nnerEx.Message);
           }
        }
  }

取消任务

以前,如果从 ThreadPool 使用线程,则无法取消线程。现在 Task 类提供了一种基于 CancellationTokenSource 类取消已启动任务的方法,取消任务的步骤

  1. 异步方法应接受类型为“CancellationToken”的参数
  2. 创建 CancellationTokenSource 类的实例,例如
                    var cts = new CancellationTokenSource();
  1. CancellationToken 从实例传递给异步方法,例如
                   Task<string> t1 = GreetingAsync("Bulbul", cts.Token);
  1. 在长时间运行的方法中,我们必须调用 CancellationTokenThrowIfCancellationRequested() 方法。
                  static string Greeting(string name, CancellationToken token)
                    {
                       Thread.Sleep(3000);
                       token. ThrowIfCancellationRequested();
                      return string.Format("Hello, {0}", name);
                 }
  1. 从我们等待任务的地方捕获 OperationCanceledException。
  2. 现在,如果通过调用 CancellationTokenSource 实例的 Cancel 方法取消操作,则会从长时间运行的操作中抛出 OperationCanceledException。我们还可以为实例设置取消操作的时间。有关 CancellationTokenSource 类的更多详细信息,请参阅以下链接:https://msdn.microsoft.com/en-us/library/system.threading.cancellationtokensource%28v=vs.110%29.aspx

让我们在一个示例代码中看到所有这些,在这个示例中,我们在一秒后取消操作

static void Main(string[] args)
        {
            CallWithAsync();
            Console.ReadKey();           
        }

        async static void CallWithAsync()
        {
            try
            {
                CancellationTokenSource source = new CancellationTokenSource();
                source.CancelAfter(TimeSpan.FromSeconds(1));
                var t1 = await GreetingAsync("Bulbul", source.Token);
            }
            catch (OperationCanceledException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

        static Task<string> GreetingAsync(string name, CancellationToken token)
        {
            return Task.Run<string>(() =>
            {
                return Greeting(name, token);
            });
        }

        static string Greeting(string name, CancellationToken token)
        {
            Thread.Sleep(3000);
            token.ThrowIfCancellationRequested();
            return string.Format("Hello, {0}", name);
        }

并行编程

.NET 4.5 及更高版本引入了一个名为“Parallel”的类,它是线程类的一种抽象。使用“Parallel”类,我们可以实现并行性。并行性与线程化的区别在于它使用所有可用的 CPU 或核心。有两种类型的并行性可能,

  1. 数据并行性:如果我们有一个大数据集合,并且我们希望对每个数据并行执行某些操作,那么我们可以使用数据并行性。Parallel 类具有静态的 For 或 ForEach 方法来执行数据并行性,例如
             ParallelLoopResult result =
                    Parallel.For(0, 100, async (int i) =>
                    {
                        Console.WriteLine("{0}, task: {1}, thread: {2}", i,
                        Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                        await Task.Delay(10);
                       
              });

Parallel For 或 ForEach 可能会使用多个线程,并且索引(代码中的 i)不是顺序的。

如果我们要提前停止 Parallel For 或 ForEach 方法,我们可以传递 ParallelLoopState 作为参数,并根据状态中断循环。

        ParallelLoopResult result =
                    Parallel.For(0, 100, async (int i, ParallelLoopState pls) =>
                    {
                        Console.WriteLine("{0}, task: {1}, thread: {2}", i,
                        Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                        await Task.Delay(10);
                        if (i > 5) pls.Break();
              });

在中断循环时要小心,因为它在多个线程中运行,可能会比断点运行得更远。我们不应该根据断点做出任何决定。

  1. 任务并行性:如果我们要并行运行多个任务,我们可以通过调用 Parallel 类的 invoke 方法来使用任务并行性。Parallel.Invoke 方法接受一个 Action 委托数组。例如
               static void ParallelInvoke()
                {
                    Parallel.Invoke(MethodOne, MethodTwo);

               }

结论

我试图介绍 .NET 框架 4.5 提供的异步编程技术。我试图保持事情简单,没有深入探讨高级细节。许多示例和参考文献都来自 Wrox 的“Professional C# 2012 and .NET 4.5”。

© . All rights reserved.