C#5.0
异步编程
在.NET Framework
中,有三种不同的模型来简化异步编程。
-
.NET1.x
中的异步编程模型(APM)
方式(类似Beginxx/Endxx
方法,使用IAsyncResult
和AsyncCallback
来传播回调和结果) -
.NET2.0
中的基于事件的异步模式(EAP)
方式(类似xxAsync/xxCompletedEventHandler
方法,使用事件
和EventArgs
来传播回调和结果) -
.NET4
引入并由.NET4.5
扩展的任务并行库TPL
(基于任务的异步模式(TAP)
,使用Task
来传播回调及结果)
任务并行库
Task Parallel Library
任务并行库 (TPL
) 是 System.Threading
和 System.Threading.Tasks
空间中的一组公共类型和 API
。 TPL
的目的是通过简化将并行和并发添加到应用程序的过程来提高开发人员的工作效率。 TPL
动态缩放并发的程度以最有效地使用所有可用的处理器。 此外,TPL
还处理工作分区、ThreadPool
上的线程调度、取消支持、状态管理以及其他低级别的细节操作。 通过使用 TPL,你可以在将精力集中于程序要完成的工作,同时最大程度地提高代码的性能。
C#5
的异步编程主要基于TPL,因此可以在适用于异步的地方编写同步形式的代码。地狱回调、事件订阅和错误处理都消失不见,取而代之的是能够清晰表达其意图的异步代码,并且是基于开发者熟悉的结构。
任务是用于实现称之为并发Promise模型
的构造。 简单地说,它们“承诺”,会在稍后完成工作,让你使用干净的 API
与 promise
协作。
-
Task
表示不返回值的单个操作。 -
Task<T>
表示返回T
类型的值的单个操作。
异步函数
C#5
引入异步函数(asynchronous function
)的概念。通常是指用async
修饰符声明的,可包含await
表达式的方法或匿名函数。它可以“等待”(await
)一个异步操作。这个“等待”看上去非常像一个普通的阻塞调用,剩余的代码在异步操作完成前不会继续执行,但实际上它并没有阻塞当前执行线程。
await运算符
await
运算符会暂停对其所属的async
方法的求值,直到其操作数表示的异步操作完成。
只能在通过async
关键字修饰的方法
或匿名函数
中使用 await
运算符。不能在同步函数的主体、lock
语句块内以及不安全的上下文中使用 await
运算符。
await表达式
Await_expression的操作数称为任务。 它表示一个异步操作,该操作在计算await_expression
时可能会也可能不会完成。 Await 运算符的用途是在等待的任务完成之前挂起封闭异步函数的执行,然后获取其结果。
Await 表达式的任务需要是可等待的(awaitable
)。
可等待的
这个可等待的其实就是具有一个名为GetAwaiter
的可访问实例或扩展方法,方法没有任何参数,返回类型具有以下特点:
- 实现了
System.Runtime.CompilerServices.INotifyCompletion
接口(只有一个void OnCompleted (Action continuation);
方法) - 具有可访问的
bool
类型的实例属性IsCompleted
- 具有可访问的实例方法
GetResult
,无任何参数和类型参数
GetAwaiter
方法的目的是获取任务的awaiter
。
IsCompleted
属性的目的是确定任务是否已完成。 如果已完成,则无需暂停评估。
INotifyCompletion.OnCompleted
方法的目的是将 "延续" 注册到任务;也就是说,将在任务完成后调用的委托(类型为System.Action
)。
GetResult
方法的目的是在任务完成后获取任务的结果。 此结果可能会成功完成(可能包含结果值),也可能是由 GetResult
方法引发的异常。
async/await
的核心,其实就是编译器帮我们自动实现的TAP异步方法。编译器在遇到用await
修饰的表达式或匿名函数时,帮我们自动生成了一个实现了IAsyncStateMachine
接口的状态机(StateMachine
),它在内部控制处理了Task
的状态。
我们还可以手动实现TAP异步方法,那就是创建一个TaskCompletionSource<T>
对象来执行异步操作,并在操作完成时调用SetResult
、SetException
、SetCanceled
方法。例如:
//同步转异步
public static Task<T> ConvertToAsync<T>(Func<T> func)
{
var tcs = new TaskCompletionSource<T>();
tcs.SetResult(func());
return tcs.Task;
}
//异步转同步
public static T ConvertToSync<T>(Func<Task<T>> taskFunc)
{
var tcs = new TaskCompletionSource<T>();
var task = tcs.Task;
Task.Run(async () =>
{
var result = await taskFunc();
tcs.SetResult(result);
});
return task.Result;
}
若还想继续了解更多,可在基于任务的异步模式中再详细了解任务以及任务交互的信息。
最后你可以再看下Stephen Cleary
大神写的async/await异步编程中的最佳做法
调用方信息特性
调用方信息特性,可以获取有关方法调用方的信息。可以获取源代码的文件路径、源代码中的行号和调用方的成员名称。 若要获取成员调用方信息,可以使用应用于可选参数的特性。 每个可选参数指定一个默认值。下表列出在 System.Runtime.CompilerServices
命名空间中定义的调用方信息特性:
特性 | 描述 | 类型 |
---|---|---|
CallerFilePathAttribute | 包含调用方的源文件的完整路径。 完整路径是编译时的路径。 | String |
CallerLineNumberAttribute | 源文件中调用方法的行号。 | Integer |
CallerMemberNameAttribute | 调用方的方法名称或属性名称。 | String |
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[CallerMemberName] string memberName = "",
[CallerFilePath] string sourceFilePath = "",
[CallerLineNumber] int sourceLineNumber = 0)
{
Trace.WriteLine("message: " + message);
Trace.WriteLine("member name: " + memberName);
Trace.WriteLine("source file path: " + sourceFilePath);
Trace.WriteLine("source line number: " + sourceLineNumber);
}
// Sample Output:
// message: Something happened.
// member name: DoProcessing
// source file path: c:\Visual Studio Projects\CallerInfoCS\CallerInfoCS\Form1.cs
// source line number: 31
foreach循环中捕获变量的变化
string[] values = { "x", "y", "z" };
var actions = new List<Action>();
foreach (string item in values)
{
actions.Add(() => Console.WriteLine(item));
}
foreach (Action item in actions)
{
item();
}
在C#3
和C#4
中,以上代码实际上会打印出三个z
。问题就在于由lambda表达式
捕获的循环变量item
,在循环的每次迭代中捕获的变量item
的引用都是相同。由于最终变量为z,所以打印了三个z
。
在C#5
中,循环的每次迭代都有效的引入一个单独变量。每个委托都捕获并引用不同的变量,这样变量的值就是这次循环迭代中产生的值。