若有不妥之处,欢迎批评指正。html
这个问题,对于系统架构师、设计者、程序员,都是首先要面对的一个问题。程序员
在何时使用多线程技术?sql
在许多常见的状况下,可使用多线程处理来显著提升应用程序的响应能力和可用性。数据库
上一章,咱们讲了几个多线程的应用案例,主要的应用场景也作了介绍。这里再也不赘述。编程
http://www.cnblogs.com/yank/p/3232955.html安全
使用多线程,这是一个必需要弄清的问题。只有了解了多线程对结构和程序的影响,才能真正会使用多线程,使其发挥应有的效果。服务器
为何应用多线程就不安全了呢?多线程
线程安全的一个断定指标,线程之间有没有临界资源,若是有临界资源,且没有采用合理的同步机制,就会出现多个线程竞争一个资源,如若多个线程都在为得不到所需的资源,则会发生死锁。死锁,线程就会彼此僵持,系统停滞不前,若是后果严重,则直接致使系统崩溃。常见的案例有:生产者与消费者问题、哲学家就餐问题等。架构
咱就根据哲学家就餐问题作个简化:两我的去餐馆吃饭,因为资源紧张,只有一双筷子,每一个人都饿了,都想吃饭,且同时去抢筷子,平分秋色,两人每人抢到一根筷子,只有使用一双筷子才能吃饭。这时你会说了,我能够用手抓着吃,呵呵。若是是刚出锅的饺子,怕你抓不起来。两我的只能面面相觑,大眼瞪小眼,就是吃不上。若是若是僵持个一年半载,都饿死了。哈哈。若是咱们给一个约定,在拿筷子时,一下拿到一双,且吃完就交换给对方。则两我的都高高兴兴吃上饭了。筷子就是临界资源。固然,在两我的僵持的时候,能够进行外部干预,使得两我的都有饭吃。好比:强制一方将筷子空闲出来,则另外一方就饭吃了。吃完了筷子空闲出来,则另外一我的也有饭吃了。异步
只要咱们处理好临界资源问题,也就解决了线程安全问题。
使用多线程,未必必需要作好线程同步,可是若是有临界资源,则必须进行线程同步处理。
在OOP中,程序员使用的无非是:变量、对象(属性、方法)、类型等等。
1)变量
变量包括值类型和引用类型。
值类型是线程安全的,可是若是做为对象的属性,值类型就被附加到对象上,须要参考对象的线程安全性。
引用类型,这里要注意的是,对于引用对象,他包括了引用和对象实例两部分,实例须要经过对其存储位置的引用来访问,对于
private Object o = new Object(),
其实能够分解为两句话:
private Object o;
o = new Object();
其中private Object o是定义了对象的引用,也就是记录对象实例的指针,而不是对象自己。这个引用存储于堆栈中,占用4个字节;当没有使用o = new Object()时,引用自己的值为null,也就是不指向任何有效位置;当o = new Object()后,才真正根据对象的大小,在托管堆中分配空间给对象实例,而后将实例的指针位置赋值给前面的引用。这才完成一个对象的实例化。
引用类型的安全性,在于:能够由多个引用,同时指向一个内存地址。若是一个引用被修改,另外一个也会修改。
using System; namespace VariableSample { class Program { static void Main(string[] args) { Box b1 = new Box(); b1.Name = "BigBox"; Console.WriteLine("Create Box b1."); Console.WriteLine("Box: b1'Name is {0}.", b1.Name); Console.WriteLine("Create same Box b2."); Box b2 = b1; b2.Name = "LittleBox"; Console.WriteLine("Box: b2's Name is {0}.",b2.Name); Console.WriteLine("Box: b1's Name is {0}.", b1.Name); Console.ReadKey(); } } /// <summary> /// 盒子 /// </summary> public class Box { /// <summary> /// 名称 /// </summary> public string Name { get; set; } } }
输出结果:
Create Box b1. Box: b1'Name is BigBox. Create same Box b2. Box: b2's Name is LittleBox. Box: b1's Name is LittleBox.
这里对盒子名字修改,是对两个引用对象修改,其实咱们能够将其设计为两个多线程对对象的修改。这里必然存在线程安全性问题。
总之,变量的线程安全性与变量的做用域有关。
2)对象
对象是类型的实例
在建立对象时,会单独有内存区域存储对象的属性和方法。因此,一个类型的多个实例,在执行时,只要没有静态变量的参与,应该都是线程安全的。
这跟咱们调试状态下,是不同的。调试状态下,若是多个线程都建立某实例的对象,每一个对象都调用自身方法,在调试是,会发现是访问的同一个代码,多个线程是有冲突的。可是,真正的运行环境是线程安全的。
以销售员为例,假设产品是充足的,多个销售员,销售产品,调用方法:Sale(),其是线程安全的。
可是,若是涉及到仓库,必须仓库有足够的产品才能进行销售,这时,多个销售人员就有了临界资源:仓库。
在这里咱们只讨论对象的普通方法。至于方法传入的参数,以及方法内对静态变量操做的,这里须要根据参数和静态变量来断定方法的线程安全性。
销售员案例:
using System; using System.Threading; namespace MutiThreadSample.Sale { /// <summary> /// 销售 /// </summary> public class Saler { /// <summary> /// 名称 /// </summary> public string Name { get; set; } /// <summary> /// 间隔时间 /// </summary> public int IntervalTime { get; set; } /// <summary> /// 单位时间销售运量 /// </summary> public int SaleAmount { get; set; } /// <summary> /// 销售 /// </summary> public void Sale() { Console.WriteLine("销售:{0} 于 {1} 销售产品 {2}", this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(IntervalTime); } /// <summary> /// 销售 /// </summary> /// <param name="interval">时间间隔</param> public void Sale(object obj) { WHouseThreadParameter parameter = obj as WHouseThreadParameter; if (parameter != null) { while (parameter.WHouse != null && parameter.WHouse.CanOut(this.SaleAmount)) { parameter.WHouse.Outgoing(this.SaleAmount); Console.WriteLine("Thread{0}, 销售:{1} 于 {2} 销售出库产品 {3}", Thread.CurrentThread.Name, this.Name, DateTime.Now.Millisecond, this.SaleAmount); Thread.Sleep(this.IntervalTime); } } } } }
3)类型
已经讲了类的实例--对象的多线程安全性问题。这里只讨论类型的静态变量和静态方法。
当静态类被访问的时候,CLR会调用类的静态构造器(类型构造器),建立静态类的类型对象,CLR但愿确保每一个应用程序域内只执行一次类型构造器,为了作到这一点,在调用类型构造器时,CLR会为静态类加一个互斥的线程同步锁,所以,若是多个线程试图同时调用某个类型的静态构造器时,那么只有一个线程能够得到对静态类的访问权,其余的线程都被阻塞。第一个线程执行完 类型构造器的代码并释放构造器以后,其余阻塞的线程被唤醒,而后发现构造器被执行过,所以,这些线程再也不执行构造器,只是从构造器简单的返回。若是再一次调用这些方法,CLR就会意识到类型构造器被执行过,从而不会在被调用。
调用类中的静态方法,或者访问类中的静态成员变量,过程同上,因此说静态类是线程安全的。
最简单的例子,就是数据库操做帮助类。这个类的方法和属性是线程安全的。
using System; namespace MutiThreadSample.Static { public class SqlHelper { /// <summary> /// 数据库链接 /// </summary> private static readonly string ConnectionString = ""; /// <summary> /// 执行数据库命令 /// </summary> /// <param name="sql">SQL语句</param> public static void ExcuteNonQuery(string sql) { //执行数据操做,好比新增、编辑、删除 } } }
可是,对于静态变量其线程安全性是相对的,若是多个线程来修改静态变量,这就不必定是线程安全的。而静态方法的线程安全性,直接跟传入的参数有关。
总之:
针对变量、对象、类型,说线程安全性,比较笼统,在这里,主要是想让你们明白,哪些地方须要注意线程安全性。对于变量、对象(属性、方法)、静态变量、静态方法,其线程安全性是相对的,须要根据实际状况而定。
万剑不离其宗,其断定标准:是否有临界资源。
经常使用的集合类型有List、Dictionary、HashTable、HashMap等。在编码中,集合应用很普遍中,经常使用集合来自定义Cache,这时候必须考虑线程同步问题。
默认状况下集合不是线程安全的。在System.Collections 命名空间中只有几个类提供Synchronize方法,该方法可以超越集合建立线程安全包装。可是,System.Collections命名空间中的全部类都提供SyncRoot属性,可供派生类建立本身的线程安全包装。还提供了IsSynchronized属性以肯定集合是不是线程安全的。可是ICollection泛型接口中不提供同步功能,非泛型接口支持这个功能。
Dictionary(MSDN解释)
此类型的公共静态(在 Visual Basic 中为 Shared)成员是线程安全的。 但不保证全部实例成员都是线程安全的。
只要不修改该集合,Dictionary<TKey, TValue> 就能够同时支持多个阅读器。 即使如此,从头至尾对一个集合进行枚举本质上并非一个线程安全的过程。 当出现枚举与写访问互相争用这种极少发生的状况时,必须在整个枚举过程当中锁定集合。 若容许多个线程对集合执行读写操做,您必须实现本身的同步。
不少集合类型都和Dictionary相似。默认状况下是线程不安全的。固然微软也提供了线程安全的Hashtable.
HashTable
Hashtable 是线程安全的,可由多个读取器线程和一个写入线程使用。 多线程使用时,若是只有一个线程执行写入(更新)操做,则它是线程安全的,从而容许进行无锁定的读取(若编写器序列化为 Hashtable)。 若要支持多个编写器,若是没有任何线程在读取 Hashtable 对象,则对 Hashtable 的全部操做都必须经过 Synchronized 方法返回的包装完成。
从头至尾对一个集合进行枚举本质上并非一个线程安全的过程。 即便某个集合已同步,其余线程仍能够修改该集合,这会致使枚举数引起异常。 若要在枚举过程当中保证线程安全,能够在整个枚举过程当中锁定集合,或者捕捉因为其余线程进行的更改而引起的异常。
线程安全起见请使用如下方法声明
/// <summary> /// Syncronized方法用来创造一个新的对象的线程安全包装 /// </summary> private Hashtable hashtable = Hashtable.Synchronized(new Hashtable());
在枚举读取时,加lock,这里lock其同步对象SyncRoot
/// <summary> /// 读取 /// </summary> public void Read() { lock(hashtable.SyncRoot) { foreach (var item in hashtable.Keys) { Console.WriteLine("Key:{0}",item); } } }
在第三章作了具体讲解,并介绍了经常使用的几种线程同步的方法,具体可见:
http://www.cnblogs.com/yank/p/3227324.html
IIS有多个应用程序池,每一个应用程序池对应一个w3wp.exe的进程,每一个应用程序池对应多个应用程序,每一个应用程序对应一个应用程序域,应用程序域中包含了共享数据和多个线程,线程中有指定操做。由下图咱们就能清晰的了解整个结构。
线程能够大大提升应用程序的可用性和性能,可是多线程也给咱们带来一些新的挑战,要不要使用多线程,如何使用多线程,须要根据实际状况而定。
1)复杂度
使用多线程,可能使得应用程序复杂度明显提升,特别是要处理线程同步和死锁问题。须要仔细地评估应该在何处使用多线程和如何使用多线程,这样就能够得到最大的好处,而无需建立没必要要的复杂并难于调试的应用程序。
2)数量
线程不易过多,线程的数量与服务器配置(多核、多处理器)、业务处理具体过程,都有直接关系。线程量过少,不能充分发挥服务器的处理能力,也不能有效改善事务的处理效率。线程量过多,须要花费大量的时间来进行线程控制,最后得不偿失。能够根据实际状况,经过检验测试,设定一个特定的合理的范围。
3)同步和异步调用之间的选择
应用程序既能够进行同步调用,也能够进行异步调用。同步 调用在继续以前等待响应或返回值。若是不容许调用继续,就说调用被阻塞 了。异步或非阻塞 调用不等待响应。异步调用是经过使用单独的线程执行的。原始线程启动异步调用,异步调用使用另外一个线程执行请求,而与此同时原始的线程继续处理。
4)前台线程和后台线程之间的选择
.NET Framework 中的全部线程都被指定为前台线程或后台线程。这两种线程惟一的区别是 — 后台线程不会阻止进程终止。在属于一个进程的全部前台线程终止以后,公共语言运行库 (CLR) 就会结束进程,从而终止仍在运行的任何后台线程。
在默认状况下,经过建立并启动新的 Thread 对象生成的全部线程都是前台线程,而从非托管代码进入托管执行环境中的全部线程都标记为后台线程。然而,经过修改 Thread.IsBackground 属性,能够指定一个线程是前台线程仍是后台线程。经过将 Thread.IsBackground 设置为 true,能够将一个线程指定为后台线程;经过将 Thread.IsBackground 设置为 false,能够将一个线程指定为前台线程。
在大多数应用程序中,您会选择将不一样的线程设置成前台线程或后台线程。一般,应该将被动侦听活动的线程设置为后台线程,而将负责发送数据的线程设置为前台线程,这样,在全部的数据发送完毕以前该线程不会被终止。只有在确认线程被系统随意终止没有不利影响时,才应该使用后台线程。若是线程正在执行必须完成的敏感操做或事务操做,或者须要控制关闭线程的方式以便释放重要资源,则使用前台线程。
到如今为止,您可能会认识到许多应用程序都会从多线程处理中受益。然而,线程管理并不只仅是每次想要执行一个不一样的任务就建立一个新线程的问题。有太多的线程可能会使得应用程序耗费一些没必要要的系统资源,特别是,若是有大量短时间运行的操做,而全部这些操做都运行在单独线程上。另外,显式地管理大量的线程多是很是复杂的。
线程池化技术经过给应用程序提供由系统管理的辅助线程池解决了这些问题,从而使得您能够将注意力集中在应用程序任务上而不是线程管理上。
在须要时,能够由应用程序将线程添加到线程池中。当 CLR 最初启动时,线程池没有包含额外的线程。然而,当应用程序请求线程时,它们就会被动态建立并存储在该池中。若是线程在一段时间内没有使用,这些线程就可能会被处置,所以线程池是根据应用程序的要求缩小或扩大的。
注意:每一个进程都建立一个线程池,所以,若是您在同一个进程内运行几个应用程序域,则一个应用程序域中的错误可能会影响相同进程内的其余应用程序域,由于它们都使用相同的线程池。
线程池由两种类型的线程组成:
辅助线程。辅助线程是标准系统池的一部分。它们是由 .NET Framework 管理的标准线程,大多数功能都在它们上面执行。
完成端口线程.这种线程用于异步 I/O 操做(经过使用 IOCompletionPorts API)
对于每一个计算机处理器,线程池都默认包含 25 个线程。若是全部的 25 个线程都在被使用,则附加的请求将排入队列,直到有一个线程变得可用为止。每一个线程都使用默认堆栈大小,并按默认的优先级运行。
下面代码示例说明了线程池的使用。
private void ThreadPoolExample() { WaitCallback callback = new WaitCallback( ThreadProc ); ThreadPool.QueueUserWorkItem( callback ); }
在前面的代码中,首先建立一个委托来引用您想要在辅助线程中执行的代码。.NET Framework 定义了 WaitCallback 委托,该委托引用的方法接受一个对象参数而且没有返回值。下面的方法实现您想要执行的代码。
private void ThreadProc( Object stateInfo ) { // Do something on worker thread. }
能够将单个对象参数传递给 ThreadProc 方法,方法是将其指定为 QueueUserWorkItem 方法调用中的第二个参数。在前面的示例中,没有给 ThreadProc 方法传递参数,所以 stateInfo 参数为空。
在下面的状况下,使用 ThreadPool 类:
有大量小的独立任务要在后台执行。
不须要对用来执行任务的线程进行精细控制。
Thread是显示来管理线程。只要有可能,就应该使用 ThreadPool 类来建立线程。
在下面的状况下,使用 Thread 对象:
须要具备特定优先级的任务。
有可能运行很长时间的任务(这样可能阻塞其余任务)。
须要确保只有一个线程能够访问特定的程序集。
须要有与线程相关的稳定标识。