C# 初见异步方法 满满的全是坑

初见

虽然写了不少的C#代码,但却还么有使用过带有async关键字的异步方法。平常遇到的需求,使用普通的多线程都可以完美解决,感觉异步方法只是微软想当然开发出来的,虽然华丽但是毫无用处的垃圾功能。直到有一天,我要修改一个几乎全是异步方法的代码。

这里介绍一下本次修改遇到的全部的坑。可以通过这些坑纠正我们的认知偏差。

坑1 何时异步执行

请查看为何下面两个代码输出内容不同:

internal class Program
{
    public static void Main()
    {
        Program p = new Program();
        _ = p.AsyncTask();
        Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);
    }

    private async Task AsyncTask()
    {
        Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);
        await Task.Delay(1);
    }
}

输出内容:

执行异步方法 id:1
主线程退出 id:1

第二段代码:

internal class Program
{
    public static void Main()
    {
        Program p = new Program();
        _ = p.AsyncTask();
        Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);
    }

    private async Task AsyncTask()
    {
        await Task.Delay(1);
        Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);
    }
}

输出内容:

主线程退出 id:1
执行异步方法 id:6

接下来将上面第二段代码转换一下,就能看出来为什么了。
第二段代码等价于:

internal class Program
{
    public static void Main()
    {
        Program p = new Program();
        Task task = p.AsyncTask();
        task.ContinueWith(t => 
        {
            Console.WriteLine("执行异步方法 id:" + Environment.CurrentManagedThreadId);
        });
        Console.WriteLine("主线程退出 id:" + Environment.CurrentManagedThreadId);
    }

    private Task AsyncTask()
    {
        return Task.Delay(1);
    }
}

可以看出,C#的关键字async与await太误导人了。
同时也可以看出异步方法的本质:
在遇到需要等待执行的Task任务,让线程直接执行异步方法的后续代码,同时从线程池中取出新线程执行任务。

坑2 如何在非异步方法中调用异步方法

internal class Program
{
    public static void Main()
    {
        Program p = new Program();
        _thread = new Thread(p.CheckThreadId);
        _thread.Start();
        // 方法结束 交还ui线程
    }

    private static Thread? _thread;

    private async Task CheckThreadId()
    {
        for (int i = 0; i < 10; i++)
        {
            // 执行任务
            await Task.Delay(1000);
        }
    }
    }
}

C#设计异步方法的主要目标,就是使用Task自己的线程池,免去建立新线程的开销,以及自己搭建线程池的麻烦。可是结合上面的代码看着就很奇怪,既然使用了异步方法,为何还要建立新的线程?最重要的是,无法通过判断_thread的运行状态来判断任务是否还在执行。

于是,我将线程改为Task对象,这样就可以根据Task对象的信息,判断任务是否执行完成。不过之后就出问题了,主线程,也就是winform的ui线程被占用,窗体卡死。
为何会卡死主线程,会在坑三中介绍。

internal class Program
{
    public static void Main() // 按键点击后执行的方法
    {
        Program p = new Program();
        _task = p.CheckThreadId();
        // 方法结束 交还ui线程
    }

    private static Task? _task;

    private async Task CheckThreadId()
    {
        for (int i = 0; i < 10; i++)
        {
            // 执行任务
            await Task.Delay(1000);
        }
    }
}

正确的方式应该是这样的,交给Task来执行异步方法,而不要直接调用异步方法。

internal class Program
{
    public static void Main() // 按键点击后执行的方法
    {
        Program p = new Program();
        _task = Task.Run(p.CheckThreadId);
        // 方法结束 交还ui线程
    }

    private static Task? _task;

    private async Task CheckThreadId()
    {
        for (int i = 0; i < 10; i++)
        {
            // 执行任务
            await Task.Delay(1000);
        }
    }
}

坑3 被卡死的ui线程

从该代码中,可以看出循环线程有问题。虽然每次循环都有await,但是任务只能被执行一次。不过由于未知的原因,在第一次await等待结束后,继续接受执行的百分百为ui线程,可能是因为该线程在闲置状态吧。之后的循环,因为await不再生效,所以ui线程就被卡死在其中了,winfrom窗口也被卡死。

internal class Program
{
    public static void Main() // 按键点击后执行的方法
    {
        Program p = new Program();
        _task = p.Work();
        Console.WriteLine("主线程结束");
        // 方法结束 交还ui线程
    }

    private static Task? _task;

    private async Task Work()
    {
        Task delayTask = Task.Delay(1000);
        for (int i = 0; i < 10; i++)
        {
            // 执行任务
            Console.WriteLine("执行任务" + DateTime.Now);
            await delayTask;
        }
    }
}

坑4 异步方法的异常处理

在普通的异步操作中,新建立线程中的异常不会被抛到其他线程中。若在新线程中没有catch异常,则异常信息可能会丢失。

异步方法中的异常不会锁定在一个线程中,它会通过等待操作互相转移。通过下面的测试代码可以看出,执行异步方法的主线程不受异常影响,而执行等待的两个子线程同时catch到了异步方法的异常。

public static void Main()
{
    _testTask = Task.Run(Test);
    new Thread(AwaitF1).Start();
    AwaitF2();
    Thread.Sleep(10 * 1000);
}

private static Task _testTask = null!;

private static void AwaitF1()
{
    Thread.Sleep(1000);
    try
    {
        Console.WriteLine("等待方法1");
        _testTask.Wait();
    }
    catch(Exception e)
    {
        Console.WriteLine("等待方法1 " + e.Message);
    }
}

private static async void AwaitF2()
{
    try
    {
        Console.WriteLine("等待方法2");
        await _testTask;
    }
    catch(Exception e)
    {
        Console.WriteLine("等待方法2 " + e.Message);
    }
}


private static async Task Test()
{
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(i);
        await Task.Delay(1000);
        if (i == 8)
            throw new ApplicationException("异步方法异常");
    }
}

输出信息

等待方法2
0
等待方法1
1
2
3
4
5
6
7
8
等待方法2 异步方法异常
等待方法1 One or more errors occurred. (异步方法异常)
  • 7
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值