c#异步编程:async/await的使用方法

本文详细介绍了c#中async-await异步编程模型的使用方法,但并不涉及其原理。本文分为四个部分:异步方法的执行方式、异步方法的返回值、等待异步方法执行完毕和异步方法的简单异常处理。

1.异步方法的执行方式

使用async关键字声明的方法为异步方法,其中可包含await关键字,下面举例说明:

public async static Task AddEvent(Entry entry)
{     
    HttpClient sendLogClient = new HttpClient();
    var values = new Dictionary<string, string>
    {
        { "token", "xxx" },
        { "type", entry.type.ToString() },
        { "subj", entry.subj },
        { "obj", entry.obj},
        { "time", entry.time.ToString("yyyy-MM-dd HH:mm:ss") },
    };
            
    var content = new FormUrlEncodedContent(values);

    try
    {
        HttpResponseMessage response = await sendLogClient.PostAsync("https://api.knightdusk.cn/set_entry.php", content);
        AddLog(response.ToString());
    }
    catch(TaskCanceledException)//超时重试
    {
        Task.Delay(10000).Wait();
        AddEvent(entry).Wait();
    }
    catch(Exception exp)
    {
        AddLog(exp.Message);
    }
}

此函数实现的功能是使用HTTP POST方法异步地向api.knightdusk.cn下的一个php页面发送数据。函数首先根据传入参数进行数据的组织:将要POST的数据放入Dictionary类的键值对中,然后使用FormUrlEncodedContent函数形成HTTP POST所需要的数据(不是json,而是类似于表单的数据)。直到这里,程序都是同步执行的,与普通的函数尚无任何区别。接下来在try块类使用如下包含await函数的语句

HttpResponseMessage response = await sendLogClient.PostAsync("https://api.knightdusk.cn/set_entry.php", content);

注意到PostAsync和我们定义的方法一样是一个异步方法。await是“异步地等待方法执行”,在一个异步方法中以await方法调用另一个异步方法以后,外层异步方法将立即返回。举例来讲,如果我们在其他地方调用上述函数:

public void SomeFunction()
{
    task = SomeClass.AddEvent(entry);
    Console.WriteLine("我也不知道我是来干嘛的");
    task.Wait();
}

当AddEvent执行到await时,将立即返回。换句话说,sendLogClient.PostAsync以及await之后的其他语句将和Console.WriteLine在两个线程中同时执行。

2.异步方法的返回值

异步方法只可用三种返回类型:void,Task和Task<T>,下面举例说明其含义

//SomeClass.AddEvent()方法的返回类型是Task
Task this_is_a_task = SomeClass.AddEvent(); //任何方法中可用
await SomeClass.AddEvent();//仅在async声明的异步方法中方可用

//sendLogClient.PostAsync()方法的返回类型是Task<HttpResponseMessage>;
Task<HttpResponseMessage>; this_is_another_task = sendLogClient.PostAsync();
//任何方法中可用
HttpResponseMessage response = await sendLogClient.PostAsync();    
//仅在async声明的异步方法中方可用

public void AFunction()
{
    Task<int> task = AnotherFunction();
}

public async Task&lt;int&gt; AnotherFunction()
{
    //这里有一些语句,省略
    HttpResponseMessage response = await sendLogClient.PostAsync();
    //另外一些语句
    return SomeInt;
}

前面已经讲过,在AnotherFunction() 中使用await将导致AnotherFunction()所在的方法立刻返回,该返回值就是Task或者Task<T>泛型,其中包含了异步线程的信息。另一方面,await sendLogClient.PostAsync();直接返回方法的最终执行结果,即Task<T>中的类型T。这个值是在方法最后由return语句定义的。也就是说,await后面的方法将一次执行完毕,返回最终结果。

3.等待异步线程执行完毕

再考虑之前已经讲过的一个例子:

public void SomeFunction()
{
    task = SomeClass.AddEvent(entry);
    Console.WriteLine("我也不知道我是来干嘛的");
    task.Wait();
}

现考虑删除掉task.Wait();语句。这样一来,我们便不能保证在SomeFunction返回之前AddEvent可以执行完毕。假如SomeFunction换为主函数,则情况更为糟糕:输出文字之后程序结束关闭,此时访问链接的函数多半没有运行完毕,并且再也没有机会运行完毕了。因此,使用Wait方法等待异步线程执行完毕。

Task<HttpResponseMessage>; task = sendLogClient.PostAsync();
task.Wait();
HttpResponseMessage response = task.Result;

对于Task<T>类型的任务,使用Result属性获取最终运行结果。

注意:上文讲过,异步方法也可以使用void返回类型,但是这样将无法获得Task,也即无法跟踪任务的运行情况,因此一般不适用void返回类型。
提示:使用var关键字定义变量更为简洁,此处为了清晰期间没有这样做。

4.异步方法的异常处理

在考虑最初一个例子中的代码:

try
{
    HttpResponseMessage response = await sendLogClient.PostAsync("https://api.knightdusk.cn/set_entry.php", content);
    AddLog(response.ToString());
}
catch(TaskCanceledException)//超时重试
{
    Task.Delay(10000).Wait();
    AddEvent(entry).Wait();
}
catch(Exception exp)
{
    AddLog(exp.Message);
}

异步方法可以和普通方法使用类似的try-catch异常处理方法。但注意,c#异步方法超时将抛出TaskCanceledException,而不是我们可能想象的TimeOutException。此处超时重试的方法是递归调用自己,并使用Wait函数等待执行完毕。注意,此处没有设置最大重试次数,是不明智的选择。

Task.Delay(10000)的含义是,异步调用一个需要10000毫秒,即10秒才能完成的方法。立即对其使用了wait方法以实现延时10秒的效果。或者我们可以使用以下代码创建一个至少10秒才能完成的方法:

public void SomeFunction()
{
    task = Task.Delay(10000);
    //此处省略代码XXX行
    Console.WriteLine("我也不知道我为什么要让程序故意变慢");
    task.Wait();
}

总结

最后,我们给异步等待模型的执行方法做一个总结:

public int AFunction()
{
    Task<int> task = AnotherFunction();
    SomeTimeSpendingFunction();//一个耗时的函数
    Task.Wait();
    int result = Task.Result;
}

public async Task<int> AnotherFunction()
{
    //这里有一些语句,省略
    HttpResponseMessage response = await sendLogClient.PostAsync();
    //另外一些语句
    return SomeInt;
}

调用AFunction之后,执行过程如下:

  1. 调用AnotherFunction,执行“这里有一些语句”部分,直到await处。
  2. AnotherFunction返回Task<int>类型
  3. 在两个线程中sendLogClient.PostAsync()和SomeTimeSpendingFunction()同时执行。AnotherFuntion等待PostAsync执行完毕后才继续执行后面的代码。
  4. 假设SomeTimeSpendingFunction先执行完毕,此时主线程等待AnotherFunction继续执行完毕
  5. 主线程使用Result属性获得AnotherFunction的最终结果。

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注