17 November 2014

异步的开始点

public async Task<string> DownloadAsync(string url)
{
	...
	Task<string> content = client.GetStringAsync(url);
	...
	string result = await conent;
	return result;
}

上面的代码中,当方法执行到 client.GetStringAsync(url) 时会异步执行 GetStringAsync,并立即返回一个 Task<stirng> 对象给 content,方法继续执行,直到遇到 await 后, 如果 content 还未完成,则立即返回给方法的调用者,并开启一个新的线程执行后续代码(即方法体中的剩余代码,包括在循环体内);如果可以立即获得结果,会继续在主线程执行方法体中剩余的代码并返回,方法调用者获取 Task<string> 对象后,可以继续往下执行,从而避免了主线程被阻塞,从而实现我们所需要的异步效果。

自定义 async 方法

await 一个自己定义的普通方法并不会使方法的执行开启一个新的线程,而是当真正调用 FCL 中的可等待方法时才有可能会开启一个新的线程。

public async Task MyMethodAysnc()
{
	Task<string> result = await DownloadAsync("http://...");
	...
}

这里的 await DownloadAsync("http://...") 并不会开始异步执行,而是直到执行到方法体中的 client.GetStringAsync(url) 时,才会开始真正的异步。

异常捕获

async 方法的返回值类型有 void, Task, Task<T>,其中 void 返回类型导致异常直接抛出,而无法被 try/catch 捕获到,只用于事件注册。TaskTask<T> 返回类型默认会把异常包装到 Task 对象中,只有调用该对象的相关方法如,awaitTask 才会触发异常并抛出。

注意 async lambda 要使用 Func<Task> 作为类型,因为 Action 类型默认返回值为 void,会导致异常无法被正常捕获。

使用 async 的标准规范是 async all the way,但是当主方法无法使用 async 关键字时,如 static void Main(),这时我们可以定义一个中间方法,这个中间方法的返回值为 Task,因此在主方法中调用该中间方法而不去 await 它。但是,这会引发另外一个问题,该中间方法的后续代码会被提前执行,由于我们没有显示地等待中间方法执行结束,我们可以使用 Wait() 或者 Result 方式,但这种做法会阻塞线程,且容易造成死锁。一种方式是在 Task 上调用 ContinueWith 方法,并通过指定 TaskContinuationOptionsOnlyOnRanToCompletionOnlyOnFaulted 来处理任务完成或失败的情况;另一种方式是显示地使用 Task 手动创建一个任务并执行它,但由于我们显示地创建 Task,所有发生在 Task 中的异常都无法被抛出,因此我们需要在 Task 的执行方法中显示地使用 try/catch 进行异常捕获。getting start with async await

TaskScheduler

TaskScheduler 提供了获取 Scheduler 的三种方式:

  • Current: 获取 ThreadPoolTaskScheduler
  • Default: 获取 ThreadPoolTaskScheduler
  • FromCurrentSynchronizationContext (): 只能从 UI 线程中调用并得到包含 SynchronizationContext.CurrentSynchronizationContextTaskScheduler,从非 UI 线程获取会触发异常

在使用 Task.Factory.StartNew 时,我们可以指定 TaskScheduler,指定任务调度所在的线程。通常应该优先考虑使用 Task.Run

我们也可以定义自己的 TaskScheduler

上下文

同步上下文

SynchronizationContext 是用于解决当不同的应用程序,比如 ASP.NETWinForm 或者 WPF 共用某一类库时,如果想要执行更新 UI 操作,必须区分对待不同的应用以获得 UI ThreadSynchronizationContext 使得我们可以在类库中忽略具体的应用类型,不同的应用类型提供一个具体的 Concrete SynchronizationContext,而我们在类库中只需使用 SynchronizationContext 即可。UIKitSynchronizationContext

同步上下文与线程有关,在 UI 线程中请求 SynchronizationContext.Current 得到如 UIKitSynchronizationContext 的上下文对象;如果在工作线程中请求 SynchronizationContext.Current 得到 null,表示默认使用 SynchronizationContext 的实现。

SynchronizationContext 的默认实现与 UI 无关,在其调用线程上执行同步委托,在 ThreadPool 中执行异步委托。

await 默认会捕获同步上下文,如果是 GUI 程序,捕获的是 UI 线程,所以 await 后的更新 UI 界面的操作是合法的;如果是 Console 程序,默认捕获的是线程池线程。然而,捕获 UI 线程会导致占用主线程资源,因此最好的方式是不捕获 UI 线程,使用线程池线程来执行。

// 使用线程池线程
await DownloadAsync("http://...").ConfigAwait(false);

执行不进行 UI 操作的所有异步方法应该使用 ConfigAwait(false) 来提升性能,特别是类库中异步方法。 之所以是所有,是因为:

  • await 的方法立即执行结束时,此时该方法仍执行在 UI 线程上,后续方法依然需要配置 ConfigAwait(false) 才能避免捕获同步上下文。
  • 每个方法的上下文捕获是独立的,如 A -> B, 在 B 中使用 ConfigAwait(false) 后,B 中剩余的方法在线程池线程上执行,而 A 中的剩余方法仍然在 UI 线程中执行。

执行上下文

线程执行时默认会拥有自己的上下文,上下文中保存的线程执行时寄存器中的值和一些环境信息,当启动一个新的线程时,默认执行上下文会流向新线程,以便于新线程能够访问原线程的一些信息,通常我们不会去考虑执行上下文。关于同步上下文与执行上下文 ExecutionContext vs SynchronizationContext

异步执行 I/O 密集型操作

using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8, true)) 
{ 
	await fs.ReadAsync(...);
} 

普通的 I/O 操作,当线程从 user-mode 到 kernel-mode 读取磁盘文件时,由于读取磁盘文件的操作不涉及到线程,因此线程会被阻塞,直到文件读取完成,线程获取读取结果并恢复执行。而异步的 I/O 操作,上面 true 参数和 ReadAsync 方法结合使用,当读取磁盘文件时,线程不会被阻塞,而是立即返回,如果是线程池线程,则返回线程池,等下执行下一个任务单元。这种方式减少了不必要线程的创建,从而解决了系统资源,提升了性能,特别是在执行数据库访问操作时,大大增加了服务器的可响应性,可扩展性。Performing IO Bounds Asynchronous Operations

状态机

编译器会为我们使用 await 的代码自动生成状态机的实现。如

private static async Task<int> HttpLengthAsync(string uri)
{
    string text = await new HttpClient().GetStringAsync(uri);
    return text.Length;
}

经过编译之后会生成类似下面的代码:

private static Task<int> HttpLengthAsync(string uri)
{
    // create state machine and initialize it
    var sm = new HttpLengthSM {
        m_builder = AsyncTaskMethodBuilder<int>.create();
        uri = uri
    };
    
    // start running state machine
    sm.m_builder.start(ref sm);
    // return its task to the caller
    return sm.m_builder.Task;
}

private struct HttpLengthSM: IAsyncStateMachine
{
    public string uri, text;  // params & local variable
    public AsyncTaskMethodBuilder<int> m_builder;
    public int m_state = 0;
    private TaskAwaiter<string> m_awaiter;
    
    private void MoveNext()
    {
        int length;
        try
        {
            if(m_state == 0)
            {
                m_awaiter = new HttpClient.GetStringAsync(this.uri).GetAwaiter();
                if(!m_awaiter.IsCompleted)
                {
                    // if not completed synchronously, prepare to call back
                    m_state = 1;
                    m_awaiter.OnCompleted(this.MoveNext);
                    return;
                }
            }
            
            text = m_awaiter.GetResult();  // completed
            length = text.Length;
        }
        catch(Exception ex)
        {
            m_builder.SetException(ex);
        }
        m_builder.SetResult(length);
    }
}

AsyncTaskMethodBuilder<int> 用于实现可等待的 Task,处理异常或返回最终的结果,TaskAwaiter<string> 用于实际执行请求并获取请求结果。

Note:

The zen of async: Best practices for best performance

Cache tasks when applicable.
Use ConfigureAwait(false) in libraries.
Avoid gratuitous use of ExecutionContext.
Remove unnecessary locals.
Avoid unnecessary awaits.

Parallel Programming in .NET Framework 4 / Why use async await / Asynchronous Programming / Scott Hanselman / asp.net / Be Aware Of The Synchronization-Context / Understanding-SynchronizationContext / Synchronous and Asynchronous I/O / Extending The Async Methods