引言:
最近一段时间都在研究关于.Net线程的内容,觉得线程是每个程序员都应该掌握的,所以写下这个线程的系列希望能给大家学习过程中一些帮助,同时也是自己对线程的巩固,当中如果有什么错漏还请大家指出,这样我们可以互相得到进步。
目录:
一、线程的介绍
二、线程调度和优先级
三、前台线程和后台线程
四、简单线程的使用
一、线程的介绍
在介绍线程之前, 很有必要知道什么是进程,以及与线程的关系。
进程(Process)是应用程序的实例要使用的资源的一个集合(从可以简化理解:进程就是一种资源,是应用程序所用的资源)。每个应用程序都在各自的进程中运行来确保应用程序不受其他应用程序的影响,如果一个应用程序失败了, 只会影响自己的进程,其他进程中的应用程序可以继续运行。进程是操作系统为我们提供的一种保护应用程序的一种机制。
线程是进程中基本执行单元, 一个进程中可以包含多个线程,在进程入口执行的第一个线程是一个进程的主线程,在.Net应用程序中,都是以Main()方法作为程序的入口的, 所以在程序运行过程中调用这个方法时,系统就会自动创建一个主线程。(他们之间的关系简单说:线程是进程的执行单元,进程是线程的一个容器了)。
二、线程调度和优先级
Windows之所以被称为抢占式多线程操作系统,是因为线程可以在任意时间被抢占,并调度另一个线程。每个线程都分配了从0~31的一个优先级。系统首先把高优先级的线程分配给CPU执行。Windows 支持7个相对线程优先级:Idle,Lowest,Below Normal,Normal,Above Normal,Highest和Time-Critical,Normal是默认的线程优先级,然而在程序中可以通过设置Thread的Priority属性来改变线程的优先级,它的类型为ThreadPriority枚举类型,包含枚举有:Lowest,BelowNormal,Normal,AboveNormal和Highest,CLR为自己保留了 Idle和Time-Critical优先级。具体每个枚举值含义如下表:
| 成员名称 | 说明 |
|---|---|
| Lowest | 可以将 Thread何其他优先级的线程之后。 |
| BelowNormal | 可以将 Thread Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前。 |
| Normal |
可以将 Thread AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前。 默认情况下,线程具有 Normal 优先级。 |
| AboveNormal | 可以将 Thread Highest 优先级的线程之后,在具有 Normal 优先级的线程之前。 |
| Highest | 可以将 Thread 其他优先级的线程之前。 |
三、前台线程和后台线程
在.net中线程分为前台线程和后台线程,在一个进程中,当所有前台线程停止运行时,CLR会强制结束仍在运行的任何后台线程,这些后台线程直接被终止,不会抛出异常。
所以我们应该在前台线程中执行我们确实要完成的事情,另外, 应该把非关键的任务使用后台线程,我们用Thread创建的是线程为前台线程。让我们通过下面的一段代码来看看前台线程和后台线成的区别:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(Worker); // 使线程成为一个后台线程 backthread.IsBackground = true; // 通过Start方法启动线程 backthread.Start(); // 如果backthread是前台线程,则应用程序大约5秒后才终止 // 如果backthread是后台线程,则应用程序立即终止 Console.WriteLine("Return from Main Thread"); } private static void Worker() { // 模拟做10秒 Thread.Sleep(5000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine("Return from Worker Thread"); } }
运行上面代码可以发现:控制台中显示字符串: Return form Main Thread 后就退出了, 字符串 Return from Worker Thread字符串根本就没有显示,这是因为此时的backthread线程为后台线程,当主线程(执行Main方法的线程,主线程当然也是前台线程了)结束运行后,CLR会强制终止后台线程的运行,整个进程就被销毁了,并不会等待后台线程运行完后才销毁。如果把 backthread.IsBackground = true; 注释掉后, 就可以看到控制台过5秒后就输出 Return from Worker Thread。再在Worker方法最后加一句 代码:Console.Read(); 就可以看到这样的结果了:

注意:有些人可能会问我不想把 backthread.IsBackground = true;注释掉, 又想把Worker()方法中的字符串输出在控制太上怎么做呢? 其实是有解决的办法的, 我们可以调用thread.Join()方法来实现,Join()方法能保证主线程(前台线程)在异步线程thread(后台线程)运行结束后才会运行。
实现代码如下:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(Worker); // 使线程成为一个后台线程 backthread.IsBackground = true; // 通过Start方法启动线程 backthread.Start(); backthread.Join(); // 模拟主线程的输出 Thread.Sleep(2000); Console.WriteLine("Return from Main Thread"); Console.Read(); } private static void Worker() { // 模拟做3秒 Thread.Sleep(3000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine("Return from Worker Thread"); } }
运行结果(与上面得到的结果一样):

四、简单线程的使用
其实在上面介绍前台线程和后台线程的时候已经通过ThreadStart委托创建了一个线程了,此时已经实现了一个多线程的一个过程,为此系列中将多线程也是做一个铺垫吧。下面通过ParameterizedThreadStart委托的方式来实现多线程。
以ParameterizedThreadStart委托的方式来实现多线程:
using System; using System.Threading; class Program { static void Main(string[] args) { // 创建一个新线程(默认为前台线程) Thread backthread = new Thread(new ParameterizedThreadStart(Worker)); // 通过Start方法启动线程 backthread.Start("123"); // 如果backthread是前台线程,则应用程序大约5秒后才终止 // 如果backthread是后台线程,则应用程序立即终止 Console.WriteLine("Return from Main Thread"); } private static void Worker(object data) { // 模拟做5秒 Thread.Sleep(5000); // 下面语句,只有由一个前台线程执行时,才会显示出来 Console.WriteLine(data + " Return from Worker Thread"); Console.Read(); } }
注意:此时Worker方法传入了一个参数,并且Start方法也传递了一个字符传参数。 对比与之前创建Thread的不同,
运行结果为:

写到这里, 本系列的第一篇差不多讲完了,在后续的文章将会介绍Thread方法的使用以及通过一些例子来展示他们的不同之处(像Abort()方法Interrupt方法等)对于线程的一些高级使用(如线程池,并行编程和PLINQ、线程同步和计时器)都会在后续中讲到。希望本系列可以给初学线程的人有所帮助。
Windows】线程漫谈——.NET线程同步之Monitor和lock
摘要: 本系列意在记录Windwos线程的相关知识点,包括线程基础、线程调度、线程同步、TLS、线程池等。
从这篇开始,在线程同步的方法上,开始在.NET平台上做个总结,同时对比Windows原生的API方法。你可以发现其中的联系。
.NET中的Monitor和lock
相信很多看官早已对此十分熟悉了。本文作为总结性的文章,有一些篇幅将对比Monitor和关键段的关系。由于lock就是Monitor,所以先从Monitor说起,通常Monitor是像下面这样使用的:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Monitor.Entry(lockObj);try{ // lockObj的同步区}catch(Exception e){ // 异常处理代码}finally{ Monitor.Exit(lockObj); // 解除锁定} |
当某个线程在Monitor.Entry返回后就获得了对其中lockObj的访问权限,其他试图获取lockObj的线程将被阻塞,直到线程调用Monitor.Exit释放lockObj的所有权。这意味着下面三点:
- 如果lockObj是空闲的,那么第一个调用Entry的线程将立即获得lockObj;
- 如果调用Entry的线程已经获准访问lockObj,那么不会阻塞;
- 如果调用Entry时lockObj已被其他线程锁定,则线程等待直到lockObj解锁;
事实上其中的第二点是个重要的特征,这种情况将发生在递归的情况下。Monitor应该会记录线程获准访问lockObj的次数,以正确的对锁定次数进行递减。
我花了一些时间研究Monitor到底对应底层是什么实现方式,但是我并没有找到证据证明Monitor和关键段有什么必然联系。但是从表象上看,Monitor的API方式和关键段如此相似,而且上述的三个特点也几乎完全一致,况且MSDN也把Monitor表述成Critical Section,因此,暂且认为Monitor就是关键段的包装吧!
在我之前的文章【Windows】线程漫谈——线程同步之关键段中详细介绍了Windows API关键段,下面列出这两种API的对比:
| .NET Monitor API | Windows API |
| Monitor.Entry(lockObj) | EnterCriticalSection(&cs) |
| Monitor.Exit(lockObj) | LeaveCriticalSection(&cs) |
| Monitor.TryEntry(lockObj) | TryEnterCriticalSection(&cs) |
| -- | InitializeCriticalSection(&cs); |
| -- | DeleteCriticalSection(&cs); |
| -- | InitializeCriticalSectionAndSpinCount |
| -- | SetCriticalSectionSpinCount |
| Monitor.Pulse | -- |
| Monitor.Wait | -- |
可以看到Monitor简化了关键段的使用,而且还提供了额外的Wait和Pulse方法(因为不常用,因此这里不展开了)。但是如果Monitor真的就是关键段实现的话,Monitor却不能让我们设置旋转锁的尝试次数,这是一个缺陷。
关于Wait和Pulse顺便提一下,我个人认为是条件变量的一个替代方案。关于条件变量详见【Windows】线程漫谈——线程同步之Slim读/写锁。
最后再次强调,这里的对比只是本人一厢情愿,未必说Monitor真的就是关键段!
针对Monitor锁定的lockObj有如下问题需要注意:
- lockObj不能是值类型,因为这里会被装箱,而每次装箱的引用不同,因此C#在编译阶段就保证了这种限制
- lockObj最好不要是public对象,因为可能会导致死锁,比如下面这个极端的情况:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
public class Foo{ public void Bar() { lock (this) { Console.WriteLine("Class:Foo:Method:Bar"); } }}public class MyClient{ public void Test() { Foo f = new Foo(); lock (f) //获准了f对象 { ThreadStart ts = new ThreadStart(f.Bar); Thread t = new Thread(ts); t.Start(); //新线程执行Bar方法需要获得f的访问权限,但是已被当前线程锁定,新线程将阻塞 t.Join(); //新线程将无法返回,死锁 } }} |
- lockObj最好不要是字符串,由于字符串驻留的原因,可能导致死锁:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
public class Foo { public void Bar() { lock ("Const")//Const将驻留 { Console.WriteLine("Class:Foo:Method:Bar"); } } } public class MyClient { private string lockObj = "Const"; public void Test() { Foo f = new Foo(); lock (lockObj) //由于lockObj是"Const","Const"被驻留,所以实际上lock是同一个对象 { ThreadStart ts = new ThreadStart(f.Bar); Thread t = new Thread(ts); t.Start(); //新线程执行Bar方法需要获得lockObj的访问权限,但是已被当前线程锁定,新线程将阻塞 t.Join(); //新线程将无法返回,死锁 } } } |
上面两个例子已经用了lock而不是Monitor,事实上,lock经过编译后就是Monitor,但是lock无法使用Monitor.TryEntry:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
.try{ ... IL_0037: call void [mscorlib]System.Threading.Monitor::Enter(object, bool&) ...} // end .tryfinally{ ... IL_0069: call void [mscorlib]System.Threading.Monitor::Exit(object) ...} |
最后,设计一个简单的带一个缓冲队列的Log方法,要求线程安全,下面给出C#的实现(在前面的【Windows】线程漫谈——线程同步之关键段利用关键段给出了C++的实现,这里的代码结构几乎一样,注释就省略了):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
public class LogInfo { public int Level{get;set;} public string Message{get;set;} } public class Log { private static List<LogInfo> LogQueue = new List<LogInfo>(); private static object _lockLog = new object(); private static object _lockQueue = new object(); public void Log(int Level, string Message) { if (Monitor.TryEnter(_lockLog)) { Monitor.Enter(_lockQueue); foreach (var l in LogQueue) { LogInternal(l.Level, l.Message); } LogQueue.Clear(); Monitor.Exit(_lockQueue); LogInternal(Level, Message); Monitor.Exit(_lockLog); } else { Monitor.Enter(_lockQueue); LogQueue.Add(new LogInfo { Level = Level, Message = Message }); Monitor.Exit(_lockQueue); } } protected virtual void LogInternal(int Level, string Message) { //真实的log动作可能会耗费非常长的时间 } } |
劳动果实,转载请注明出处:http://www.cnblogs.com/P_Chou/archive/2012/07/18/monitor-in-net-thread-sync.html

浙公网安备 33010602011771号