【.net6第四章】.NetCore多线程


提示:以下是本篇文章正文内容,下面案例可供参考

一、进程、线程、句柄,同步、异步

进程:就是计算机概念,一种虚拟的记录,描述一个应用在计算机上运行的时候,所消耗的各种资源你的集合。例如:CPU,磁盘,网络资源。
线程:某一个程序进程中所执行的具体的某一个动作的最小执行流。
句柄:句柄是对系统资源的抽象引用,程序员通常不需要知道具体的内存地址或资源的物理布局,只需通过句柄来操作这些资源。
同步:主要涉及多个进程、线程或操作的协调,以确保它们以一种有序和一致的方式运行。
异步(Asynchronous):编程是一种编程范式,它允许某些任务在后台运行,而不会阻塞(即停止执行)主程序流程。在异步操作中,程序可以在等待异步任务完成的同时继续执行其他任务,这对于提高应用程序的响应性和性能尤其重要

二、Task

Task和prallel的所操作的线程都是来自线程池。

1.线程开启

开启线程方式:

Task task = new Task(new Action(() =>
{
      Console.WriteLine("Hello World!");
}));
task.Start(); //开启线程

Task.Run(() =>
{
      Console.WriteLine("Hello World!");
});

Task.Run<int>(() =>
{
    return DateTime.Now.Year;
});

TaskFactory factory = Task.Factory;
factory.StartNew(() =>
{
        Console.WriteLine("Hello World!");
});

TaskFactory factory = new TaskFactory();
Task<int> tTask = factory.StartNew<int>(() => DateTime.Now.Year);

父子级线程:
一个Task内部,开启了几个线程以后;Task内部的线程可以理解为子线程;
父线程在等待的时候,子线程没有线程等待;子线程可能内容还没有执行万一,父线程就已经结束了;

Task task = new Task(() =>
{
    Debug.WriteLine($"task开始了: {Thread.CurrentThread.ManagedThreadId.ToString("00")} ");
    Task task1 = new Task(() =>
    {
        Debug.WriteLine($"task1: {Thread.CurrentThread.ManagedThreadId.ToString("00")} ");
        Thread.Sleep(1000);
        Debug.WriteLine("我是task1线程");
    });

    Task task2 = new Task(() =>
    {
        Debug.WriteLine($"task2: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
        Thread.Sleep(1000);
        Debug.WriteLine("我是task2线程");
    });
    task1.Start();
    task2.Start();
    Debug.WriteLine($"task: {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
});
task.Start();//线程启动了
task.Wait();   //单个线程的等待;等待task 内部的内容执行完毕,这里会卡顿界面;是主线程等待;


//TaskCreationOptions.PreferFairness 相对来说比较公平执行的:如果是先申请的线程,就会优先执行;
 Task task1= new Task(() =>
{

}, TaskCreationOptions.PreferFairness);

Task task2 = new Task(() =>
{

}, TaskCreationOptions.PreferFairness);

在这里插入图片描述

2.线程等待

Task task = Task.Run(() =>
{
    Debug.WriteLine("Hello World");
});
task.Wait(); //等待这个线程执行完毕

Thread.Sleep(100); //主线程执行到这里会卡顿等待,100ms后继续执行

Task.Delay(300); //不卡顿界面延迟300ms继续执行逻辑

//不卡顿界面:言辞3000ms以后,去执行一段业务逻辑:执行的动作就是ContinueWith内部的委托;
//ContinueWith内部的执行有可能是一个全新的线程去执行,也有可能是主线程去执行; 
Task.Delay(3000).ContinueWith(t =>
{
    Debug.WriteLine($"Delay || 线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")}");
    stopwatch.Stop();
    Debug.WriteLine($"Task.Delay(3000):{stopwatch.ElapsedMilliseconds}");
});

3.线程等待的多种方式

场景模拟:现在需要多人同时开发一个项目,那么我们就可以开启多线程去完成。

List<Task> taskList = new List<Task>();
TaskFactory factory = new TaskFactory();

taskList.Add(factory.StartNew(() =>Coding("A","负责权限的数据库设计")));
taskList.Add(factory.StartNew(() => Coding("B","负责WeChat接口对接")));
taskList.Add(factory.StartNew(() => Coding("C","负责脚手架框架搭建")));
taskList.Add(factory.StartNew(() => Coding("D","负责ABPVNetxt框架搭建")));
taskList.Add(factory.StartNew(() => Coding("E","负责编写Webapi")));

正常情况下我们我们不需要去考虑完成的前后顺序,那么我要在上面任何以为的工作完成之后,F来搭建环境要如何处理呢?

//方案一:
//Task.WaitAny:等待几个线程中的某一个线程执行结束,主线程会等待,会卡顿界面,一直到某一个线程执行结束后,才往后执行。体验不好 !!!
Task.WaitAny(taskList.ToArray());
Debug.WriteLine($"F准备环境");

//方案二:
//相当于是一个回调;主线程执行的时候,不会阻塞UI界面;当taskList集合中的某一个线程执行结束语了;就触发后面委托中的动作;
factory.ContinueWhenAny(taskList.ToArray(), ts =>
{
    Debug.WriteLine($"F准备环境");
});

如果所有人开发完毕后,要正式上线要如何处理呢?

//方案一:
//主线程等待,阻塞UI界面,其中某一个线程执行结束,然后继续往后执行;
Task.WaitAny(taskList.ToArray());
Task.WaitAll(taskList.ToArray());
Debug.WriteLine($"正式上线!!!");

//方案二:
factory.ContinueWhenAll(taskList.ToArray(), ts =>
{
    Debug.WriteLine($"正式上线!!!");
});

演变=》如果要知道那个最先开发完成,我们也可以通过状态来判断。

List<Task> taskList = new List<Task>();
TaskFactory factory = new TaskFactory();

taskList.Add(factory.StartNew(obj => Coding("A","负责权限的数据库设计"),"A"));
taskList.Add(factory.StartNew(obj => Coding("B","负责WeChat接口对接"), "B"));
taskList.Add(factory.StartNew(obj => Coding("C","负责脚手架框架搭建"), "C"));
taskList.Add(factory.StartNew(obj => Coding("D","负责ABPVNetxt框架搭建"), "D"));
taskList.Add(factory.StartNew(obj => Coding("E","负责编写Webapi"), "E"));

factory.ContinueWhenAny(taskList.ToArray(), ts =>
{
    Debug.WriteLine($"{ts.AsyncState}开发完成,,F准备环境");
});

factory.ContinueWhenAll(taskList.ToArray(), ts =>
{
    Debug.WriteLine($"正式上线");
});

Console.Read();

 void Coding(string name, string projectName)
{
    Debug.WriteLine($"Coding Start  || {name} {projectName}  {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(3000);
    Debug.WriteLine($"Coding   End ||  {name} {projectName} {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} {lResult}");
}

运行结果:
在这里插入图片描述

4.线程等待应用场景

ContinueWhenAny:如果需要在一堆任务中,某一种执行结束后,去触发一个动作;
ContinueWhenAll:如果需要回调和需要控制顺序
WaitAny:如果有不确定的数据源,比如可能是缓存,可能是数据库,可能是第三方接口的时候,我们可以使用WaitAny,只要查到任何一种数据源的数据就直接返回
WaitAll:例如有多个模块信息需要同时加载时。
我之前在做首页优化接口时,也是使用多线程加载各个模块数据:

await Task.WhenAll(
    Task.Run(async () =>
    {
        //首页模块A
    }),
    Task.Run(async () =>
    {
        //首页模块B
    }),
    Task.Run(async () =>
    {
        //首页模块C
    })
);

三、Parallel

1.可以传入多个委托
2.多个委托中的内容是会开启线程来执行—执行这里的线程有可能是新的线程,也有可能是主线程参与计算的
3.会阻塞主线程—相当于是主线程等待子线程执行结束

List<Action> acitonList = new List<Action>();
acitonList.Add(() => { });
acitonList.Add(() => { });
acitonList.Add(() => { });
Parallel.Invoke(acitonList.ToArray());//也是可以开启线程,可以一次放入很多个委托去执行;

我们可以看下这个的运行结果:

Task.Run(() =>
{ 
    Parallel.Invoke(
  () =>
  {
      Debug.WriteLine($"线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
      this.DoSomething("this is Action 01");
  },
  () =>
  {
      Debug.WriteLine($"线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
      this.DoSomething("this is Action 02");
  },
  () =>
  {
      Debug.WriteLine($"线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")}");
      this.DoSomething("this is Action 03");
  });
});

在这里插入图片描述
如果想要不阻塞主线程,可以在外围包一个Task.Run。Parallel是基于Task一个封装;
那么Parallel有什么作用呢?
如果有一大堆的任务要执行;100个任务—需要多线来执行:100个任务要开启100个线程?

Parallel.For(0,100,index=>
 {
    //执行任务
 })

那么我们Parallel最大的优势就是可以控制线程的数量,不至于出现线程泛滥的情况

ParallelOptions options = new ParallelOptions();
//开启5个线程去去执行100个任务
 options.MaxDegreeOfParallelism =5
 Parallel.ForEach(intlist, options, s =>
{
    //执行任务
});

四、多线程的异步处理

我们在日常代码中常常使用try…catch…finally…去捕捉到异常,那么我们在多线程中要如何处理呢?
1.在多线程中,如果发生异常,使用try-catch包裹,是捕捉不到异常的,异常肯定是被吞掉了
2.多线程中,如果要捕捉异常,需要设置主线程等待子线程执行结束;可以捕捉到异常
在这里插入图片描述
3.多线程内部发生异常后,抛出的异常类型是:system.AggregateException(多线程的异常类型)
在这里插入图片描述
当多个线程发生异常要如何捕捉到呢?我们可以做个demo进行测试

try
{
    List<Task> tasklist = new List<Task>();
    for (int i = 0; i < 20; i++)
    {
        string keywork = $"Click_{i}";
        tasklist.Add(Task.Run(() =>
       {
           Thread.Sleep(new Random().Next(50, 100));
           if (keywork == "Click_6")
           {
               throw new Exception("Click_6");
           }
           else if (keywork == "Click_9")
           {
               throw new Exception("Click_9");
           }
           else if (keywork == "Click_12")
           {
               throw new Exception("Click_12");
           }
       }));
    }
    Task.WaitAll(tasklist.ToArray());

}
catch (AggregateException aex) //具体
{
    Debug.WriteLine(aex.Message);
    foreach (var exception in aex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
catch (Exception ex) //父类(Exception 一切异常的父类) 抽象
{
    Debug.WriteLine(ex.Message);
}

我们可以看到AggregateException 会有异常集合!!!

那么我们模拟一个场景,如果三个线程去执行一个业务逻辑体(查询:线程1,执行修改数据库;线程2,执行修改缓存,线程3,执行一个调用接口去修改另外一个服务器上的数据),如果中间有某一个线程出现异常了,那就代表整个逻辑有问题,那要如何处理?
多线程执行的时候,有时候,必须是多个线程都执行成功,才算成功,只要有一个线程异常了,就表示都异常----既然都是异常了,就应该让线程取消,逻辑取消

五、线程取消

线程取消:线程是无法从外部取消的(除非关闭进程),只能自己取消自己。
如何执行标准的线程取消?
1.实例化一个 CancellationTokenSource
2.包含了一个IsCancellationRequested 属性,属性值默认为false
3.包含了一个Cancel方法。Cancel的作用:方法如果被执行,CancellationTokenSource内部的属性值由false变为true,且只能从false变为true

demo:


try
{
    List<Task> tasks = new List<Task>();
    CancellationTokenSource cts = new CancellationTokenSource();
    for (int i = 0; i < 100; i++)  //开启并发线程
    {
        string keywork = $"task_{i}";
        Task.Run(() =>
        {
            Thread.Sleep(new Random().Next(10, 300)); //随机休眠
            if (cts.IsCancellationRequested == false)
            {
                Debug.WriteLine($" 线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 正常开始了");
            }
            else
            {
                Debug.WriteLine($" 线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 没有正常开始了");
            }

            try
            {
                
                if (keywork  == "task_9")
                {
                    throw new Exception("task_9");
                }
                if (keywork == "task_6")
                {
                    throw new Exception("task_6");
                }
            }
            catch (Exception)
            {
                cts.Cancel();  //取消线程
            }

            if (cts.IsCancellationRequested == false)
            {
                Debug.WriteLine($" 线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 正常结束了。。。");
            }
            else
            {

                Debug.WriteLine($" 线程ID:  {Thread.CurrentThread.ManagedThreadId.ToString("00")} {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")} 没有正常结束了。。。");

            }
        });
    }
}
catch (AggregateException aex)
{
    foreach (var exception in aex.InnerExceptions)
    {
        Debug.WriteLine(exception.Message);
    }
}

你们可以自己拿这个demo执行看结果。。。。

4…当有一个线程发生异常的时候,其他线程就会有三种情况:1.已经结束的线程无法去处理,2.已经正常开始,我可以让你取消,让这线程在结束的时候,抛出异常(取消了);3.还有部分的线程根本都还没有开启:只要有异常的线程,就应该让没有开启的线程不再开启了。


try
{
    List<Task> tasks = new List<Task>();
    CancellationTokenSource cts = new CancellationTokenSource();
    for (int i = 0; i < 100; i++)
    {
        string keywork = $"Task_{i}";
        tasks.Add(Task.Run(() =>
        {
            Thread.Sleep(new Random().Next(10, 300));
            cts.Token.ThrowIfCancellationRequested();

            try
            {
                //为什么要先定义一个字符串,然后在这里判断? 
                if ($"Task_{i}" == "Task_9")
                {
                    throw new Exception("Task_9");
                }
                if (keywork == "Task_6")
                {
                    throw new Exception("Task_6");
                }
            }
            catch (Exception)
            {
                cts.Cancel();
            }
            cts.Token.ThrowIfCancellationRequested();
        }, cts.Token)); //有线程发生异常后,还没有开启的线程就不再开启了
    }
    Task.WaitAll(tasks.ToArray());
}
catch (AggregateException aex)
{
    foreach (var exception in aex.InnerExceptions)
    {
        Debug.WriteLine(exception.Message);
    }
}

输出:
在这里插入图片描述
这是表示有剩余未开启的线程已经取消

六、 线程安全

线程安全:一段完整的业务逻辑,单线程执行和多线程执行后的结果如果完全一致,是线程安全的;否则就表示是线程不安全;
这个就是最直接的线程安全问题:
在这里插入图片描述
因为我们开启的是多线程,其中就可能在同一个时刻出现抢占资源的情况,所以会出现线程安全问题。
那要如何解决线程安全问题呢?
1.加锁,标准锁,锁的本质是独占引用。可以解决线程安全问题,加锁是反多线程,会影响性能。

private readonly static object obj = new object();

在这里插入图片描述

2.使用单线程,把要操作的整块数据做一个切分。例如9w条数据,分成三块,再开启多线程,每个线程去执行3W条数据,等待三个线程执行结束后,在单线程做一个汇总。(这个需要将程序加以设计)

3.使用线程安全对象。
这是系统提供的三种线程安全数据集

BlockingCollection<int> blockinglist = new BlockingCollection<int>();
ConcurrentBag<int> conocurrentbag = new ConcurrentBag<int>();
ConcurrentDictionary<string, int> concurrentDictionary = new ConcurrentDictionary<string, int>();
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
ConcurrentStack<int> concurrentStack = new ConcurrentStack<int>();

七、中间变量

在这里插入图片描述
为什么我们每次循环开启多线程,i一直是等于5的呢?
因为Task开启线程的时候是延迟开启的,延迟的时间很短。循环并不会阻塞主线程,当有一个线程在正式的执行业务逻辑的时候,循环已经结束了,所以会取到最后的值!!!

如何解决?
可以另外定义个变量,在每次循环的时候赋值,循环多少次就会有多少个变量(k),每个线程使用的是每一次循环内部的变量(k)
在这里插入图片描述

总结

如果还有其他思路请在评论区指出,我们继续完善,,往共勉!!!!!!!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值