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






4.96/5 (73投票s)
C# (.NET 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 中有三种不同的异步模式
- 异步编程模型 (APM) 模式
- 基于事件的异步模式 (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
- 基于任务的异步模式 (TAP):推荐此模型,因此我们将详细讨论它
是否需要线程
如果我们使用 .NET 4.5 中引入的异步编程模式,在大多数情况下我们不需要手动创建线程。编译器完成了开发人员过去常做的困难工作。
创建新线程成本很高,需要时间。除非我们需要控制一个线程,否则“基于任务的异步模式 (TAP)”和“任务并行库 (TPL)”足以用于异步和并行编程。TAP 和 TPL 使用 Task(我们稍后会讨论什么是 Task)。通常 Task 使用线程池中的线程(*线程池*是由 .NET 框架已经创建和维护的线程集合。如果我们使用 Task,大多数情况下我们不需要直接使用线程池。如果您想了解更多关于线程池的信息,请访问此链接:https://msdn.microsoft.com/en-us/library/h4732ks0.aspx)
但 Task 可以运行
- 在当前线程中
- 在新线程中
- 在线程池中的线程中
- 甚至不需要任何线程
但如果我们使用 Task,作为开发人员,我们无需担心线程的创建或使用,.NET 框架为我们处理内部的困难。
无论如何,如果我们需要对线程进行一些控制,例如,
- 我们想为线程设置名称
- 我们想为线程设置优先级
- 我们想让我们的线程成为前台或后台
那么我们可能需要使用 thread 类创建自己的线程。
使用 Thread 类创建线程
Thread 类的构造函数接受一个委托参数,类型为
ThreadStart
:此委托定义一个返回类型为 void 且无参数的方法。- 和
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”,该方法必须返回“可等待”类型。以下是“可等待”的类型
- 任务
- Task<T>
- 自定义“可等待”类型。使用自定义类型是一种罕见且高级的场景;我们在此不讨论它。
基于任务的异步模式
首先,我们需要一个返回 Task 或 Task<T> 的异步方法。我们可以通过以下方式创建 Task
Task.Factory.StartNew
方法:在 .NET 4.5 之前(在 .NET 4 中),这是创建和调度任务的主要方法。Task.Run
或Task.Run<T>
方法:从 .NET 4.5 开始应使用此方法。此方法足以满足大多数常见情况。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>
方法创建我们的任务。此方法将指定的工作排队到线程池上运行,并返回该工作的任务句柄。从同步方法创建异步任务需要以下步骤
- 假设我们有一个同步方法,但完成需要一些时间
static string Greeting(string name)
{
Thread.Sleep(3000);
return string.Format("Hello, {0}", name);
}
- 要异步访问此方法,我们必须将其包装在一个异步方法中。假设名称是“
GreetingAsync
”。约定是在异步方法名称后添加“Async”后缀。
static Task<string> GreetingAsync(string name) { return Task.Run<string>(() => { return Greeting(name); }); }
- 现在我们可以使用 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)
”
- 任务延续:“
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
类取消已启动任务的方法,取消任务的步骤
- 异步方法应接受类型为“
CancellationToken
”的参数 - 创建
CancellationTokenSource
类的实例,例如
var cts = new CancellationTokenSource();
- 将
CancellationToken
从实例传递给异步方法,例如
Task<string> t1 = GreetingAsync("Bulbul", cts.Token);
- 在长时间运行的方法中,我们必须调用
CancellationToken
的 ThrowIfCancellationRequested() 方法。
static string Greeting(string name, CancellationToken token) { Thread.Sleep(3000); token. ThrowIfCancellationRequested(); return string.Format("Hello, {0}", name); }
- 从我们等待任务的地方捕获 OperationCanceledException。
- 现在,如果通过调用 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 或核心。有两种类型的并行性可能,
- 数据并行性:如果我们有一个大数据集合,并且我们希望对每个数据并行执行某些操作,那么我们可以使用数据并行性。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(); });
在中断循环时要小心,因为它在多个线程中运行,可能会比断点运行得更远。我们不应该根据断点做出任何决定。
- 任务并行性:如果我们要并行运行多个任务,我们可以通过调用 Parallel 类的 invoke 方法来使用任务并行性。
Parallel.Invoke
方法接受一个 Action 委托数组。例如
static void ParallelInvoke() { Parallel.Invoke(MethodOne, MethodTwo); }
结论
我试图介绍 .NET 框架 4.5 提供的异步编程技术。我试图保持事情简单,没有深入探讨高级细节。许多示例和参考文献都来自 Wrox 的“Professional C# 2012 and .NET 4.5”。