« 上一篇下一篇 »

异步多线程的一些知识点

    前言

    本篇按自己的理解,对异步多线程的一些知识点进行记录,顺便聊聊.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方法中才能使用,他们本质上是实现线程之间的调度,当调用线程遇到awaitTask后,会直接返回不继续运行之后的代码(同时阻塞调用线程),等Task运行结束后,由子线程继续运行未完成代码

    (在没有await的情况下,由于Task是非阻塞的,这段代码本来应该由调度线程直接执行),相当于,awaitTask之后的代码变成了Task的回调函数,效果与task.continueWith("后续代码")一致。

    通过await/async这个语法糖,可以用同步编码书写异步过程,提高程序可读性的同时降低了编码难度。

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

    以下是简单的测试代码

    usingSystem;

    usingSystem.Collections.Generic;

    usingSystem.Linq;

    usingSystem.Text;

    usingSystem.Threading;

    usingSystem.Threading.Tasks;

    namespaceConsoleApp1

    {

    classProgram

    {

    staticvoidMain(string[]args)

    {

    Console.WriteLine("当前111Main主线程ID:{0}",Thread.CurrentThread.ManagedThreadId.ToString());

    vart1=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());

    vart=ToDoWithTimeOut();

    Console.WriteLine(t.Result);

    Console.ReadKey();

    }

    privatestaticasyncTask<int>AsyncGetsum()

    {

    Console.WriteLine("准备AsyncGetsum");

    awaitTask.Delay(10000);//遇到await返回main函数,之后的代码变成回调Delay之后再执行相当于回调

    Console.WriteLine("等待了10秒AsyncGetsum开始执行");

    intsum=0;

    for(inti=0;i<=10;i++)

    {

    Console.WriteLine("当前AsyncGetsum线程ID:{0}",Thread.CurrentThread.ManagedThreadId.ToString());

    sum+=i;

    System.Diagnostics.Debug.WriteLine("sum+="+i);

    awaitTask.Delay(50);

    }

    returnsum;

    }

    privatestaticasyncTask<string>ToDoAsync()

    {

    awaitTask.Delay(TimeSpan.FromSeconds(3));

    return"ToDoSuccess!";

    }

    publicstaticasyncTask<string>ToDoWithTimeOut()

    {

    vartoDoTask=ToDoAsync();

    vartimeOutTask=Task.Delay(TimeSpan.FromSeconds(2));

    //varcompletedTask=Task.WhenAny(toDoTask,timeOutTask);

    varcompletedTask=awaitTask.WhenAny(toDoTask,timeOutTask);

    if(completedTask==timeOutTask)

    {

    return"No";

    }

    returnawaittoDoTask;

    }

    }

    }

    线程安全

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

    Lock

    解决线程安全问题,最常见的方法就是加锁

    最标准的写法--->privatestaticreadonlyobjectlick=newobject()

    通过锁定内存的引用地址让对象只会同时被一个线程调用来确保线程安全

    (顺便提一点,锁定内存只在多线程中有用,单线程是随意进入的,所以类似递归函数中出现lock(this)这种写法是不会产生死锁的!)