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

.NET 4.5 中的多线程支持改进

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.46/5 (38投票s)

2011 年 10 月 26 日

CPOL

5分钟阅读

viewsIcon

91042

downloadIcon

1094

简要概述 .NET 4.5 中对异步编程的新语言支持。

引言

这是对 .NET 4.5 中即将推出的多线程编程新语言支持的一次快速介绍。这些示例是使用去年 9 月发布的 Visual Studio 11 开发者预览版编程的。

多线程的演进

与 Java 一样,C# 从一开始就支持多线程。在早期,这意味着 CLR 中有一组同步类,以及 lock 语句。虽然这比调用系统库要好,但编写多线程代码的难度对于大多数开发人员和项目来说仍然太高。

但多线程编程越来越重要,因为计算机的未来发展方向是多核并行处理,而不是更快的时钟速度。因此,Microsoft 在几乎每个版本中都不断增强 .NET 的多线程支持。.NET 4 引入了任务并行库 (TPL),这是一个重要的概念性进步。.NET 4.5 通过将任务直接集成到语言中,在此基础上更进一步。

任务而非线程

这种最新的多线程方法是根本不考虑或处理线程!相反,您使用任务进行编程,任务只不过是将来某个时候可用的一个值(数据)(技术上讲,是“未来值”)。这有什么帮助?

目的是使并行代码尽可能像顺序代码一样编写和运行。考虑以下函数,它在显示结果之前调用另外两个耗时较长的函数。

// Calling code
..
Function();  // thread blocks here until Function completes
..


public void Function()
{
 string s1 = GetExpensiveString();
 string s2 = GetAnotherExpensiveString();
 Console.WriteLine(s1 + s2);
}

这段代码的问题在于,GetAnotherExpensiveString 必须在 GetExpensiveString 完成后才能开始。此外,调用代码(以及程序的其余部分)在两个耗时函数返回之前都会被阻塞。

在这个简单示例中,'expensive' 方法只是模拟耗时

private static string GetExpensiveString()
{
 for (int i=0; i < 5; i++)
    Thread.Sleep(1000); // allow other threads to jump in

 return DateTime.Now.ToLongTimeString();
}

// GetAnotherExpensiveString() the same

在 .NET 4.5 中,这段代码变为:

// Calling code
..
FunctionAsync();  // control returns well before FunctionAsync completes 
..


public async Task FunctionAsync()
{
 string s1 = await GetExpensiveStringAsync();
   .. control yields; s1 assigned sometime later
 string s2 = await GetAnotherExpensiveStringAsync();
   .. more time passes
 Console.WriteLine(s1 + s2);
}


private static Task<string> GetExpensiveStringAsync()
{
 return Task<string>.Factory.StartNew( () => GetExpensiveString() );
}

// GetAnotherExpensiveStringAsync the same

我们引入了两个关键字和一个命名约定,以指示控制可以切换到另一个线程然后回退到原处的位置。

让我们先看一下耗时的方法。它们已更改为返回 Task<string> 而不是 string,并且按照惯例,它们的名称现在以“Async”作为后缀。因此,它们不再返回我们想要的 string,而是返回一个 Task 对象,这是一种代理或承诺,表示“我稍后会将字符串提供给您。”由于我的耗时函数不访问任何共享变量,因此没有锁定问题,所以异步版本只是在任务中调用同步函数。

但我仍然将返回值赋给字符串变量。这之所以可行,是因为在每个调用前面都有 await 关键字。这本质上表示“每当此任务完成时,请回到这里,为其赋值,然后继续。”

另一个更改是函数签名:我们用 async 关键字装饰了函数,将返回值更改为 Task,并再次按照惯例添加了“Async”后缀。这些更改告诉编译器和其他程序员,此函数包含异步控制流。

异步控制流

我们的函数看起来很相似(这就是目的),但行为却截然不同。当您调用 Function 时,您会等待一段时间,直到写入控制台才返回。在 FunctionAsync 中,对 GetExpensiveStringAsync 的调用会在另一个线程上启动一个任务,然后立即返回,此时此线程上的控制流会继续。

过了一会儿,任务完成,CLR 会在 FunctionAsync 中恢复执行。在这种情况下,过程会重复:CLR 启动另一个后台任务,过了一段时间后再回到这里以最终执行控制台写入。

请注意,与 Function 不同,FunctionAsync 的调用者在快速返回后会继续执行。现在,您可以并行执行其他工作,并在需要时与 FunctionAsync 同步。这是修改后的调用代码:

// Original calling code
// Function();  // thread blocks here until Function completes ...

Task task = FunctionAsync();

// .. do other work here in parallel with FunctionAsync ..

task.Wait();  // block here in case FunctionAsync is still running

// FunctionAsync has finished. If it had a return value, 
// retrieve it from the task like this:
// var result = task.Result;

那么,是什么使得这一切得以工作呢?

在底层,C# 已将我们的简单函数转换成了一个状态机,并创建了隐藏类,这些类基本上将调用堆栈保存在堆上。每个 await 表达式都标记了一个函数可以切换线程或“暂停”然后稍后恢复的位置。

这在概念上都很简单,但在细节上却相当复杂。这尤其适用于恢复复杂的控制流(例如嵌套的 if 和循环)以及传播异常。这与迭代器中 yield 语句的工作方式有相似之处,并且也利用了对最初通过 lambda 表达式引入的闭包中的局部变量的捕获。

更多异步

我们在这里实现了一些并行执行,但我们可以做得更好。我们将 GetExpensiveString 移到后台,以便其他处理可以并行进行。但按照目前的结构,GetAnotherExpensiveStringGetExpensiveString 完成之前无法开始执行。这个最终版本解决了这个问题。

public async Task FunctionAsync()
{
    Task<string> t1 = GetExpensiveStringAsync();
    Task<string> t2 = GetAnotherExpensiveStringAsync();
 
    await Task.WhenAll(t1, t2);
 
    string s1 = t1.Result;
    string s2 = t1.Result;
    Console.WriteLine(s1 + s2);
}

为了让两个耗时函数并行运行,我们必须更直接地处理任务对象。在这里,我们使用任务的 WhenAll 方法来等待所有其他任务完成。.NET 4.5 的 Task API 添加了几个新方法,可以非常方便地组合和同步任务。

总结

不要被迷惑:这里没有魔法。异步编程**仍然**是困难和复杂的。这个简单的示例程序最终运行在四个不同的线程上。现在它不像以前那样容易重复,因为它受每次运行时的时序变化的影响,尤其是在多核处理器上。

顺序代码执行是每个程序员大脑中根深蒂固的基本期望。异步编程总是很困难,因为它违反了这一基本假设。它迫使您以不同的方式思考您的程序。在我开始掌握这些新特性之前,我花了大约一天的时间来摸索(挣扎)。从这个意义上说,它可能与学习 LINQ 或 lambda 表达式相似。一如既往,Microsoft 的工具、示例和文档支持将帮助您快速掌握。

这是值得的,因为收益非常显著:在这个例子中,每个耗时函数需要五秒钟才能执行,而最终的并行程序总共只需要五秒多一点,而原始版本需要十秒。除了速度提升之外,在生产环境中真正的优势是可以保持 UI 实时响应。

新的 .NET 异步编程模型是一个巨大的进步。与其他领域一样,它允许您在更高的层面进行思考和编程,同时它会管理细节。编译器和运行时中有很多复杂的机制来实现这一点,这也应该是这样的。

历史

  • 2011 年 10 月 27 日 - 根据反馈进行了修订。
  • 2011 年 10 月 26 日 - 原始文章。
© . All rights reserved.