C#中的异步陷阱

本文主要介绍异步编程中,常见的异步陷阱:

1、Async没有异步运行

我们来看下面代码,猜测他是如何打印出下面的三个字符串:

/*Gotcha #1: Async does not run asynchronously*/
static void Main(string[] args)
{
    Console.WriteLine("begin" + "" + DateTime.Now.ToString() + "");
    var child = Gotcha1.WorkThenWait();
    Console.WriteLine("started" + "" + DateTime.Now.ToString() + "");
    child.Wait();
    Console.WriteLine("completed" + "" + DateTime.Now.ToString() + "");
    Console.ReadKey();
}
public static async Task WorkThenWait()
{
    Thread.Sleep(5000);
    Console.WriteLine("work" + "" + DateTime.Now.ToString() + "");
    await Task.Delay(10000);
}

看这段代码,如果你猜想,他会按顺序打印出“begin“,”started”,“work”,“completed”,那样的话,你就错了。这段代码会输出“begin“,“work”,“started”,“completed”。

可以看出来,本段代码本来的猜想是,由于WorkThenWait()方法中包含繁重耗时的任务(这里为Thread.Sleep(5000)),所以打算让他异步执行这个方法,但是,可以看出问题出在await关键字用在了该段繁重耗时任务之后,所以后面的child.Wait()方法只是等待了Task.Delay(10000)的执行。

执行结果如下:

 2、忽略结果

我们来看下面代码,猜测他是如何执行的,是否会等待:

/*Gotcha #2: Ignoring results*/
static void Main(string[] args)
{
    Gotcha2.Handler();
    Console.ReadKey();
}
public static async Task Handler()
{
    Console.WriteLine("Before");
    Task.Delay(5000);
    Console.WriteLine("After");
}

看这段代码,你是否期待它会打印“Before”,等待5秒之后,再打印“After”?当然,又错了。他会立即打印两条字符串,直接没有任何等待。问题出在,虽然Task.Delay返回了Task返回值,但是我们忘记了使用await关键字去等待直到他完成。

执行结果如下:

 3、Async void方法

我们来看下面代码,判断下是否能成功打印出异常信息:

/*Gotcha #3: Async void methods*/
static void Main(string[] args)
{
    Gotcha3.CallThrowExceptionAsync();
    Console.ReadKey();
}

private static async void ThrowExceptionAsync()
{
    throw new InvalidOperationException();
}

public static void CallThrowExceptionAsync()
{
    try
    {
        ThrowExceptionAsync();
    }
    catch (Exception)
    {
        Console.WriteLine("Failed");
    }
}

你是否觉得这段代码会打印“Failed”?当然,这个异常没有被捕获到,因为ThrowExceptionAsync方法开始执行并立即返回(这个异常发生在background thread内部)。问题根源在于:对于例如 async void Foo(){...},这种方法,C#编译器将生成一个返回void的方法,然后它会在后台创建一个task并执行。这意味着你无法知道这项工作实际何时发生。

 4、Async void lambda函数

如果你将异步lambda函数作为委托传递给某个方法时。在这种情况下,C#编译器将从委托类型推断方法的类型。如果使用Action delegate,则编译器生成async void function(这种后台启动线程工作并返回void)。如果使用Func<Task> delegate,编译器讲生成返回Task的function。

我们来看下面代码,判断这段代码是在5秒之后执行完(等待所有的Task完成sleeping),或者它立即完成了?

/*Gotcha #4: Async void lambda functions*/
static void Main(string[] args)
{
    Gotcha4.Test();
    Console.WriteLine("ok");
}

public  static void Test()
{
    Parallel.For(0, 10, async i => {
        await Task.Delay(5000);
    });
}

当然,直接看For无法判断出他是否等待,我们借助小工具看下如下代码的源码:

public void TestMethod()
{
    Parallel.For(0, 10, async i => {
        await Task.Delay(5000);
    });
}
// AsyncGotchasLib.Class1
public void TestMethod()
{
    int arg_23_0 = 0;
    int arg_23_1 = 10;
    Action<int> arg_23_2;
    if ((arg_23_2 = Class1.<>c.<>9__0_0) == null)
    {
        arg_23_2 = (Class1.<>c.<>9__0_0 = new Action<int>(Class1.<>c.<>9.<TestMethod>b__0_0));
    }
    Parallel.For(arg_23_0, arg_23_1, arg_23_2);
}

可以看到For中,lambda函数最终编译为了Action Delegate,因此这段代码不会等待5秒,会立即执行完成,它的等待会在后台线程执行。

 5、嵌套任务

看下面代码,问题是,下面两个print直接会不会等待5秒:

/*Gotcha #5: Nesting of tasks*/
static void Main(string[] args)
{
    Gotcha5.Test();
    Console.ReadKey();
}

public async static void Test()
{
    Console.WriteLine("Before");
    await Task.Factory.StartNew(
      async () => { await Task.Delay(5000); Console.WriteLine("后台线程等待5秒后"); });
    Console.WriteLine("After");
}

同样,相当意外,并没有在两个打印(“Before”,“After”)之间等待。为什么?StratNew 方法接受一个委托,并返回一个Task<T>,其中T表示又delegate返回的类型。在这个方法中,这个delegate返回Task,因此,我们获得的结果为Task<Task>。使用await关键字仅仅等待外部Task的完成(它将立即返回内部task),这代表内部task将被忽略,内部task将在后台线程执行。

原文地址:https://www.cnblogs.com/SimplePerson/p/7395513.html