C# 异步多线程(Task await/async)理解

前言

本篇按自己的理解,对异步多线程的一些知识点进行记录,顺便聊聊.NetFramework中常用类之间的关系。
旨在帮助各位同学理清异步编程的学习路线,并不是个具体的使用教程。

基础知识

线程是归属于操作系统的控制流,并不是由代码生成,代码只负责请求资源,由CPU处理请求在操作系统中获得线程。(这是粗劣的个人理解,但是知道这点就能解释为什么多线程很多反常识的现象)

无序性

多线程相对于单线程,很明显的一个特点就是无序性、不可预测性。

  • 启动无序:
    代码顺序开启多个线程,但是线程启动仍然是无序的。
    原因:CLR顺序向操作系统请求多个线程,这些请求几乎同时发出,CPU随机处理这些请求分配线程,所以哪个线程先开启是无序的。

  • 执行时间不确定
    即使是单线程,执行同一个代码段,时间也是不确定的。
    原因:设计操作系统的调度策略 以及CPU分片。

  • 结束无序

常用类

随着.NetFramework不同版本对于线程的抽象不断演化,类型也逐渐丰富。
大致历史:
Thread-->ThreadPool-->Task/TaskFactory-->Parallel
Thread是初代NetFramework里的对象,拥有最高自由度的线程操作,所以使用不当会造成严重错误(比如可以new一万个线程造成电脑死机)
ThreadPool抽象对于多线程的发展起到了里程碑的作用,后续的模型都基于此发展起来。
Task/TaskFactory是目前最流行的对象,网络上详细的教程很多,大家自行学习即可。
可以参考https://www.cnblogs.com/wyy1234/p/9172467.html
Parallel其实和Task很像,Task不能操作主线程,Parallel在运行时主线程也参与计算。

await/async

专门聊一聊await/async,其实他们和前面几个不是同一层级的,await/async本质只是语法糖,并没有产生新的线程类型对象。
await/async需要与Task一起使用,await只有在async方法中才能使用,他们本质上是实现线程之间的调度,当调用线程遇到await Task后,会直接返回不继续运行之后的代码(同时阻塞调用线程) ,等Task运行结束后,由子线程继续运行未完成代码
(在没有await的情况下,由于Task是非阻塞的,这段代码本来应该由调度线程直接执行),相当于,await Task之后的代码变成了Task的回调函数,效果与task.continueWith("后续代码")一致。
通过await/async这个语法糖,可以用同步编码书写异步过程,提高程序可读性的同时降低了编码难度。

ps:写过js的同学其实会比较好理解,这玩意和promise是一样的玩意,就是语法稍微不同。

以下是简单的测试代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static  void Main(string[] args)
        {
            Console.WriteLine("当前111Main主线程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
            var t1 = AsyncGetsum();
            Console.WriteLine("子线程执行AsyncGetsum 主线程不阻塞 继续执行");
            Console.WriteLine("开始等待t1.Result结果 主线程阻塞");
            Console.WriteLine(t1.Result);  //会阻塞主线程

            Console.WriteLine(" Task.Delay(10000) 开始");
            Task.Delay(10000);//不会阻塞主线程
            Console.WriteLine(" Task.Delay(10000) 结束");


            Console.WriteLine("当前222Main主线程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
            var t = ToDoWithTimeOut();
            Console.WriteLine(t.Result);


            Console.ReadKey();
        }

        private static async Task<int> AsyncGetsum()
        {
            Console.WriteLine("准备 AsyncGetsum");
            await Task.Delay(10000); //遇到await 返回main函数 , 之后的代码变成回调  Delay之后再执行 相当于回调
            Console.WriteLine("等待了10秒 AsyncGetsum开始执行");

            int sum = 0;
            for (int i = 0; i <= 10; i++)
            {
                Console.WriteLine("当前AsyncGetsum线程ID:{0}", Thread.CurrentThread.ManagedThreadId.ToString());
                sum += i;
                System.Diagnostics.Debug.WriteLine("sum += " + i);
                await Task.Delay(50);
            }
            return sum;
        }

        private static async Task<string> ToDoAsync()
        {
            await Task.Delay(TimeSpan.FromSeconds(3));
            return "To Do Success!";
        }

        public static async Task<string> ToDoWithTimeOut()
        {
            var toDoTask = ToDoAsync();
            var timeOutTask = Task.Delay(TimeSpan.FromSeconds(2));

            //var completedTask = Task.WhenAny(toDoTask, timeOutTask);
            var completedTask = await Task.WhenAny(toDoTask, timeOutTask);
            if (completedTask == timeOutTask)
            {
                return "No";
            }
            return await toDoTask;
        }

    }
}

线程安全

多线程中另外一块需要注意的就是线程安全,单线程中正常运行的代码很肯能在多线程中就会出错,特别是在多线程对于同一个对象进行修改的时候。

Lock

解决线程安全问题,最常见的方法就是加锁
最标准的写法 ---> private static readonly object lick = new object()
通过锁定内存的引用地址 让对象只会同时被一个线程调用来确保线程安全
(顺便提一点,锁定内存只在多线程中有用,单线程是随意进入的,所以类似递归函数中出现lock(this)这种写法是不会产生死锁的!)

posted @ 2021-10-21 16:41  LazyTiming  阅读(3057)  评论(0编辑  收藏  举报