一篇读懂C#异步

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

引言

从.net framework 4.5时代以前,召唤异步多线程的惯用伎俩是使用Thread类。
今天,我们的主角变成了Task。
何时该使用异步io,何时该使用多线程。许多人都没有区分CPU-bound 与 IO-bound,现在。让我们来好好聊聊异步应用吧

简单使用❤

首先,说说我们的需求:在真实开发中,我们通常需要一些延时操作。

例如:在用户点击【发布沸点】30s后,如果有5人点击了 like 此沸点(假设这个论坛实时性高)。那么会立即将该沸点推荐到【热门沸点】(论坛的一个板块)

我们来实现其功能:

  1. 定义一下 ThumbsUp:他的作用是用来统计点赞信息,JsonResult<T>:他的作用是返回处理结果与状态信息
public class JsonResult<T>
{
    public JsonResult(T result)
    {
        Result = result;
        Status = HttpStatusCode.OK;
    }
    public HttpStatusCode Status { get; set; }
    public string ErrMsg { get; set; }
    public T Result { get; set; }
}
public class ThumbsUp
{
    public string UserId { get; set; }
}
复制代码

  1. 发布功能和like功能的实现

image.png

在Publish方法里面我们可以看到:我们通过了IServiceProvider的 GetService 方法获取了ILogger的实例

TIPS: 为何不在构造函数里注入一个ILogger?

实际上,我们使用了 Task.Factory.StartNew 方法打开了一个新线程,而记录器通常可能连接了数据库啊之类的可释放资源,当在新线程里面使用这个记录器时。Controller可能已被释放(这个记录器也会被释放),就会造成访问了已释放对象的异常


  1. 访问一下API看看实际效果:

首先,我们先访问一下url:pins/like 共刷新5次,目的是让Likes 的个数超过5个。然后,再访问 url: pins/publish,模拟发布沸点,查看代码运行截图:

image.png

一点思考💭

刚刚我们已经使用了Task.Factory.StartNew创建了一个线程完成了后续任务。其实上面的方法并不是很推荐,因为它阻塞了一个线程,该线程一直在等待响应完成。而这种操作会导致Asp.net core 线程池的里的可用线程减少。最终导致服务器性能下降

使用异步IO💨

请看下面代码:

[ApiController]
[Route("[controller]/[action]")]
public class PinsController : ControllerBase
{
    /// <summary>
    /// 把他当做从数据库中获取的值
    /// </summary>
    public readonly static List<ThumbsUp> Likes = new List<ThumbsUp>();
    private readonly IServiceProvider _sp;
    public PinsController(IServiceProvider sp)
    {
        _sp = sp;
    }
    [HttpGet]
    public async Task<JsonResult<bool>> Publish()
    {
        //step1: 一系列操作...记录到数据库
        var logger = _sp.GetService<ILogger<PinsController>>();

        //step2: 发布30s后统计其点赞数量,通知用户和推送到热门沸点
        _ = Task.Delay(TimeSpan.FromSeconds(30)).ContinueWith(_ =>
        {
            if (Likes.Count >= 5)
            {
               //step1: 推送至热点....

               //step2: 通知用户
               logger.LogError("现在的时间是:{0},我们假装通知用户...", DateTime.Now.ToString("HH:mm:ss"));
            }
        });
        await Task.CompletedTask;
        return new JsonResult<bool>(true) { ErrMsg = DateTime.Now.ToString("HH:mm:ss") };
    }

    [HttpGet]
    public bool Like()
    {
        Likes.Add(new ThumbsUp { UserId = Guid.NewGuid().ToString() });
        return true;
    }
}
复制代码

我们首先使用了async/await,它帮我们创建了一个异步状态机,然后将Task.Factory.StartNew修改为了 Task.Delay并为它创建了一个延续任务。延续任务里面,做了相应的业务逻辑。并且使用了弃元符号消除了警告。这里与StartNew方法有何区别呢?

StartNew方法就是我们常说的CPU-bound,它首先直接返回了Response,而后简单粗暴的开启了一个线程阻塞来完成业务逻辑。 而 async/await 这种方式(IO-bound)它不产生新线程,只是将代码生成异步状态机。通过异步回调的形式继续处理结果

参考文献😉

参考资料:

写在最后

这篇文章,缺少一些故事感...后期有空完善