多线程-Thread和ThreadPool
多线程原理
多线程都是基于委托的。
多线程优缺点
缺点:
1、导致程序复杂,开发调试维护困难,特别是线程交互。
2、线程过多导致服务器卡死,因为占用CPU 内存等资源。
优点:
1、良好的交互,特别对于复杂性的操作,用户要等待很久,界面卡着不动。
2、充分利用服务器资源,提高整个系统性能。
3、在没有界面的系统中,比如说接口,服务等等系统中,当一个任务特别耗时,等他其他服务器的相应 ,这个等待的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。现在的mvc中都自动加入了async await了。
4、多线程对复杂的计算很有效果,CPU多核效果更明显。 ( 当前CPU核心数量 Environment.ProcessorCount)
5、C#中使用多线程的地方。自己创建使用。另外一种是框架中自带的。比如BackgroundWorker类, 线程池,threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。(曾经面试的时候,问道多线程 给面试官说asp.net就是多线程的,他说不是的。唉。。。)
多线程创建
1、Thread()
参数ThreadStart,ParameterizedThreadStart
ThreadStart:一个没有参数委托
ParameterizedThreadStart:有一个参数的委托,参数为object(又是委托,参数还是object。可以用类、泛型封装一下)。
特点:消耗CPU 内存,但是速度快。线程的启动和执行,执行时间(OS对CPU时间片划分)都是无序的 不确定的,不要人为控制。
异步委托,其实是线程池,线程池是向CLR申请,CLR向OS(操作系统)申请。
缺点:只能有一个参数而且为object,没有返回值,不易管理线程(线程池有没有内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成是获得一个返回值,后面将有task async await 控制线程。也是FCL发展的进步)
前台后台线程
Thread.IsBackground 默认为前台线程
前台线程:比如说一个winform程序,里面起了一个前台线程,当winfrom窗体关闭时,这个前台线程还在执行的,知道他执行完毕。不收主线程影响。线程是寄托在进程上的,只要有一个前台线程没有退出,进程就不会结束,
后台线程:进程退出,后台线程也随着关闭。
线程优先级
在多线程的程序运行中会有同时来自多个线程请求,此时为了在请求队列中优先处理某一些线程请求,从而对线程进行了优先级的划分。在线程的优先级中一共分为以下五个等级:
Hightest、
AboveNormal、
Normal、
BelowNomal、
Lowest
在调用线程之前给线程赋值他的优先级别。
myThread.Priority = ThreadPriority.Lowest;
注意:1、在调线程优先级的时候,也可以设置相关联进程的优先级别(Process.GetCurrentProcess().PriorityClass)可以提高速度(不推荐用)。
2、线程优先级一共有五个,从中间到两边。默认中间Normal级别。
线程阻塞及状态
1、线程创建
线程创建后,没有启动此时状态为Unstarted
状态定义:尚未对线程调用 System.Threading.Thread.Start 方法。
2、Start()
定义:导致操作系统将当前实例的状态更改为 System.Threading.ThreadState.Running。
线程启动,线程体开始执行,状态改为Running。
Runidng定义:线程已启动,它未被阻塞,并且没有挂起的 System.Threading.ThreadAbortException。
3、Suspend()
定义:挂起线程,或者如果线程已挂起,则不起作用。
不过改方法已经过时, 方法执行后线程对应的状态为Suspended。改状态不占用cpu,不占用资源。
Suspended定义:线程已挂起。
4、Resume()
定义:继续已挂起的线程。
不过改方法已经过时, 方法执行后线程对应的状态为Running。
Running定义:线程已启动,它未被阻塞,并且没有挂起的 System.Threading.ThreadAbortException。
5、Sleep(...)
定义:将当前线程挂起指定的时间。可以定义时间参数
让线程挂起定义的时间,或者叫休眠,不在占用内存CPU。状态改为WaitSleepJoin
WaitSleepJoin定义:线程已被阻止。这可能是因为:调用 System.Threading.Thread.Sleep(System.Int32) 或 System.Threading.Thread.Join、请求锁定(例如通过调用
System.Threading.Monitor.Enter(System.Object) 或 System.Threading.Monitor.Wait(System.Object,System.Int32,System.Boolean))或等待线程同步对象(例如
System.Threading.ManualResetEvent)。
6、Join(...)
定义:在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。(是不是看定义一脸蒙蔽。其实就是卡着其他线程(包括其他主线程 子线程),一直等该子线程走完。和endinvoke类似)。继续占用CPU和内存,线程方法一直在执行。可以设置时间参数。
如果后续的处理依赖于另一个已经终止的线程,可以调用Join()方法,等待线程中止。等待该线程结束在往下走。
7、Abort()
定义:在调用此方法的线程上引发 System.Threading.ThreadAbortException,以开始终止此线程的过程。调用此方法通常会终止线程。
调用方法后线程就死了,不能在恢复了,资源也被回收了。线程状态修改为Aborted 。
Aborted定义:线程状态包括 System.Threading.ThreadState.AbortRequested 并且该线程现在已死,但其状态尚未更改为 System.Threading.ThreadState.Stopped。
就讲这几种常用的把,用得多容易出问题。。。 程序复杂度,开发难度,维护难度。
线程锁、共享资源、同步、线程通信
- 锁(同步锁)
一、LOCK(排它锁)
LOCK(A)原理: 个人认为这个A和他的地址,是否变化有关系。(当另一个线程进去的时候就会检查地址内容等等,如果发现和原来锁起来的不一样,就认为没没有锁起来,所有锁起来一个永远不会变化的类型private static readonly object)。
LOCK是一个关键字。开始处调用Monitor.Enter,而在块的结尾处调用 Monitor.Exit。lock就是Monitor.Enter和Monitor.Exit的包装。 如果 Interrupt 中断正在等待输入 lock 语句的线程,将引发 ThreadInterruptedException。
工作过程:锁起来的代码视为临界区,其实是互斥锁锁起来的。,让多线程访问临界区代码时,必须顺序访问。他的作用是在多线程环境下,确保临界区中的对象只被一个线程操作,防止出现对象被多次改变情况。
lock各种类型分析:没有对错,只是看运用的场景以及通用性。(看着网上N多人说应该所这个,不应该锁那个。编译都没报错,自己逻辑写的不符合业务怪语言了)
1、lock(this)
this为当前实例,但是实例经常会变化,导致不稳定。
2、lock(typeof())
锁定一个实例的类型,会把所有的类型都锁定,锁定范围广。
3、lock(string)
这个要从stirng的clr暂留讲起。简单的说就是string作为一个常用的类型,被设计为高性能但是消耗资源,同一个系统中所有值一样的string其实都是一个(地址一样 值一样)。所以锁的时候会锁起来很多相同的string。
4、最佳实践:锁起来一个唯一的,系统起来之后不会变动的。确定锁定范围。
private static readonly object A=new object()
lock(A){
临界区
}
代码的话,大家可以找一个单利模式的代码。
二、Monitor(排它锁)
Monitor.Enter和Monitor.Exit这个东西跟lock的作用一样。事实上。lock就是Monitor.Enter和Monitor.Exit的包装。
Monitor.Wait(object......)定义:释放对象上的锁并阻止当前线程,直到它重新获取该锁。
当线程调用 Wait 时,它释放对象的锁并进入对象的等待队列,对象的就绪队列中的下一个线程(如果有)获取锁并拥有对对象的独占使用。Wait()就是交出锁的使用权,使线程处于阻塞状态,直到再次获得锁的使用权。
Monitor.Pulse(object) 定义:通知等待队列中的线程锁定对象状态的更改。
当前线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。 (但是该线程方法还是会继续执行,所以经常pulse后再加一个wait)
|- 拥有锁的线程 lockObj->|- 就绪队列(ready queue) |- 等待队列(wait queue)
推荐:http://www.cnblogs.com/zhycyq/articles/2679017.html
三、Interlock(原子锁)
当多个任务对一个int 型整数进行自增操作时,(是int简单类型而不是Integer对象),也需要用到同步方法。但如果这时使用Lock,Moniter或SpinLock时总是显得得不偿失。一个自增操作的开销非常小,但此时加锁,等待,解锁操作的开销远远大于一个自增的操作。这时可以考虑使用 Interlock类,它提供了原子操作。而且需要的代价非常低,简单轻便。仅对整形数据(即int类型,long也行)进行同步。(因为加减在计算机底层不是原子性操作 什么寄存器锁存器之类的)
推荐文章:http://blog.csdn.net/kkfdsa132/article/details/5474013
四、VolatileRead VolatileWrite(原子锁)
VolatileWrite:立即向字段写入一个值,以使该值对计算机中的所有处理器都可见。 当线程在共享区(临界区)传递信息时,通过此方法来原子性的写入最后一个值;
VolatileRead:读取字段值。无论处理器的数目或处理器缓存的状态如何,该值都是由计算机的任何处理器写入的最新值。 当线程在共享区(临界区)传递信息时,通过此方法来原子性的读取第一个值;
总结:这两个操作,都是操作的最新值。记住这个就可以了。因为加减法操作不是原子操作。
释放对象上的锁并阻止当前线程,直到它重新获取该锁。
五、volatile关键字(原子锁) 开头字母为小写
(开头大写的Volatile,是一个类volatile是对Volatile的封装)
volatile关键字的本质含义是告诉编译器,声明为Volatile关键字的变量或字段都是提供给多个线程使用的。Volatile无法声明为局部变量。作为原子性的操作,Volatile关键字具有原子特性,所以线程间无法对其占有,它的值永远是最新的。
简单来说volatile关键字是告诉c#编译器和JIT编译器,不对volatile标记的字段做任何的缓存。确保字段读写都是原子操作,最新值。
volatile支持的类型:
- 引用类型;
- 指针类型(在不安全的上下文中);
- 类型,如 sbyte、byte、short、ushort、int、uint、char、float 和 bool;
- 具有以下基类型之一的枚举类型:byte、sbyte、short、ushort、int 或 uint;
- 已知为引用类型的泛型类型参数;
- IntPtr 和 UIntPtr;
六、ReaderWriterLock
当某个线程进入读取模式时,此时其他线程依然能进入读取模式,假设此时一个线程要进入写入模式,那么他不得不被阻塞。直到读取模式退出为止。
老式写法为LOCK,interlock不能实现读写分离。当读的次数大于写的次数,LOCK就会影响性能。
在.NET3.5中提供ReaderWriterLockSlim,如需要在2.0中使用请换ReaderWriterLock,用法差不多改了写方法名,MSDN中说ReaderWriterLockSlim性能比较高
ReaderWriterLockSlim
http://www.cnblogs.com/08shiyan/p/6423532.html
七、SemaphoreSlim
老版本为Semahore 为一个密封类型,继承: WaitHandle。新版本为SemaphoreSlim,非静态类类型。
System.Threading.Semaphore)是表示一个Windows内核的信号量对象。如果预计等待的时间较短,可以考虑使用SemaphoreSlim,它则带来的开销更小(效率高)。
Semahore 定义:限制可同时访问某一资源或资源池的线程数。(信号量)
WaitOne();定义:阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。(此方法来自他的父类WaitHandle)
SemaphoreSlim 定义:对可同时访问资源或资源池的线程数加以限制的 System.Threading.Semaphore 的轻量替代。
1、Wait() 定义:阻止当前线程,直至它可进入 System.Threading.SemaphoreSlim 为止。
可以设置等待时间。其实就是等待有可用信号量就执行线程。
2、Release()定义:退出 System.Threading.SemaphoreSlim 一次。
释放一个信号量。
3、AvailableWaitHandle
注意锁后面带Slim的都是轻量级的。后面版本出来的。
八、Event(事件锁)
AutoResetEvent(自动重置事件)
定义:通知正在等待线程已发生事件。 (发送信号 线程同步)
继承AutoResetEvent:EventWaitHandle : WaitHandle
public AutoResetEvent(bool initialState);
若要将初始状态设置为终止,则为 true;若要将初始状态设置为非终止,则为 false。
waitone()定义:阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。
set()定义:将事件状态设置为终止状态,允许一个或多个等待线程继续。
reset()定义:将事件状态设置为非终止状态,导致线程阻止。(线程还是会往下走。)
(感觉官方定义都解释的让门莫不着头脑,下面是个人理解)
Set方法将信号置为发送状态 Reset方法将信号置为不发送状态WaitOne等待信号的发送。
通俗的来讲只有等myResetEven.Set()成功运行后,myResetEven.WaitOne()才能够获得运行机会;Set是发信号,WaitOne是等待信号,只有发了信号,等待的才会执行。如果不发的话,WaitOne后面的程序就永远不会执行。
ManualResetEvent(手动重置事件)
作用 通过线程间相互发送通知,达到线程同步,资源同步
和AutoResetEvent结账2类似,
区别:AutoResetEvent.Set只能唤醒一个线程。ManualResetEvent.Set可以唤醒所有WaitOne的线程。
工作过程:1、AutoResetEvent.Set()后,当某个线程得到信号,AutoResetEvent会自动又将信号置为不发送状态,则其他调用WaitOne的线程只有继续等待.也就是说,AutoResetEvent一次只唤醒一个线程。
2、ManualResetEvent.Set()后,所有其他调用waitone的线程获得信号得以继续执行,而ManualResetEvent不会自动将信号置为不发送.也就是说,除非手工调用了ManualResetEvent.Reset().方法,则ManualResetEvent将一直保持有信号状态,ManualResetEvent也就可以同时唤醒多个线程继续执行。如果AutoResetEvent的程序换成ManualResetEvent的话,就需要在waitone后面做下reset。
九、Mutex(互斥锁)
定义:一个同步基元,也可用于进程间同步。
Mutex : WaitHandle
同一时间只能一个线程获取。
我们可以把Mutex看作一个出租车,乘客看作线程。乘客首先等车,然后上车,最后下车。当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。
互斥锁可适用于一个共享资源每次只能被一个线程访问的情况。
线程使用Mutex.WaitOne()方法等待C# Mutex对象被释放,如果它等待的C# Mutex对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而
在此期间,其他想要获取这个C# Mutex对象的线程都只有等待。
//创建一个处于未获取状态的互斥锁
Public Mutex();
//如果owned为true,互斥锁的初始状态就是被主线程所获取,否则处于未获取状态。如果设为true,子线程要用的时候,主线程一定要调用方法ReleaseMutex方法释放(测试例子的时候经常出错,主要是这个参数不理解)
Public Mutex(bool owned);
如果要获取一个互斥锁。应调用互斥锁上的WaitOne()方法,该方法继承于Thread.WaitHandle类
它处于等到状态直至所调用互斥锁可以被获取,因此该方法将组织住主调线程直到指定的互斥锁可用,如果不需要拥有互斥锁,用ReleaseMutex方法释放,从而使互斥锁可以被另外一个线程所获取。
十、锁定集合
每个非泛型集合,都提供了一个SyncRoot的属性,如果要锁定集合,请锁定此属性。比如Queue、ArrayList、HashTable和Stack,已经提供了一个供lock使用的对象SyncRoot。
遗憾的是,我们现在已经不鼓励使用非泛型集合了,所以,泛型集合的锁定,请自己选择或创建一个锁定对象来实现。
十一、为什么我们需要锁
首先要理解锁定是解决竞争条件的,也就是多个线程同时访问某个资源,造成意想不到的结果。比如,最简单的情况是,一个计数器,两个线程 同时加一,后果就是损失了一个计数,但相当频繁的锁定又可能带来性能上的消耗,还有最可怕的情况死锁。那么什么情况下我们需要使用锁,什么情况下不需要 呢?
1)只有共享资源才需要锁定
只有可以被多线程访问的共享资源才需要考虑锁定,比如静态变量,再比如某些缓存中的值,而属于线程内部的变量不需要锁定。
2)多使用lock,少用Mutex
如果你一定要使用锁定,请尽量不要使用内核模块的锁定机制,比如.NET的Mutex,Semaphore,AutoResetEvent和 ManuResetEvent,使用这样的机制涉及到了系统在用户模式和内核模式间的切换,性能差很多,但是他们的优点是可以跨进程同步线程,所以应该清 楚的了解到他们的不同和适用范围。(其实是少使用继承WaitHandler的锁)(有些锁加油Slim结尾,这种事轻量级的,可以使用)
3)了解你的程序是怎么运行的
实际上在web开发中大多数逻辑都是在单个线程中展开的,一个请求都会在一个单独的线程中处理,其中的大部分变量都是属于这个线程的,根本没有必要考虑锁 定,当然对于ASP.NET中的Application对象中的数据,我们就要考虑加锁了。
4)把锁定交给数据库
数 据库除了存储数据之外,还有一个重要的用途就是同步,数据库本身用了一套复杂的机制来保证数据的可靠和一致性,这就为我们节省了很多的精力。保证了数据源 头上的同步,我们多数的精力就可以集中在缓存等其他一些资源的同步访问上了。通常,只有涉及到多个线程修改数据库中同一条记录时,我们才考虑加锁。
5)业务逻辑对事务和线程安全的要求
这 条是最根本的东西,开发完全线程安全的程序是件很费时费力的事情,在电子商务等涉及金融系统的案例中,许多逻辑都必须严格的线程安全,所以我们不得不牺牲 一些性能,和很多的开发时间来做这方面的工作。而一般的应用中,许多情况下虽然程序有竞争的危险,我们还是可以不使用锁定,比如有的时候计数器少一多一, 对结果无伤大雅的情况下,我们就可以不用去管它。
- 资源共享
在多线程的环境中,可能需要共同使用一些公共资源,这些资源可能是变量,方法逻辑段等等,这些被多个线程共用的区域统称为临界区(共享区),临界区的资源不是很安全,因为线程的状态是不定的,所以可能带来的结果是临界区的资源遭到其他线程的破坏,我们必须采取策略或者措施让共享区数据在多线程的环境下保持完成性不让其受到多线程访问的破坏。
- 同步
当多个线程访问同一资源时候,就需要线程同步,一个一个访问,不然容易出现数据错误不一致。
推荐 http://www.cnblogs.com/kissdodog/category/464176.html
多线程等待、回调
多线程Thread自身不带回调函数,线程执行过程,以及返回值,参数一般还是Object。导致很多不便。
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
private static void ThreadCallBack(Action act, Action callback) { ThreadStart ts = () => { act(); callback.Invoke(); }; Thread t = new Thread(ts); t.Start(); }
其实就是把两个方法逻辑放进线程start委托里面。
如果想用Thread获得返回值。但是由于Thread都是不带返回值的。我们只能设置一个共有变量,然后设置这个变量的值,但是后面线程也要join(),等待线程执行完毕。
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
private static stringThreadCallBack(Func<string> act, Action callback) { string str=""; ThreadStart ts = () => { str= act(); callback.Invoke(); }; Thread t = new Thread(ts); t.Start(); t.join(); return str; }
线程池
原理:一直创建线销毁程,导致线程数量比较多,调度频繁。线程池,先准备好一定数量的线程,给需要的线程使用,使用完之后在还给线程池。这样可以限制使用线程的数量。
一直创建线程。这样就一直创建线程 会浪费资源。所以出现了线程池。
好处:较少线程创建 开启 销毁等消耗的资源。线程池会先初始化几个线程,供使用,使用完的线程还会还给线程池。
什么时候使用:单个任务简单。但是需要创建很多这个简单任务。
线程池最多管理线程数量=“处理器核心数 * 250”; 注意有的服务器是多U的,核心数是所有CPU 的总和。
最小线程数:CPU的核心数量。
通过线程池创建的线程 默认 为后台线程,而且优先级别为Normal。
异步、Task、Async Await都是用的线程池。(异步使用endinvok就会自动回收线程)
ThreadPool.QueueUserWorkItem()
第一个参数为WaitCallback:为要执行的方法。 WaitCallback是一个委托,(嘿嘿是一个委托 上下端分离,逻辑分离)
第二个参数为object:要执行的方法的参数。注意:参数为object
初识TASK
定义:表示一个异步操作。
public class Task : IThreadPoolWorkItem, IAsyncResult, IDisposable
异步操作,想起了委托。IAsyncResult就是委托异步返回的结果,IThreadPoolWorkItem应该task也像线程池一样。