首页 > 代码库 > C#多线程和线程池
C#多线程和线程池
1、概念
1.0 线程的和进程的关系以及优缺点
windows系统是一个多线程的操作系统。一个程序至少有一个进程,一个进程至少有一个线程。进程是线程的容器,一个C#客户端程序开始于一个单独的线程,CLR(公共语言运行库)为该进程创建了一个线程,该线程称为主线程。例如当我们创建一个C#控制台程序,程序的入口是Main()函数,Main()函数是始于一个主线程的。它的功能主要 是产生新的线程和执行程序。C#是一门支持多线程的编程语言,通过Thread类创建子线程,引入using System.Threading命名空间。
多线程的优点:
1 2 | 1、 多线程可以提高CPU的利用率,因为当一个线程处于等待状态的时候,CPU会去执行另外的线程 2、 提高了CPU的利用率,就可以直接提高程序的整体执行速度 |
多线程的缺点:
1 2 3 | 1、线程开的越多,内存占用越大 2、协调和管理代码的难度加大,需要CPU时间跟踪线程 3、线程之间对资源的共享可能会产生可不遇知的问题 |
1.1 前台线程和后台线程
C#中的线程分为前台线程和后台线程,线程创建时不做设置默认是前台线程。即线程属性IsBackground=false。
Thread.IsBackground = false;//false:设置为前台线程,系统默认为前台线程。
区别以及如何使用:
这两者的区别就是:应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。一般后台线程用于处理时间较短的任务,如在一个Web服务器中可以利用后台线程来处理客户端发过来的请求信息。而前台线程一般用于处理需要长时间等待的任务,如在Web服务器中的监听客户端请求的程序。
线程是寄托在进程上的,进程都结束了,线程也就不复存在了!
只要有一个前台线程未退出,进程就不会终止!即说的就是程序不会关闭!(即在资源管理器中可以看到进程未结束。)
1.3 多线程的创建
下面的代码创建了一个子线程,作为程序的入口mian()函数所在的线程即为主线程,我们通过Thread类来创建子线程,Thread类有 ThreadStart 和 ParameterizedThreadStart类型的委托参数,我们也可以直接写方法的名字。线程执行的方法可以传递参数(可选),参数的类型为object,写在Start()里。
class Program { //我们的控制台程序入口是main函数。它所在的线程即是主线程 static void Main(string[] args) { Thread thread = new Thread(ThreadMethod); //执行的必须是无返回值的方法 thread.Name = "子线程"; //thread.Start("王建"); //在此方法内传递参数,类型为object,发送和接收涉及到拆装箱操作 thread.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) //方法内可以有参数,也可以没有参数 { Console.WriteLine("{0}开始执行。", Thread.CurrentThread.Name); } }
首先使用new Thread()创建出新的线程,然后调用Start方法使得线程进入就绪状态,得到系统资源后就执行,在执行过程中可能有等待、休眠、死亡和阻塞四种状态。正常执行结束时间片后返回到就绪状态。如果调用Suspend方法会进入等待状态,调用Sleep或者遇到进程同步使用的锁机制而休眠等待。具体过程如下图所示:
2、线程的基本操作
线程和其它常见的类一样,有着很多属性和方法,参考下表:
2.1 线程的相关属性
我们可以通过上面表中的属性获取线程的一些相关信息,下面是代码展示和输出结果:
static void Main(string[] args) { Thread thread = new Thread(ThreadMethod); //执行的必须是无返回值的方法 thread.Name = "子线程"; thread.Start(); StringBuilder threadInfo = new StringBuilder(); threadInfo.Append(" 线程当前的执行状态: " + thread.IsAlive); threadInfo.Append("\n 线程当前的名字: " + thread.Name); threadInfo.Append("\n 线程当前的优先级: " + thread.Priority); threadInfo.Append("\n 线程当前的状态: " + thread.ThreadState); Console.Write(threadInfo); Console.ReadKey(); } public static void ThreadMethod(object parameter) { Console.WriteLine("{0}开始执行。", Thread.CurrentThread.Name); }
输输出结果:
2.2 线程的相关操作
2.2.1 Abort()方法
Abort()方法用来终止线程,调用此方法强制停止正在执行的线程,它会抛出一个ThreadAbortException异常从而导致目标线程的终止。下面代码演示:
static void Main(string[] args) { Thread thread = new Thread(ThreadMethod); //执行的必须是无返回值的方法 thread.Name = "小A"; thread.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) { Console.WriteLine("我是:{0},我要终止了", Thread.CurrentThread.Name); //开始终止线程 Thread.CurrentThread.Abort(); //下面的代码不会执行 for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name,i); } }
执行结果:和我们想象的一样,下面的循环没有被执行
2.2.2 ResetAbort()方法
Abort方法可以通过跑出ThreadAbortException异常中止线程,而使用ResetAbort方法可以取消中止线程的操作,下面通过代码演示使用 ResetAbort方法。
static void Main(string[] args) { Thread thread = new Thread(ThreadMethod); //执行的必须是无返回值的方法 thread.Name = "小A"; thread.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) { try { Console.WriteLine("我是:{0},我要终止了", Thread.CurrentThread.Name);
//开始终止线程 Thread.CurrentThread.Abort(); } catch(ThreadAbortException ex) { Console.WriteLine("我是:{0},我又恢复了", Thread.CurrentThread.Name);
//恢复被终止的线程 Thread.ResetAbort(); } for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name,i); } }
执行结果:
2.2.3 Sleep()方法
Sleep()方法调已阻塞线程,是当前线程进入休眠状态,在休眠过程中占用系统内存但是不占用系统时间,当休眠期过后,继续执行,声明如下:
public static void Sleep(TimeSpan timeout); //时间段 public static void Sleep(int millisecondsTimeout); //毫秒数
实例代码:
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "小A"; threadA.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name,i); Thread.Sleep(300); //休眠300毫秒 } }
将上面的代码执行以后,可以清楚的看到每次循环之间相差300毫秒的时间。
2.2.4 join()方法
Join方法主要是用来阻塞调用线程,直到某个线程终止或经过了指定时间为止。官方的解释比较乏味,通俗的说就是创建一个子线程,给它加了这个方法,其它线程就会暂停执行,直到这个线程执行完为止才去执行(包括主线程)。她的方法声明如下:
public void Join(); public bool Join(int millisecondsTimeout); //毫秒数 public bool Join(TimeSpan timeout); //时间段
为了验证上面所说的,我们首先看一段代码:
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "小A"; Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "小B"; threadA.Start();
//threadA.Join(); threadB.Start();
//threadB.Join(); for (int i = 0; i < 10; i++) { Console.WriteLine("我是:主线程,我循环{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); //休眠300毫秒 } Console.ReadKey(); } public static void ThreadMethod(object parameter) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name,i); Thread.Sleep(300); //休眠300毫秒 } }
因为线程之间的执行是随机的,所有执行结果和我们想象的一样,杂乱无章!但是说明他们是同时执行的。
现在我们把代码中的 ThreadA.join()方法注释取消,首先程序中有三个线程,ThreadA、ThreadB和主线程,首先主线程先阻塞,然后线程ThreadB阻塞,ThreadA先执行,执行完毕以后ThreadB接着执行,最后才是主线程执行。
看执行结果:
<iframe id="iframe_0.24664590856991708" style="border: none; width: 385px; height: 482px;" src="data:text/html;charset=utf8,%3Cstyle%3Ebody%7Bmargin:0;padding:0%7D%3C/style%3E%3Cimg%20id=%22img%22%20src=%22http://oa.sarnasea.com/Content/ueditor/net/upload/image/20161019/6361249762004155227979269.png?_=5976096%22%20style=%22border:none;max-width:810px%22%3E%3Cscript%3Ewindow.onload%20=%20function%20()%20%7Bvar%20img%20=%20document.getElementById(‘img‘);%20window.parent.postMessage(%7BiframeId:‘iframe_0.24664590856991708‘,width:img.width,height:img.height%7D,%20‘http://www.cnblogs.com‘);%7D%3C/script%3E" frameborder="0" scrolling="no" width="320" height="240"></iframe>
2.2.5 Suspent()和Resume()方法
其实在C# 2.0以后, Suspent()和Resume()方法已经过时了。suspend()方法容易发生死锁。调用suspend()的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被”挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成死锁。所以不应该使用suspend()。
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "小A"; threadA.Start(); Thread.Sleep(3000); //休眠3000毫秒 threadA.Resume(); //继续执行已经挂起的线程 Console.ReadKey(); } public static void ThreadMethod(object parameter) { Thread.CurrentThread.Suspend(); //挂起当前线程 for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); } }
执行上面的代码。窗口并没有马上执行 ThreadMethod方法输出循环数字,而是等待了三秒钟之后才输出,因为线程开始执行的时候执行了Suspend()方法挂起。然后主线程休眠了3秒钟以后又通过Resume()方法恢复了线程threadA。
2.2.6 线程的优先级
如果在应用程序中有多个线程在运行,但一些线程比另一些线程重要,这种情况下可以在一个进程中为不同的线程指定不同的优先级。线程的优先级可以通过Thread类Priority属性设置,Priority属性是一个ThreadPriority型枚举,列举了5个优先等级:AboveNormal、BelowNormal、Highest、Lowest、Normal。公共语言运行库默认是Normal类型的。见下图:
直接上代码来看效果:
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "A"; Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "B"; threadA.Priority = ThreadPriority.Highest; threadB.Priority = ThreadPriority.BelowNormal; threadB.Start(); threadA.Start(); Thread.CurrentThread.Name = "C"; ThreadMethod(new object()); Console.ReadKey(); } public static void ThreadMethod(object parameter) { for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } }
执行结果:
上面的代码中有三个线程,threadA,threadB和主线程,threadA优先级最高,threadB优先级最低。这一点从运行结果中也可以看出,线程B 偶尔会出现在主线程和线程A前面。当有多个线程同时处于可执行状态,系统优先执行优先级较高的线程,但这只意味着优先级较高的线程占有更多的CPU时间,并不意味着一定要先执行完优先级较高的线程,才会执行优先级较低的线程。
优先级越高表示CPU分配给该线程的时间片越多,执行时间就多
优先级越低表示CPU分配给该线程的时间片越少,执行时间就少
3、线程同步
什么是线程安全:
线程安全是指在当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
线程有可能和其他线程共享一些资源,比如,内存,文件,数据库等。当多个线程同时读写同一份共享资源的时候,可能会引起冲突。这时候,我们需要引入线程“同步”机制,即各位线程之间要有个先来后到,不能一窝蜂挤上去抢作一团。线程同步的真实意思和字面意思恰好相反。线程同步的真实意思,其实是“排队”:几个线程之间要排队,一个一个对共享资源进行操作,而不是同时进行操作。
为什么要实现同步呢,下面的例子我们拿著名的单例模式来说吧。看代码
public class Singleton { private static Singleton instance; private Singleton() //私有函数,防止实例 { } public static Singleton GetInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
单例模式就是保证在整个应用程序的生命周期中,在任何时刻,被指定的类只有一个实例,并为客户程序提供一个获取该实例的全局访问点。但上面代码有一个明显的问题,那就是假如两个线程同时去获取这个对象实例,那。。。。。。。。
我们队代码进行修改:
public class Singleton{ private static Singleton instance; private static object obj=new object(); private Singleton() //私有化构造函数 { } public static Singleton GetInstance() { if(instance==null) { lock(obj) //通过Lock关键字实现同步 { if(instance==null) { instance=new Singleton(); } } } return instance; }}
经过修改后的代码。加了一个 lock(obj)代码块。这样就能够实现同步了,假如不是很明白的话,咱们看后面继续讲解~
3.0 使用Lock关键字实现线程同步
首先创建两个线程,两个线程执行同一个方法,参考下面的代码:
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "王文建"; Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "生旭鹏"; threadA.Start(); threadB.Start(); Console.ReadKey(); } public static void ThreadMethod(object parameter) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); } }
执行结果:
通过上面的执行结果,可以很清楚的看到,两个线程是在同时执行ThreadMethod这个方法,这显然不符合我们线程同步的要求。我们对代码进行修改如下:
static void Main(string[] args) { Program pro = new Program(); Thread threadA = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "王文建"; Thread threadB = new Thread(pro.ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "生旭鹏"; threadA.Start(); threadB.Start(); Console.ReadKey(); } public void ThreadMethod(object parameter) { lock (this) //添加lock关键字 { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); } } }
执行结果:
我们通过添加了 lock(this) {...}代码,查看执行结果实现了我们想要的线程同步需求。但是我们知道this表示当前类实例的本身,那么有这么一种情况,我们把需要访问的方法所在的类型进行两个实例A和B,线程A访问实例A的方法ThreadMethod,线程B访问实例B的方法ThreadMethod,这样的话还能够达到线程同步的需求吗。
static void Main(string[] args) { Program pro1 = new Program(); Program pro2 = new Program(); Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "王文建"; Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "生旭鹏"; threadA.Start(); threadB.Start(); Console.ReadKey(); } public void ThreadMethod(object parameter) { lock (this) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); } } }
执行结果:
我们会发现,线程又没有实现同步了!lock(this)对于这种情况是不行的!所以需要我们对代码进行修改!修改后的代码如下:
private static object obj = new object(); static void Main(string[] args) { Program pro1 = new Program(); Program pro2 = new Program(); Thread threadA = new Thread(pro1.ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "王文建"; Thread threadB = new Thread(pro2.ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "生旭鹏"; threadA.Start(); threadB.Start(); Console.ReadKey(); } public void ThreadMethod(object parameter) { lock (obj) { for (int i = 0; i < 10; i++) { Console.WriteLine("我是:{0},我循环{1}次", Thread.CurrentThread.Name, i); Thread.Sleep(300); } } }
通过查看执行结果。会发现代码实现了我们的需求。那么 lock(this) 和lock(Obj)有什么区别呢?
lock(this) 锁定 当前实例对象,如果有多个类实例的话,lock锁定的只是当前类实例,对其它类实例无影响。所有不推荐使用。 lock(typeof(Model))锁定的是model类的所有实例。 lock(obj)锁定的对象是全局的私有化静态变量。外部无法对该变量进行访问。 lock 确保当一个线程位于代码的临界区时,另一个线程不进入临界区。如果其他线程试图进入锁定的代码,则它将一直等待(即被阻止),直到该对象被释放。 所以,lock的结果好不好,还是关键看锁的谁,如果外边能对这个谁进行修改,lock就失去了作用。所以一般情况下,使用私有的、静态的并且是只读的对象。
总结:
1、lock的是必须是引用类型的对象,string类型除外。
2、lock推荐的做法是使用静态的、只读的、私有的对象。
3、保证lock的对象在外部无法修改才有意义,如果lock的对象在外部改变了,对其他线程就会畅通无阻,失去了lock的意义。
不能锁定字符串,锁定字符串尤其危险,因为字符串被公共语言运行库 (CLR)“暂留”。 这意味着整个程序中任何给定字符串都只有一个实例,就是这同一个对象表示了所有运行的应用程序域的所有线程中的该文本。因此,只要在应用程序进程中的任何位置处具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。通常,最好避免锁定 public 类型或锁定不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则 lock(this) 可能会有问题,因为不受控制的代码也可能会锁定该对象。这可能导致死锁,即两个或更多个线程等待释放同一对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题。而且lock(this)只对当前对象有效,如果多个对象之间就达不到同步的效果。lock(typeof(Class))与锁定字符串一样,范围太广了。
3.1 使用Monitor类实现线程同步
Lock关键字是Monitor的一种替换用法,lock在IL代码中会被翻译成Monitor.
lock(obj)
{
//代码段
}
就等同于
Monitor.Enter(obj);
//代码段
Monitor.Exit(obj);
Monitor的常用属性和方法:
Enter(Object) 在指定对象上获取排他锁。
Exit(Object) 释放指定对象上的排他锁。
Pulse 通知等待队列中的线程锁定对象状态的更改。
PulseAll 通知所有的等待线程对象状态的更改。
TryEnter(Object) 试图获取指定对象的排他锁。
TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
常用的方法有两个,Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
Enter(Object)的用法很简单,看代码
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "A"; Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "B"; threadA.Start(); threadB.Start(); Thread.CurrentThread.Name = "C"; ThreadMethod(); Console.ReadKey(); } static object obj = new object(); public static void ThreadMethod() { Monitor.Enter(obj); //Monitor.Enter(obj) 锁定对象 try { for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } } catch(Exception ex){ } finally { Monitor.Exit(obj); //释放对象 } }
TryEnter(Object)和TryEnter() 方法在尝试获取一个对象上的显式锁方面和 Enter() 方法类似。然而,它不像Enter()方法那样会阻塞执行。如果线程成功进入关键区域那么TryEnter()方法会返回true. 和试图获取指定对象的排他锁。看下面代码演示:
我们可以通过Monitor.TryEnter(monster, 1000),该方法也能够避免死锁的发生,我们下面的例子用到的是该方法的重载,Monitor.TryEnter(Object,Int32),。
static void Main(string[] args) { Thread threadA = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadA.Name = "A"; Thread threadB = new Thread(ThreadMethod); //执行的必须是无返回值的方法 threadB.Name = "B"; threadA.Start(); threadB.Start(); Thread.CurrentThread.Name = "C"; ThreadMethod(); Console.ReadKey(); } static object obj = new object(); public static void ThreadMethod() { bool flag = Monitor.TryEnter(obj, 1000); //设置1S的超时时间,如果在1S之内没有获得同步锁,则返回false
//上面的代码设置了锁定超时时间为1秒,也就是说,在1秒中后,
//lockObj还未被解锁,TryEntry方法就会返回false,如果在1秒之内,lockObj被解锁,TryEntry返回true。我们可以使用这种方法来避免死锁 try { if (flag) { for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } } } catch(Exception ex) { } finally { if (flag) Monitor.Exit(obj); } }
Monitor.Wait和Monitor()Pause()
Wait(object)方法:释放对象上的锁并阻止当前线程,直到它重新获取该锁,该线程进入等待队列。
Pulse方法:只有锁的当前所有者可以使用 Pulse 向等待对象发出信号,当前拥有指定对象上的锁的线程调用此方法以便向队列中的下一个线程发出锁的信号。接收到脉冲后,等待线程就被移动到就绪队列中。在调用 Pulse 的线程释放锁后,就绪队列中的下一个线程(不一定是接收到脉冲的线程)将获得该锁。
另外:
Wait 和 Pulse 方法必须写在 Monitor.Enter 和Moniter.Exit 之间。
上面是MSDN的解释。不明白看代码:
首先我们定义一个攻击类,
/// <summary> /// 怪物类 /// </summary> internal class Monster { public int Blood { get; set; } public Monster(int blood) { this.Blood = blood; Console.WriteLine("我是怪物,我有{0}滴血",blood); } }
然后在定义一个攻击类
/// <summary> /// 攻击类 /// </summary> internal class Play { /// <summary> /// 攻击者名字 /// </summary> public string Name { get; set; } /// <summary> /// 攻击力 /// </summary> public int Power{ get; set; } /// <summary> /// 法术攻击 /// </summary> public void magicExecute(object monster) { Monster m = monster as Monster; Monitor.Enter(monster); while (m.Blood>0) { Monitor.Wait(monster); Console.WriteLine("当前英雄:{0},正在使用法术攻击打击怪物", this.Name); if(m.Blood>= Power) { m.Blood -= Power; } else { m.Blood = 0; } Thread.Sleep(300); Console.WriteLine("怪物的血量还剩下{0}", m.Blood); Monitor.PulseAll(monster); } Monitor.Exit(monster); } /// <summary> /// 物理攻击 /// </summary> /// <param name="monster"></param> public void physicsExecute(object monster) { Monster m = monster as Monster; Monitor.Enter(monster); while (m.Blood > 0) { Monitor.PulseAll(monster); if (Monitor.Wait(monster, 1000)) //非常关键的一句代码 { Console.WriteLine("当前英雄:{0},正在使用物理攻击打击怪物", this.Name); if (m.Blood >= Power) { m.Blood -= Power; } else { m.Blood = 0; } Thread.Sleep(300); Console.WriteLine("怪物的血量还剩下{0}", m.Blood); } } Monitor.Exit(monster); } }
执行代码:
static void Main(string[] args) { //怪物类 Monster monster = new Monster(1000); //物理攻击类 Play play1 = new Play() { Name = "无敌剑圣", Power = 100 }; //魔法攻击类 Play play2 = new Play() { Name = "流浪法师", Power = 120 }; Thread thread_first = new Thread(play1.physicsExecute); //物理攻击线程 Thread thread_second = new Thread(play2.magicExecute); //魔法攻击线程 thread_first.Start(monster); thread_second.Start(monster); Console.ReadKey(); }
输出结果:
总结:
第一种情况:
- thread_first首先获得同步对象的锁,当执行到 Monitor.Wait(monster);时,thread_first线程释放自己对同步对象的锁,流放自己到等待队列,直到自己再次获得锁,否则一直阻塞。
- 而thread_second线程一开始就竞争同步锁所以处于就绪队列中,这时候thread_second直接从就绪队列出来获得了monster对象锁,开始执行到Monitor.PulseAll(monster)时,发送了个Pulse信号。
- 这时候thread_first接收到信号进入到就绪状态。然后thread_second继续往下执行到 Monitor.Wait(monster, 1000)时,这是一句非常关键的代码,thread_second将自己流放到等待队列并释放自身对同步锁的独占,该等待设置了1S的超时值,当B线程在1S之内没有再次获取到锁自动添加到就绪队列。
- 这时thread_first从Monitor.Wait(monster)的阻塞结束,返回true。开始执行、打印。执行下一行的Monitor.Pulse(monster),这时候thread_second假如1S的时间还没过,thread_second接收到信号,于是将自己添加到就绪队列。
- thread_first的同步代码块结束以后,thread_second再次获得执行权, Monitor.Wait(m_smplQueue, 1000)返回true,于是继续从该代码处往下执行、打印。当再次执行到Monitor.Wait(monster, 1000),又开始了步骤3。
- 依次循环。。。。
第二种情况:thread_second首先获得同步锁对象,首先执行到Monitor.PulseAll(monster),因为程序中没有需要等待信号进入就绪状态的线程,所以这一句代码没有意义,当执行到 Monitor.Wait(monster, 1000),自动将自己流放到等待队列并在这里阻塞,1S 时间过后thread_second自动添加到就绪队列,线程thread_first获得monster对象锁,执行到Monitor.Wait(monster);时发生阻塞释放同步对象锁,线程thread_second执行,执行Monitor.PulseAll(monster)时通知thread_first。于是又开始第一种情况...
Monitor.Wait是让当前进程睡眠在临界资源上并释放独占锁,它只是等待,并不退出,当等待结束,就要继续执行剩下的代码。
3.0 使用Mutex类实现线程同步
Mutex的突出特点是可以跨应用程序域边界对资源进行独占访问,即可以用于同步不同进程中的线程,这种功能当然这是以牺牲更多的系统资源为代价的。
主要常用的两个方法:
public virtual bool WaitOne() 阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号获取互斥锁。
public void ReleaseMutex() 释放 System.Threading.Mutex 一次。
使用实例:
static void Main(string[] args) { Thread[] thread = new Thread[3]; for (int i = 0; i < 3; i++) { thread[i] = new Thread(ThreadMethod1); thread[i].Name = i.ToString(); } for (int i = 0; i < 3; i++) { thread[i].Start(); } Console.ReadKey(); } public static void ThreadMethod1(object val) { mutet.WaitOne(); //获取锁 for (int i = 0; i < 500; i++) { Console.Write(Thread.CurrentThread.Name); } mutet.ReleaseMutex(); //释放锁 }
2、线程池
上面介绍了介绍了平时用到的大多数的多线程的例子,但在实际开发中使用的线程往往是大量的和更为复杂的,这时,每次都创建线程、启动线程。从性能上来讲,这样做并不理想(因为每使用一个线程就要创建一个,需要占用系统开销);从操作上来讲,每次都要启动,比较麻烦。为此引入的线程池的概念。
好处:
1.减少在创建和销毁线程上所花的时间以及系统资源的开销
2.如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。
在什么情况下使用线程池?
1.单个任务处理的时间比较短
2.需要处理的任务的数量大
线程池最多管理线程数量=“处理器数 * 250”。也就是说,如果您的机器为2个2核CPU,那么CLR线程池的容量默认上限便是1000
通过线程池创建的线程默认为后台线程,优先级默认为Normal。
代码示例:
static void Main(string[] args) { ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadMethod1), new object()); //参数可选 Console.ReadKey(); } public static void ThreadMethod1(object val) { for (int i = 0; i <= 500000000; i++) { if (i % 1000000 == 0) { Console.Write(Thread.CurrentThread.Name); } } }
有关线程池的解释请参考:
http://www.cnblogs.com/JeffreyZhao/archive/2009/07/22/thread-pool-1-the-goal-and-the-clr-thread-pool.html
======
C#多线程和线程池