线程

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来控制代码是否结束

 

用于实现协作取消模型的常规模式是:

 

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 建议:避免多线程去操作同一个数据这样就可以不用考虑锁,对数据进行分拆,高效又安全

posted @ 2021-05-24 11:12  碗粥  阅读(107)  评论(0编辑  收藏  举报