Document

.NET异步和多线程系列(一)

本系列将和大家分享.Net中的异步多线程,本文是该系列的开篇。首先来看些概念:

进程:计算机概念,虚拟的概念,程序在服务器运行时占据全部计算资源的总和,我们给它起个名字叫进程。

线程:计算机概念,虚拟的概念,进程在响应操作时最小单位,也包含CPU 内存  网络  硬盘IO。

多线程:计算机概念,一个进程有多个线程同时运行。

进程与线程的区别:

  1、线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;

  2、一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线。

  3、进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;

  4、调度和切换:线程上下文切换比进程上下文切换要快得多。

  5、包含关系:没有线程的进程可以看作是单线程的。如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。

线程和进程关系示意图:

总之,线程和进程都是一种抽象的概念,线程是一种比进程还小的抽象,线程和进程都可用于实现并发。在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位,它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程。

后来,随着计算机的发展,对多个任务之间上下文切换的效率要求越来越高,就抽象出一个更小的概念——线程,一般一个进程会有多个(也可以是一个)线程。

任务调度:大部分操作系统的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态,等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。

为何不使用多进程而是使用多线程?

线程廉价,线程启动比较快,退出比较快,对系统资源的冲击也比较小。而且线程彼此分享了大部分核心对象(File Handle)的拥有权。如果使用多重进程,它是不可预期的,且测试困难。

为什么可以多线程呢?

  1、多个CPU的核可以并行工作,4核8线程,这里的线程指的是模拟核。

  2、CPU分片,1s的处理能力分成1000份,操作系统调度着去响应不同的任务。从宏观角度来说,感觉就是多个任务在并发执行,从微观角度来说,一个物理cpu同一时刻只能为一个任务服务。

并行和并发:多核之间的叫并行,CPU分片的叫并发。

同步和异步:

  同步方法:发起调用,完成后才继续下一行;非常符合开发思维,有序执行。

  异步方法:发起调用,不等待完成,直接进入下一行,启动一个新线程来完成方法的计算。

 

至此相关概念介绍完了,下面我们正式进入本章主题:

C#里面的多线程:Thread类是C#语言对线程对象的一个封装。

为了演示此处我们使用VS2017建个Windows窗体应用程序MyAsyncThread,目标框架为:.NET Framework 4.6.1,如下所示:

此外,如果大家有使用过异步多线程的都知道,我们的异步多线程是不好调试的,一般我们会采用输出一些信息等方法来辅助我们调试和理解。对此我们有个小技巧,就是将应用程序的输出类型改为控制台应用程序,这样我们在启动程序的时候就会有2个窗口,一个是应用程序窗口,另外一个是控制台窗口。如下所示:

设置完,启动后就有2个窗口了,如下所示:

首先我们来看个很简单的同步方法:

复制代码
/// <summary>
/// 一个比较耗时耗资源的私有方法
/// </summary>
private void DoSomethingLong(string name)
{
    Console.WriteLine($"****************DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    long lResult = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        lResult += i;
    }
    //Thread.Sleep(2000);
    Console.WriteLine($"****************DoSomethingLong   End  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
}
复制代码
复制代码
/// <summary>
/// 同步方法
/// </summary>
private void btnSync_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnSync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    int l = 3;
    int m = 4;
    int n = l + m;
    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"btnSync_Click_{i}");
        this.DoSomethingLong(name);
    }
    Console.WriteLine($"****************btnSync_Click   End {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}
复制代码

运行起来,点击同步方法后,会发现如果控制台还未输出完,此时WinForm窗口会卡在那里,无法拖动:

如果我们使用委托的BeginInvoke异步方法去执行会发现WinForm界面不会卡主:

复制代码
/// <summary>
/// 异步方法
/// </summary>
private void btnAsync_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsync_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    Action<string> action = this.DoSomethingLong;

    //action.Invoke("btnAsync_Click_1");
    //action("btnAsync_Click_1");

    //委托自身需要的参数+2个异步参数
    //action.BeginInvoke("btnAsync_Click_1", null, null);

    for (int i = 0; i < 5; i++)
    {
        string name = string.Format($"btnAsync_Click_{i}");
        action.BeginInvoke(name, null, null); //异步(开启一个子线程去完成)
    }

    Console.WriteLine($"****************btnAsync_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}
复制代码

此时控制台输出结果如下:

点击异步方法去执行,会发现马上可以拖动WinForm窗口,并不会像同步方法那样卡界面。那这是为什么呢?

通过对比同步方法和异步方法的执行结果会发现:

复制代码
/// <summary>
/// 异步方法
/// 1 同步方法卡界面:主线程(UI线程)忙于计算,无暇他顾。
///   异步多线程方法不卡界面:主线程闲置,计算任务交给子线程完成。
///   改善用户体验,WinForm点击个按钮不至于卡死;
///   Web应用发个短信通知,异步多线程去发短信;
/// 
/// 2 同步方法慢,只有一个线程计算
///   异步多线程方法快,因为5个线程并发计算
///   12658ms   3636ms  不到4倍   CPU密集型计算(资源受限)
///   10126ms   2075ms  差不多5倍,也不到5倍,Sleep(资源够用)
///   多线程其实是资源换性能,1 资源不是无限的  2 资源调度损耗
///   
///   一个订单表统计很耗时间,能不能多线程优化下性能?  不能!这就是一个操作,没法并行
///   需要查询数据库/调用接口/读硬盘文件/做数据计算,能不能多线程优化下性能? 可以,多个任务可以并行
///   线程不是越多越好,因为资源有限,而且调用有损耗
///   
/// 3 同步方法有序进行,异步多线程无序
///   启动无序:线程资源是向操作系统申请的,由操作系统的调度策略决定,所以启动顺序随机
///   同一个任务同一个线程,执行时间也不确定,CPU分片
///   以上相加,结束也无序
///   使用多线程请一定小心,很多事儿不是相当然的,尤其是多线程操作间有顺序要求的时候,
///   通过延迟一点启动来控制顺序?或者预计下结束顺序?  这些都不靠谱!
///   
///   需要控制顺序,晚点分解!
/// </summary>
复制代码

如果我们想在异步方法调用完之后再执行某些业务逻辑的话要怎么实现呢?(控制顺序)

方法1:调用BeginInvoke的时候传入回调函数。

复制代码
/// <summary>
/// 异步进阶
/// </summary>
private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    Action<string> action = this.DoSomethingLong;

    //1 回调:将后续动作通过回调参数传递进去,子线程完成计算后,去调用这个回调委托
    IAsyncResult asyncResult = null; //IAsyncResult是对异步调用操作的描述
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"{object.ReferenceEquals(ar, asyncResult)}");
        Console.WriteLine($"btnAsyncAdvanced_Click计算成功了。{ar.AsyncState}。{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    };
    asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "浪子天涯"); //第三个参数“浪子天涯”为回调传参

    Console.WriteLine($"****************btnAsyncAdvanced_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}
复制代码

执行结果如下:

方法2:通过asyncResult.IsCompleted属性

为了演示此处将DoSomethingLong方法的执行时间调长一点

复制代码
/// <summary>
/// 一个比较耗时耗资源的私有方法
/// </summary>
private void DoSomethingLong(string name)
{
    Console.WriteLine($"****************DoSomethingLong Start  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
    long lResult = 0;
    for (int i = 0; i < 1_000_000_000; i++)
    {
        lResult += i;
    }
    Thread.Sleep(2000);
    Console.WriteLine($"****************DoSomethingLong   End  {name}  {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}***************");
}
复制代码
复制代码
/// <summary>
/// 异步进阶
/// </summary>
private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    Action<string> action = this.DoSomethingLong;

    //1 回调:将后续动作通过回调参数传递进去,子线程完成计算后,去调用这个回调委托
    IAsyncResult asyncResult = null; //IAsyncResult是对异步调用操作的描述
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"{object.ReferenceEquals(ar, asyncResult)}");
        Console.WriteLine($"btnAsyncAdvanced_Click计算成功了。{ar.AsyncState}。{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    };
    asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "浪子天涯"); //第三个参数“浪子天涯”为回调传参

    //2 通过IsComplate等待,卡界面--主线程在等待,边等待边提示
    //( Thread.Sleep(200);位置变了,少了一句99.9999)
    int i = 0;
    while (!asyncResult.IsCompleted)
    {
        Thread.Sleep(200); //放在这里不合适,因为有延迟,主线程在等待过程中的子线程可能已经完成了
        if (i < 9)
        {
            Console.WriteLine($"中华民族复兴完成{++i * 10}%....");
        }
        else
        {
            Console.WriteLine($"中华民族复兴完成99.999999%....");
        }
        //Thread.Sleep(200); //放在这里比较合适
    }
    Console.WriteLine("中华民族复兴已完成,沉睡的东方雄狮已觉醒!");

    Console.WriteLine($"****************btnAsyncAdvanced_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}
复制代码

看下执行结果:

此处比我们预想的多了一个“...99.999999%”,那是因为有延迟,主线程在等待过程中的子线程可能已经完成了。故需要对Thread.Sleep(200)的位置做一个调整。

复制代码
/// <summary>
/// 异步进阶
/// </summary>
private void btnAsyncAdvanced_Click(object sender, EventArgs e)
{
    Console.WriteLine($"****************btnAsyncAdvanced_Click Start {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");

    Action<string> action = this.DoSomethingLong;

    //1 回调:将后续动作通过回调参数传递进去,子线程完成计算后,去调用这个回调委托
    IAsyncResult asyncResult = null; //IAsyncResult是对异步调用操作的描述
    AsyncCallback callback = ar =>
    {
        Console.WriteLine($"{object.ReferenceEquals(ar, asyncResult)}");
        Console.WriteLine($"btnAsyncAdvanced_Click计算成功了。{ar.AsyncState}。{Thread.CurrentThread.ManagedThreadId.ToString("00")}");
    };
    asyncResult = action.BeginInvoke("btnAsyncAdvanced_Click", callback, "浪子天涯"); //第三个参数“浪子天涯”为回调传参

    //2 通过IsComplate等待,卡界面--主线程在等待,边等待边提示
    //( Thread.Sleep(200);位置变了,少了一句99.9999)
    int i = 0;
    while (!asyncResult.IsCompleted)
    {
        //Thread.Sleep(200); //放在这里不合适,因为有延迟,主线程在等待过程中的子线程可能已经完成了
        if (i < 9)
        {
            Console.WriteLine($"中华民族复兴完成{++i * 10}%....");
        }
        else
        {
            Console.WriteLine($"中华民族复兴完成99.999999%....");
        }
        Thread.Sleep(200); //放在这里比较合适
    }
    Console.WriteLine("中华民族复兴已完成,沉睡的东方雄狮已觉醒!");

    Console.WriteLine($"****************btnAsyncAdvanced_Click End   {Thread.CurrentThread.ManagedThreadId.ToString("00")} " +
        $"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}***************");
}
复制代码

此时的执行结果就是对的了:

方法3:WaitOne等待

{
    //3 WaitOne等待,即时等待 限时等待
    asyncResult.AsyncWaitHandle.WaitOne();//直接等待任务完成
    asyncResult.AsyncWaitHandle.WaitOne(-1);//一直等待任务完成
    asyncResult.AsyncWaitHandle.WaitOne(1000);//最多等待1000ms,超时就不等了
}

方法4:EndInvoke即时等待

{
    //4 EndInvoke  即时等待,而且可以获取委托的返回值 一个异步操作只能End一次
    action.EndInvoke(asyncResult); //等待某次异步调用操作结束
}
复制代码
{
    Func<int> func = () =>
    {
        Thread.Sleep(2000);
        return DateTime.Now.Hour;
    };
    int iResult = func.Invoke();
    IAsyncResult asyncResult = func.BeginInvoke(ar =>
    {
        //int iEndResultIn = func.EndInvoke(ar); //在这里执行也是可以的,但是一个异步操作只能EndInvoke一次
    }, null);
    int iEndResult = func.EndInvoke(asyncResult); //可以获取委托的返回值

    Console.WriteLine($"iResult:{iResult}--iEndResult:{iEndResult}");
}
复制代码

 

Demo源码:

链接:https://pan.baidu.com/s/1qCXi1HsgBv_uFROIMfFYsw 
提取码:1nb4
posted @ 2021-05-21 10:11  从未被超越  阅读(336)  评论(0编辑  收藏  举报