线程
CLR中的线程并不等于操作系统线程,所以代码并不能随心所欲地控制操作系统线程。线程是操作系统调度的最小单元。
基础知识:
0.1、 并行和并发:多核之间叫并行,是真正的同时执行;CPU时间分片是并发,不是真正的同时执行。
0.2、 4核4线程:CPU有四个物理核心,任务管理器会显示出4张CPU图表
4核8线程:使用了超线程技术,把一个物理核心模拟成了2个逻辑核心,所以任务管理器会显示出8张CPU表
0.3 线程不是越多越好,线程的本质是资源换性能,但资源不是无限的,且资源调度会有损耗
0.4 CPU时间分片:微观上来看,多个线程是挨个执行的;宏观上来看,用户感受不到线程的切换,感觉上是一起在执行
0.5 操作系统32位和64位的区别
- 支持内存不同:32位最多4G,64位理论上不限,只要有足够的内存条
- 支持的处理器不同
- 处理数据的能力:32和64代表的是CPU可以处理的最大位数
1、 委托BeginInvoke可以开启一个线程异步执行委托;
1.1、如果异步执行(BeginInvoke)完后,想执行另一个方法,应该怎么做?
非阻塞:利用回调函数。action.BeginInvoke(委托参数, (ar) => { 需要回调的方法},回调参数)
阻塞:
- IAsyncResult的属性IsComplete来判断委托是否完成
- IAsyncResult.AsyncWaitHandle.WaitOne() 实际上是信号量来阻塞住
- EndInvoke(asyncResult) 阻塞住,如果委托有返回值在在这里可以拿到返回值
2、 Thread类:是对线程对象的一个封装,这是CLR中的线程概念。要想开启一个线程,代码首先告诉CLR,CLR再去向操作系统申请。
2.1 Thread与线程池创建的线程相比有两个优点:
- 优先级:Thread线程可以指定优先级Priority,线程池创建的线程均以普通优先级运行。但注意:高优先级的线程并不一定总是比低优先级的线程先执行,操作系统线程不是想让它先执行就先执行,想停止就停止,操作系统有一套自己的调度,指定优先级之后多次统计的话,代码中指定的高优先级线程会比低优先级线程先执行的次数多(概率/运气);另外先执行并不一定能保证先结束;
- IsBackground:指定是否是后台线程
2.2 Thread类中不要用的方法
Suspend()、Resume()、ResetResume() 原因:线程是操作系统的,并不是代码中写一句挂起、恢复就可以立即生效的,会有延迟,也有可能根本就不会停止。
Abort()不建议使用,它是通过抛异常的方法向操作系统发通知(除此之外没有别的办法)来停止线程,会有延时,也不一定真能停下来(例如:网络请求、数据库查询 请求都已经发出去了是没有办法停止的,只能不要去处理返回的数据)
2.3 线程等待
- 判断状态 thread.ThreadState
- thread.Join() 执行这句话的线程等待thread线程执行完毕
- Thread没有自带的回调,需要封装自己封装一层,初始化线程的委托和需要回调的函数重新封装成初始化线程的委托
2.4 thread 获取执行结果,没有现成的,只能封装
需要结果时,就执行返回的委托,产生阻塞等待结果。
3、 ThreadPool静态(.NET Framework 2.0):如果某个对象创建和销毁代价比较高,同时这个对象还可以反复使用,就需要一个池——线程池。
Thread类中提供给开发人员的API过多,因为线程有“运气”的成分,所以开发人员并不能很好的使用,所以ThreadPool中的API很少。
ThreadPool里面的线程都是后台线程,都以普通优先级运行。
3.1使用
ThreadPool.QueueUserWorkItem(需要处理的事情,参数)
3.2 阻塞
没有现成的可以阻塞的方法,只能使用信号量 ManualResetEvent来控制阻塞
false(关闭)—Set打开—true(WaitOne就能通过)
true(打开)—Rest关闭—false(WaitOne只能等待)
3.3 没有现成的支持返回值获取和回调的方法
3.4 设置线程池最大和最小可用数量,全局有效,但只会统计线程池开启的线程
ThreadPool.SetMinThreads(,) ThreadPool.SetMaxThreads
4、 Task(.NET Framework3.0):Thread提供的API太多,太灵活,不好用。ThreadPool提供的API太少。所以Task出现了,Task的线程是基于线程池的,Task提供了丰富的API。
4.1 使用
- Task task = new Task(委托) task.Start()
- Task.Run(委托)
- Task.Factory.StartNew(委托)
- new TaskFactory().StartNew(委托)
4.2 Task.Delay() 异步等待
Task.Delay(等待时间).ContinuedWith(委托) //线程先等待然后再执行委托,其实就相当于委托中有一个Thread.Sleep
4.3 等待,均是阻塞的
- Task.WaitAll()
- Task.WaitAny()
- taskFactory.ContinueWhenAll( , 回调)
- askFactory.ContinueWhenAny( , 回调)
5、 Parallel 在Task基础上又封装了一层主要是为了支持任务并发执行(在同一个时间点一起开始执行),最大特点:主线程也会作为一个线程运行其中的一个任务,因而界面会阻塞
5.1 常用方法
Parrallel.Invoke(多个action)
Parrallel.For(多个action)
Parrallel.ForEach(多个action)
6、 多线程常见问题
6.1 多线程异常处理:多线程里面抛出的异常,会终结当前线程,但是不会影响别的线程,线程异常会被吞掉。
捕获线程异常需要等待 Task.WaitAll(list), 多线程专用异常类 AggregateException
常规建议:多线程的委托中不允许抛出异常,包一层try catch,然后记录下异常信息。
6.2 多线程取消
已经开始执行的任务无法取消, 但是可以通过判断cancelSource.Token.IsCancellation
Requested来控制代码是否结束
用于实现协作取消模型的常规模式是:
- 实例化 CancellationTokenSource 对象,此对象管理取消通知并将其发送给单个取消标记。
- 将 CancellationTokenSource.Token 属性返回的标记传递给每个侦听取消的任务或线程。
- CancellationToken.IsCancellationRequested从接收取消标记的操作中调用方法。 为每个任务或线程提供响应取消请求的机制。 是否选择取消操作以及具体操作方式取决于应用程序逻辑。
- 调用 CancellationTokenSource.Cancel 方法以提供取消通知。 这会将 CancellationToken.IsCancellationRequested 取消标记的每个副本上的属性设置为 true 。
- Dispose CancellationTokenSource对象 。
6.3 线程安全&lock
线程安全的定义: 如果代码在进程中有多个线程同时运行这一段,如果每次运行的结果都跟单线程运行时的结果一致,那么线程就是安全的。
6.3.1 作为锁的对象: 静态、私有、只读
6.3.2 递归调用lock(this) 不会死锁,因为是同一个线程
6.3.3 作为锁的对象不应该是string, string在内存分配上是重用的会冲突
6.3.4 lock里面的代码不要太多,因为线程执行到这只能一个个通过就变成了单线程
6.3.5 线程安全集合:System.Colletions.Concurrent 下面的集合才是线程安全的
6.3.6 建议:避免多线程去操作同一个数据这样就可以不用考虑锁,对数据进行分拆,高效又安全