ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

进程-线程-多线程-异步

2020-02-29 21:42:15  阅读:278  来源: 互联网

标签:异步 Task Console Thread taskList 线程 WriteLine 多线程


进程-线程-多线程-异步

一、多线程的本质

  • cpu的计算速度太快了导致硬件跟不上,就是木桶原理(盛水多少取决于最短板)。
  • cpu的计算能力很强,所以cpu可以分“时间片”,一个cpu可以分N+个时间片,每个时间片上跑一个线程(代码流)。cpu按顺序执行时间片,因为cpu太强,线程切换的太快,导致人感觉不出执行的卡顿,感觉上是多线程并发的。
  • 微观角度:一个核同一时刻只能执行一个线程;
  • 宏观角度: 是多线程并发的。
  • cpu分时间片执行线程,线程所需资源的不同会导致有 上下文的切换 ,其实上下文的切换会有性能的损失,但因为cpu计算能力超过其它硬件太多,不损失也是浪费。
  • 扩展:4核8线程—核是物理的核,这里的线程是指虚拟核(一个核虚拟出2个核)。

1、并发多线程的启动、结束顺序

并发线程的启动是无序。
执行相同代码的并发线程的运行时间也不相同,所以结束时间也不同。

a、如何控制多线程的调用顺序

  • 可以用委托提供的BeginInvoke方法,它是异步线程方法,并支持回调函数,用它来控制线程调用顺序。
    代码:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Thread_Task_Async
{

    public class MoreThread
    {
        public void DoSometing(string name)
        {
            Console.WriteLine($"{name}____{Thread.CurrentThread.ManagedThreadId}");
        }

        public void Do()
        {
            Console.WriteLine($"主线程开始");
            Action<string> action = DoSometing;//无返回值的委托
            IAsyncResult asyncResult = null;//这是BeginInvoke的返回值类型。
            AsyncCallback callback = e =>  //这是回调函数
            {
                Console.WriteLine(object.ReferenceEquals(asyncResult, e));
                Console.WriteLine(e.AsyncState);
                Console.WriteLine($"回调函数完成   {Thread.CurrentThread.ManagedThreadId}");
            };
            //BeginInvoke是异步调用委托,callback是回调函数。
            //BeginInvoke的返回值是回调函数中的参数
            //第三个参数是object类型,它是BeginInvoke的返回值中的字段AsyncState,可以用它传递任何你想给回调函数传的东西。
            asyncResult = action.BeginInvoke("Do", callback, "xxxx");
            Console.WriteLine($"主线程完成");
        }
    }
}

b、阻塞主线程等待子线程

  • 方法一:IAsyncResult类是委托BeginInvoke方法的返回值,IAsyncResult的IsCompleted字段标志这委托是否完成。可以用IsCompleted字段写个循环阻塞主线程。
  • 方法二:IAsyncResult.AsyncWaitHandle.WaitOne();此方法可以阻塞主线程,等待子线程。
  • 方法三:委托的EndInvoke(IAsyncResult)方法可以阻塞主线程,等待子线程。并且该方法带返回值。

二、异步操作的本质

所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。

三、线程

1、Thread

  • Thread是C#推出的最早的封装了操作线程的类。
  • Thread中的Suspend()方法功能是线程挂起,C#现已经弃用,因为此方法在线程被挂起后仍然保持对资源的占有,使其它线程无法访问。
  • Thread中的Resume()方法的功能是唤醒线程,C#现已弃用。
  • Thread中的Abort()方法的功能是销毁,方式是抛出异常。不建议用。(主线程告诉子线程Abort时,需要时间,所以子线程停止运行会不及时,并且有些动作不能停止。)
  • Thread.ResetAbort();//取消异常。
  • Thread.Join();//当前线程等待thread完成。
  • Thread.Sleep(100);//当前线程休息100ms,线程在等待时cpu会把时间片交出去,也就是说cpu的这个时间片可以做别的事,但是内存是被占有的。
  • Thread.IsBackground = true;//true为 后台线程 ,false为 前台线程 ,默认为false。
  • Thread.Priority = ThreadPriority.Highest;// 设置线程优先级,CPU会给优先级高的线程分配时间片。

2、ThreadPool

  • DotNetFramework 2.0推出的
  • ThreadPool.QueueUserWorkItem(); // 把用户的工作队列进线程池。
  • ThreadPool.GetMaxThreads(); // 获取最大线程数,和最大I/O线程数
  • ThreadPool.GetMinThreads(); // 获取最小线程数,和最小I/O线程数
  • ThreadPool.SetMaxThreads() // 设置最大线程数,和最大I/O线程数
  • ThreadPool.SetMinThreads() // 设置最小线程数,和最小I/O线程数
  • ThreadPool没有提供阻塞主线程等待子线程完成的方法。
    可以用ManualResetEvent类,它有一个bool值的字段A,一个WaitOne()方法、Set()方法、Reset()方法。
    //A为false时,线程执行到WaitOne方法时,会被等待。
    //A为true时,线程执行到WaitOne方法时,可以通过。
    //Set()方法,会将A赋值为true。
    //Reset()方法,会将A赋值为flase。
    根据此类的特性,可以用它来实现“阻塞主线程等待子线程完成”的功能。
  • 回调函数可以自己模拟出来。
    做法:将两个委托传递给一个方法,此方法开一个线程顺序执行这两个委托。
  • 对ThreadPool进行设置,对Task和TaskFactory是生效,因为它们是基于ThreadPool的。

3、Task

  • DotNetFramework 3.0 的产物
  • Task 是基于ThreadPool的
  • Task.Run(委托) // 线程的启动方式。
  • Task(委托).Start() // 线程的启动方式
  • Task.WaitAll(Task数组) // 等待所有线程完成
  • Task.WaitAny(Task数组) // 等待任一线程完成
  • Task.WhenAll(Task数组) // 等待所有线程完成,并且返回值为Task,作用是返回Task后可以使用链式编程。
  • Task.WhenAny(Task数组) //同上
  • Task.ContinueWith(委托) // 此委托也是异步线程调用的(它也相当与回调函数)。
  • 如何控制线程的并发数量,不要用设置ThreadPool的线程池线程数量来控制。 用下面的方法。
    如下代码:
 private void button2_Click(object sender, EventArgs e)
        {
            Console.WriteLine("22222222222222222");
            List<int> list = new List<int>();
            for(int i =0; i<10000;i++)
            {
                list.Add(i);
            }
            Action<int> action = i =>
            {
                Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
                Thread.Sleep(new Random(i).Next(100, 300));
            };
            List<Task> taskList = new List<Task>();
            foreach(var i in list)
            {
                var k = i;
                taskList.Add(Task.Run(() => action.Invoke(k)));
                if(taskList.Count > 10)
                {
                    Task.WaitAny(taskList.ToArray());
                    taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
                }
            }
            Task.WhenAll(taskList.ToArray());
            Console.WriteLine("22222222222222222");
        }
  • 建议:把Task线程放在List 里比较好控制。
  • Task.Delay(2000) // 延迟2秒,它和Sleep的区别是Delay是不卡界面的,也就是说Delay是另起了一个线程。
    Task.Delay(2000).ContinueWith(委托) // 用户在执行一个操作后延迟2秒,在执行委托里的操作。

4、TaskFactory

  • DotNetFramework 4.0 的产物
  • TaskFactory.StartNew(委托) // 线程的启动方式。
  • TaskFactory.ContinueWhenAll(Task数组,回调委托) // 它是Task.WhenAny和Task.ContinueWith的结合。

5、Parallel

  • 并行编程
  • 是在Task的基础上做的封装
  • Parallel,主线程参与计算,节约了一个线程。
  • Parallel.Invoke(委托数组) // 启动线程,主线程参与计算
  • Parallel.For(0,5, i => {…} ) //执行5个线程,主线程 参与计算
  • Parallel.ForEach(object[] object , i => {…} ) // 执行线程,主线程 参与计算
  • ParallelOptions.MaxDegreeOfParallelism = 3 // 控制并发数量
  • Parallel支持结束多线程的本次线程和结束Parallel所有线程
    如下代码:
    Stop()和Break()不能同时存在于一个线程中。不推荐用这两个方法。
    需要注意的是Parallel没有浪费主线程,而Break()是结束当前线程,如果碰巧该Break()结束的是主线程,那么其他线程也会被干掉。
private void button3_Click(object sender, EventArgs e)
        {
            ParallelOptions parallelOptions = new ParallelOptions();
            parallelOptions.MaxDegreeOfParallelism = 3;
            Parallel.For(0, 40, parallelOptions, (i, state) =>
            {
                Console.WriteLine($"111111111111111111111111111111----{Thread.CurrentThread.ManagedThreadId}");
                //if (i == 2)
                //{
                //    Console.WriteLine("线程取消,当前任务结束");
                //    state.Break();//当前这次线程结束
                //    return;//有return代表此代码之后的操作不执行。 
                //}
                if (i == 20)
                {
                    Console.WriteLine("线程取消,当前任务结束");
                    state.Stop();   //  结束Parallel所有线程
                    return;//有return代表此代码之后的操作不执行。 
                }
                Console.WriteLine($"22222222222222222222222222----{Thread.CurrentThread.ManagedThreadId}");
            });
        }

6、async await 语法糖

  • 机制: 在async标记的方法中,以 await为界限;await之前的代码由主线程执行,await之后的代码其实被封装成了委托回调函数。该委托会在await task执行完成之后被调用,该委托也是线程的。
  • 主线程遇到await就会返回做自己的事情。
  • async await 是用状态机模式实现的。await task(委托)是一块,await 之前是一块,await之后是一块。主线程先执行await之前的代码,主线程遇到await启动子线程之后就从方法体里返回了,去做自己的事了。由子线程执行await task委托里的代码;子线程执行完之后;await之后的代码就会由任意线程执行。
  • async里的三块代码是顺序执行的。

四、异常处理

  • 异步线程里面的异常是被吞掉了,因为主线程执行完之后已经脱离try catch的范围了。
  • 如果想抓到线程里的异常,就需要await 线程。
  • AggregateException是专门处理线程的异常类。
  • 异常处理的建议:线程里不允许出现异常,自己处理好。

五、线程取消

  • task是外部无法中止的(你只能告诉OS,我要中止这个线程,而OS在接到线程中止命令后什么时候相应,你无法知道),因为线程是OS的资源,你无法掌控线程是什么时候取消的。(可能会有延迟)
  • Thread.Abort不靠谱,Abort是告诉OS取消这个线程,它有可能马上停止,也有可能有延迟(是不可控制的)。
  • 举个例子:
    你有个操作是写数据库的。Abort告诉OS停止后,OS可能有延迟,在这延迟的这段时间,cpu就有可能已经执行到数据库去写数据了。这就与我的意愿相违背了。(注意:数据库是基于http请求的,不可取消)
  • 线程取消的方法:
    原理:要让线程自己停止自己,声明一个多线程的公共访问变量,想让其他线程停止时,就改变这个公共变量的值,其他线程不断的检测它,根据值判断是否抛出 异常。
  • CancellationTokenSource此类时微软封装的标志任务是否取消,Cancel方法可使cancellationToken变量标志取消,IsCancellationRequested可以读取this是否标志取消
  • 代码参考:
private void button4_Click(object sender, EventArgs e)
        {
            try
            {
                List<Task> taskList = new List<Task>();
                TaskFactory taskFactory = new TaskFactory();
                CancellationTokenSource cts = new CancellationTokenSource();
                for (int i = 0; i < 40; i++)
                {
                    string name = String.Format($"this my work{i}");
                    Action<object> act = t =>
                    {
                        try
                        {
                            Thread.Sleep(2000);
                            if(t.ToString().Equals("this my work11"))
                            {
                                throw new Exception(string.Format($"{t} 执行失败"));
                            }
                            if (t.ToString().Equals("this my work12"))
                            {
                                throw new Exception(string.Format($"{t} 执行失败"));
                            }
                            if (cts.IsCancellationRequested)
                            {
                                Console.WriteLine($"{t}放弃执行");
                                return;
                            }
                            else
                            {
                                Console.WriteLine($"{t}执行成功");
                            }
                        }
                        catch (Exception ex)
                        {
                            cts.Cancel();
                            Console.WriteLine(ex.Message);
                        }
                    };
                    taskList.Add(taskFactory.StartNew(act, name, cts.Token));
                }
                Task.WaitAll(taskList.ToArray());
            }
            catch(AggregateException ae)
            {
                foreach(var item in ae.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
        }

六、线程安全

  • 多线程访问公共变量时可能会出问题。
  • 线程安全问题分析:
    代码:
private void button5_Click(object sender, EventArgs e)
        {
            #region 此代码存在线程安全问题
            TaskFactory taskFactory = new TaskFactory();
            List<Task> taskList = new List<Task>();
            int TotalCount = 0;
            List<int> intList = new List<int>();
            for (int i = 0; i < 1000; i++)
            {
                int newI = i;
                taskList.Add(taskFactory.StartNew(() =>
                {
                    TotalCount += 1;//TotalCount是公共资源,多线程用此资源存在问题(有可能两个线程同时获取到此资源进行+1,这样就少加了一个1).
                    intList.Add(newI);//这个同理
                }));
            }
            Task.WaitAll(taskList.ToArray());
            Console.WriteLine($"{TotalCount}");
            Console.WriteLine($"{taskList.Count}");
            Console.WriteLine($"{intList.Count}"); 
            #endregion
        }
  • 三种解决方法:
    第一种: lock解决,因为只有一个线程可以进去,没有并发,所以解决了问题,但是牺牲了性能。
  1. 用的时候注意尽量缩小lock的范围。
  2. 声明锁,必须是引用类型,值类型无效, 用private可以保证其他类不会锁此资源,static保证全局唯一性(也可以不声明)。
  3. 锁不可以声明为string类型,虽然它是引用类型,但是因为它用了享元模式。
private static readonly object ClickLock = new object();//这是微软推荐的锁的写法。
private void button5_Click(object sender, EventArgs e)
        {
            #region lock解决
            {
                TaskFactory taskFactory = new TaskFactory();
                List<Task> taskList = new List<Task>();
                int TotalCount = 0;
                List<int> intList = new List<int>();
                for (int i = 0; i < 1000; i++)
                {
                    int newI = i;
                    taskList.Add(taskFactory.StartNew(() =>
                    {
                        lock (ClickLock)
                        {
                            TotalCount += 1;
                            intList.Add(newI);
                        }
                        
                    }));
                }
                Task.WaitAll(taskList.ToArray());
                Console.WriteLine($"{TotalCount}");
                Console.WriteLine($"{taskList.Count}");
                Console.WriteLine($"{intList.Count}");
            }
            #endregion
        }

第二种: 安全队列 ConcurrentQueue,用一个线程去完成操作(不止单线程,还用了其他技术)。
第三种: 使用多线程,注意不要冲突,进行数据拆分,避免冲突。也是最推荐的一种。

Core松 发布了8 篇原创文章 · 获赞 0 · 访问量 110 私信 关注

标签:异步,Task,Console,Thread,taskList,线程,WriteLine,多线程
来源: https://blog.csdn.net/qq_41807126/article/details/104288894

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有