08-01 多线程之异步

一、进程与线程

1、定义

  • 进程:是系统进行资源分配和调度的一个独立单位,记录当前程序在运行的时候对各种资源的消耗,是一个虚拟概念
  • 线程:线程是进程的实例,计算机在执行某个动作,一个最小的执行流,也是虚拟概念
  • 句柄:句柄其实就是一个数字,对应计算机程序中的最小单位,如:当前程序运行Id。

2、关系

一个进程包括多个线程

二、C# 中的多线程

Thread是对计算机资源操作的一种封装类

1、同步方法

本质
 发起调用后,代码一行一行按照顺序执行,符合正常的开发思维,而且线程id是唯一不变的,说明所有的操作都是在同一个线程中执行的
特点
1. 卡顿界面:UI线程(主线程)处理所有的动作和计算,在执行过程中主线程一直被占用,不能再进行其他动作,直到主线程闲置,非常影响用户体验;
2. 运行速度慢,消耗资源少;
3. 同步方法是有序执行的。

(1)创建一个windows窗体应用,新建一个按钮,按钮事件执行以下代码

/// <summary>
/// 同步方法
///     特点:发起调用,代码一行一行按照顺序执行,符合正常的开发思维,而且线程id是唯一不变的
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void SyncBtn_Click(object sender, EventArgs e)
{
    //Thread.CurrentThread.ManagedThreadId 当前线程id
    Console.WriteLine($"==================SyncBtn_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    for (int i = 0; i < 5; i++)
    {
        string name = $"SyncBtn_Click_{i}";
        this.DoSomething(name);
    }
    Console.WriteLine($"==================SyncBtn_Click end   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}

(2)再添加一个模拟资源消耗的方法

#region private methods
/// <summary>
/// 模拟比较消耗资源的方法
/// </summary>
/// <param name="name"></param>
private void DoSomething(string name)
{
    Console.WriteLine($"==================DoSomething start {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    long result = 0;
    //循环一百万次
    for (int i = 0; i < 1000_000; i++)
    {
        result += i;
    }
    //Thread.Sleep(2000);
    Console.WriteLine($"==================DoSomething end   {name} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")} {result}========================");
}
#endregion

(3)运行
在这里插入图片描述

2、异步方法

本质
发起调用之后,主线程不等待完成,直接进进行下一步操作,发起新的线程来执行指定动作(每个线程的id都不同,与主线程id也不同)
特点
1. 不卡顿界面:UI线程(主线程)闲置,计算任务交给子线程执行,改善了用户体验,应用场景如发送短信邮件等;
2. 运行速度高:实现多线程并发,以资源换性能;
3. 异步方法无序执行:启动无顺序,线程资源是向操作系统申请的,由操作系统的调度策略决定,所以线程启动顺序是随机的,导致执行也是无序的,因此CPU分片执行结束也是无序的。

注:
CPU分片:假设CPU 1秒内能进行10亿次计算,把1秒内的计算能力再次切分,由操作系统去调度执行不同的计算
宏观上:相当于多个任务并发执行
微观上:一个物理CPU同一时刻,只能完成一个任务

(1)新建一个按钮,按钮事件执行以下代码

/// <summary>
/// 异步方法
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AsyncBtn_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================AsyncBtn_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    Action<string> action = this.DoSomething;
    action.Invoke("AsyncBtn_Click");
    for (int i = 0; i < 5; i++)
    {
        string name = $"AsyncBtn_Click{i}";
        //分配一个新的线程去执行委托(目前dotnet core中不支持该方法)
        action.BeginInvoke(name, null, null);
    }
    Console.WriteLine($"==================AsyncBtn_Click end   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}

但是这里有一个问题,就是截至2020年
9月14日dotnet core 还不支持BeginInvoke方法,所以需要创建一个.net framework版本的窗体应用来模拟,代码是一样的。
在这里插入图片描述
(2)运行
在这里插入图片描述
(3)分析
在这里插入图片描述
之所以会出现DoSomething 方法中的start打印完成后不是接着打印end,是因为,在这5个线程中调用DoSomething都是同时进行的,任何一个线程都不会等待另外一个线程执行。
在这里插入图片描述
(4)运行速度对比
同步方法耗时28毫秒,异步方法耗时3毫秒,性能大约是同步方法的9倍,但是实际不能达到9倍,因为线程调度也要消耗资源(但是Thread.Sleep只是等待,不消耗资源)
在这里插入图片描述

三、多线程进阶

1、控制线程执行顺序

通过异步回调委托,就实现了控制线程的顺序

思考:如果想多线程调用DoSomething完成之后,打印一句话(执行某个动作),该怎么做?
方式一:可以实现吗?

/// <summary>
/// 异步方法进阶
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void AdvancedAsync_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================AdvancedAsync_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    Action<string> action = this.DoSomething;

    //AsyncCallback callback = ar =>
    //{
    //    Console.WriteLine("AdvancedAsync_Click执行成功!");
    //};

    action.BeginInvoke("AdvancedAsync_Click", null, null);

    Console.WriteLine("AdvancedAsync_Click执行成功!");

    Console.WriteLine($"==================AdvancedAsync_Click end   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}

结果:很明显不符合需求
在这里插入图片描述
方式二:利用异步回调委托AsyncCallback,如果给异步回调委托中传递了参数,可以通过回调委托对象IAsyncResultAsyncState属性获得

 /// <summary>
 /// 异步方法进阶
 /// </summary>
 /// <param name="sender"></param>
 /// <param name="e"></param>
 private void AdvancedAsync_Click(object sender, EventArgs e)
 {
     Console.WriteLine($"==================AdvancedAsync_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
     Action<string> action = this.DoSomething;

     //异步回调
     AsyncCallback callback = ar =>
     {
         Console.WriteLine(ar.AsyncState);
         Console.WriteLine("AdvancedAsync_Click执行成功!");
     };
	//第二个参数是异步回调委托,第三个参数是给异步回调委托中传递参数
     action.BeginInvoke("AdvancedAsync_Click", callback, "异步回调参数,可以是任意类型,通过AsyncState属性获得");

     //Console.WriteLine("AdvancedAsync_Click执行成功!");

     Console.WriteLine($"==================AdvancedAsync_Click end   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
 }

结果:符合需求
在这里插入图片描述

2、获取线程返回值

(1)获取返回状态
会阻塞主线程,这里有一个小坑,如果子线程执行完成,就不会再执行while条件判断,也就是说,在子线程执行完成前可以获得返回状态,但是执行完成那一刻是获取不到的,除非再次点击按钮获取状态

/// <summary>
/// 获取异步执行状态
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Process_Click(object sender, EventArgs e)
{
    Console.WriteLine($"==================Process_Click start {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
    Func<int> func = () =>
    {
        return DateTime.Now.Year;
    };
    //异步回调
    AsyncCallback callback = ar =>
    {
        Console.WriteLine(ar.AsyncState);
    };
    //获取异步执行状态(对前面委托的动作执行后的描述)
    IAsyncResult asyncResult = func.BeginInvoke(callback, "异步回调参数");
    int i = 1;
    while (!asyncResult.IsCompleted)
    {
        if (i <= 9)
        {
            Console.WriteLine($"已完成{i * 10}%。。。。");
        }
        else
        {
            Console.WriteLine($"已完成100%。。。。");
        }
        i++;
    }
    Console.WriteLine($"==================Process_Click end   {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss.fff")}========================");
}

执行结果
在这里插入图片描述
(2)获取返回值

  • 方式一:在回调函数中获取
//异步回调
 AsyncCallback callback = ar =>
{
  int result = func.EndInvoke(ar);
  Console.WriteLine(result);
};
  • 方式二
//获取返回结果,会阻塞主线程
int result = func.EndInvoke(asyncResult);
Console.WriteLine(result);

在这里插入图片描述上面两种方式可以达到相同的效果,因为异步回调函数是在异步执行完成后执行,且EndInvoke只能被调用一次,所以上述两种方式不能同时出现
在这里插入图片描述
(3)其他

//等待异步委托执行完毕,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne();
//一直等待任务完成,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne(-1);
//限时等待;这里例子最多等待2s,会阻塞主线程
asyncResult.AsyncWaitHandle.WaitOne(2000);

四、本章代码

dotnet core版本窗体
dotnet Framework版本窗体

注:
运行窗体怎么将结果输出到命令行窗口呢?如下将项目输出类型设置为控制台应用程序即可
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值