标签:
我也想过跳过C#高级知识点概要直接讲MVC,但经过前思后想,还是觉得有必要讲的。我希望通过自己的经验给大家一些指引,带着大家一起走上ASP.NET MVC大牛之路,少走弯路。同时也希望能和大家一起交流,这样也能发现我自己的不足,对我自己的帮助也是非常大的。
建议大家对C#撑握的不错的时候,可以去看一些开源项目。走技术这条路,就要耐得住寂寞(群里双休日说要让群主找妹子进群的人必须反思),练好内功。不撑握C#高级知识点,别想看懂优秀的开源项目,更别指望吸收其编程思想;你的水平,随时可以被一个实习生代替!切记不能浮躁!
本文讲线程和并发,这块知识点太多太多了,不可能用一篇文章写的面面具到(本身主题就是C#高级知识概要嘛),我所了解的也有限。但对于Web开发,我想本文的知识点应该足够,如果后面有遇到本文没讲的,后面再补充吧。
本文目录:
线程的简单使用 并发和异步的区别 并发控制 - 锁 线程的信号机制 线程池中的线程 案例:支持并发的异步日志组件 结语
常见的并发和异步大多是基于线程来实现的,所以本文先讲线程的简单使用方法。
使用线程,我们需要引用System.Threading命名空间。创建一个线程最简单的方法就是在 new 一个 Thread,并传递一个ThreadStart委托(无参数)或ParameterizedThreadStart委托(带参数),如下:
01.
class
Program {
02.
static
void
Main(string[] args) {
03.
04.
// 使用无参数委托ThreadStart
05.
Thread t =
new
Thread(Go);
06.
t.Start();
07.
08.
// 使用带参数委托ParameterizedThreadStart
09.
Thread t2 =
new
Thread(GoWithParam);
10.
t2.Start(
‘Message from main.‘
);
11.
12.
t2.Join();
// 等待线程t2完成。
13.
14.
Console.WriteLine(
‘Thread t2 has ended!‘
);
15.
Console.ReadKey();
16.
}
17.
18.
static
void
Go() {
19.
Console.WriteLine(
‘Go!‘
);
20.
}
21.
22.
static
void
GoWithParam(object msg) {
23.
Console.WriteLine(
‘Go With Param! Message: ‘
+ msg);
24.
Thread.Sleep(
1000
);
//
模拟耗时操作
25.
}
26.
}
运行结果:
线程的用法,我们只需要了解这么多。下面我们再来通过一段代码来讲讲并发和异步。
关于并发和异步,我们先来写一段代码,模拟多个线程同时写1000条日志:
01.
class
Program {
02.
static
void
Main(string[] args) {
03.
04.
Thread t1 =
new
Thread(Working);
05.
t1.Name =
‘Thread1‘
;
06.
Thread t2 =
new
Thread(Working);
07.
t2.Name =
‘Thread2‘
;
08.
Thread t3 =
new
Thread(Working);
09.
t3.Name =
‘Thread3‘
;
10.
11.
// 依次启动3个线程。
12.
t1.Start();
13.
t2.Start();
14.
t3.Start();
15.
16.
Console.ReadKey();
17.
}
18.
19.
// 每个线程都同时在工作
20.
static
void
Working() {
21.
// 模拟1000次写日志操作
22.
for
(
int
i =
0
; i <
1000
; i++) {
23.
// 异步写文件
24.
Logger.Write(Thread.CurrentThread.Name +
‘ writes a log: ‘
+ i +
‘, on ‘
+ DateTime.Now.ToString() + ‘.
25.
‘);
26.
}
// 做一些其它的事件
27.
for
(
int
i =
0
; i <
1000
; i++) { }
28.
}
29.
}
代码很简单,相信大家都能看得懂。Logger 大家可以把它看做是一个写日志的组件,先不关心它的具体实现,只要知道它是一个提供了写日志功能的组件就行。
那么,这段代码跟并发和异步有什么关系呢?
我们先用一张图来描述这段代码:
观察上图,3个线程同时调用Logger写日志,对于Logger来说,3个线程同时交给了它任务,这种情况就是并发。对于其中一个线程来说,它在工作过程中,在某个时间请求Logger帮它写日志,同时又继续在自己的其它工作,这种情况就是异步。
(经读者反馈,为不“误导”读者(尽管我个人不觉得是误导。之前我的定义和解释不全面,没有从操作系统和CPU层次去区分这两个概念。我的文章不喜欢搬教科书,只是想用通俗易读的白话让大家理解),为了知识的专业性和严谨,现已把我理解的对并发和异步的定义删除,感谢园友们的热心讨论)。
接下来,我们继续讲几个很有用的有关线程和并发的知识 - 锁、信号机制和线程池。
CLR 会为每个线程分配自己的内存堆空间,以使他们的本地变量保持分离互不干扰。
线程之间也可以共享通用的数据,比如同一对象的某个属性或全局静态变量。但线程间共享数据是存在安全问题的。举个例子,下面的主线程和新线程共享了变量done,done用来标识某件事已经做过了(告诉其它线程不要再重复做了):
01.
class
Program {
02.
static
bool done;
03.
static
void
Main(string[] args) {
04.
05.
new
Thread(Go).Start();
// 在新的线程上调用Go
06.
Go();
// 在主线程上调用Go
07.
08.
Console.ReadKey();
09.
}
10.
11.
static
void
Go() {
12.
if
(!done) {
13.
Thread.Sleep(
500
);
// 模拟耗时操作
14.
Console.WriteLine(
‘Done‘
);
15.
done =
true
;
16.
}
17.
}
18.
}
输出结果:
输出了两个“Done”,事件被做了两次。由于没有控制好并发,这就出现了线程的安全问题,无法保证数据的状态。
要解决这个问题,就需要用到锁(Lock,也叫排它锁或互斥锁)。使用lock语句,可以保证共享数据只能同时被一个线程访问。lock的数据对象要求是不能null的引用类型的对象,所以lock的对象需保证不能为空。为此需要创建一个不为空的对象来使用锁,修改一下上面的代码如下:
01.
class
Program {
02.
03.
static
bool done;
04.
static
object locker =
new
object();
// !!
05.
06.
static
void
Main(string[] args) {
07.
08.
new
Thread(Go).Start();
// 在新的线程上调用Go
09.
Go();
// 在主线程上调用Go
10.
11.
Console.ReadKey();
12.
}
13.
14.
static
void
Go() {
15.
lock (locker) {
16.
if
(!done) {
17.
Thread.Sleep(
500
);
// Doing something.
18.
Console.WriteLine(
‘Done‘
);
19.
done =
true
;
20.
}
21.
}
22.
}
23.
}
再看结果:
使用锁,我们解决了问题。但使用锁也会有另外一个线程安全问题,那就是“死锁”,死锁的概率很小,但也要避免。保证“上锁”这个操作在一个线程上执行是避免死锁的方法之一,这种方法在下文案例中会用到。
这里我们就不去深入研究“死锁”了,感兴趣的朋友可以去查询相关资料。
有时候你需要一个线程在接收到某个信号时,才开始执行,否则处于等待状态,这是一种基于信号的事件机制。.NET框架提供一个ManualResetEvent类来处理这类事件,它的 WaiOne 实例方法可使当前线程一直处于等待状态,直到接收到某个信号。它的Set方法用于打开发送信号。下面是一个信号机制的使用示例:
01.
static
void
Main(string[] args) {
02.
03.
var signal =
new
ManualResetEvent(
false
);
04.
05.
new
Thread(() => {
06.
Console.WriteLine(
‘Waiting for signal...‘
);
07.
signal.WaitOne();
08.
signal.Dispose();
09.
Console.WriteLine(
‘Got signal!‘
);
10.
}).Start();
11.
Thread.Sleep(
2000
);
12.
13.
signal.Set();
// 打开“信号”
14.
15.
Console.ReadKey();
16.
}
运行结果:
当执行Set方法后,信号保持打开状态,可通过Reset方法将其关闭,若不再需要,通过Dispose将其释放。如果预期的等待时间很短,可以用ManualResetEventSlim代替ManualResetEvent,前者在等待时间较短时性能更好。信号机制非常有用,后面的日志案例会用到它。
线程池中的线程是由CLR来管理的。在下面两种条件下,线程池能起到最好的效用:
任务运行的时候比较短(<250ms),这样CLR可以充分调配现有的空闲线程来处理该任务; 大量时间处于等待(或阻塞)的任务不去支配线程池的线程。要使用线程中的线程,主要有下面两种方式:
1.
// 方式1:Task.Run,.NET Framework 4.0 才有
2.
Task.Run (() => Console.WriteLine (
‘Hello from the thread pool‘
));
3.
4.
// 方式2:ThreadPool.QueueUserWorkItem
5.
ThreadPool.QueueUserWorkItem (t => Console.WriteLine (
‘Hello from the thread pool‘
));
线程池使得线程可以充分有效地被使用,减少了任务启动的延迟。但是不是所有的情况都适合使用线程池中的线程,比如下面要讲的日志案例 - 异步写文件。
这里讲线程池,是为了让大家大致了解什么时候用线程池中的线程,什么时候不用。即,耗时长或有阻塞情况的不用线程池中的线程。
创建不走线程池中的线程,可以直接通过new Thread来创建,也可以通过下面的代码来创建:
1.
Task task = Task.Factory.StartNew (() => ...,TaskCreationOptions.LongRunning);
// 注意必须带TaskCreationOptions.LongRunning参数
这里用到了Task,大家不用关心它,后续博文会详细讲。
关于线程的知识很多,这里不再深入了,因为这些已经足够让我们应付Web开发了。
上文的“并发和异步的区别”的代码中我们用到了一个Logger类,现在我们就来做一个这样的Logger。
基于上面的知识,我们可以实现应用程序的并发写日志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:
在后台异步执行,和其它线程互不影响。运用上面的知识,我们来写一个这样的类。简单理一下思路:
需要一个用来存放写日志任务的队列。 需要有一个信号机制来标识是否有新的任务要执行。 当有新的写日志任务时,将该任务加入到队列中,并发出信号。 用一个方法来处理队列中的任务,当接收新任务信号时,就依次调用队列中的任务。开发一个功能前需要有个简单的思路,保证心里面有底。具体开发的时候会发现问题,然后再去补充扩展和完善等。刚开始很难想得太周全,先有个简单的思路,然后代码写起来!
下面是这样一个Logger类初步实现:
01.
public
class
Logger {
02.
03.
// 用于存放写日志任务的队列
04.
private
Queue<Action> _queue;
05.
06.
// 用于写日志的线程
07.
private
Thread _loggingThread;
08.
09.
// 用于通知是否有新日志要写的“信号器”
10.
private
ManualResetEvent _hasNew;
11.
12.
// 构造函数,初始化。
13.
private
Logger() {
14.
_queue =
new
Queue<Action>();
15.
_hasNew =
new
ManualResetEvent(
false
);
16.
17.
_loggingThread =
new
Thread(Process);
18.
_loggingThread.IsBackground =
true
;
19.
_loggingThread.Start();
20.
}
21.
22.
// 使用单例模式,保持一个Logger对象
23.
private
static
readonly Logger _logger =
new
Logger();
24.
private
static
Logger GetInstance() {
25.
/* 不安全代码
26.
lock (locker) {
27.
if (_logger == null) {
28.
_logger = new Logger();
29.
}
30.
}*/
31.
return
_logger;
32.
}
33.
34.
// 处理队列中的任务
35.
private
void
Process() {
36.
while
(
true
) {
37.
// 等待接收信号,阻塞线程。
38.
_hasNew.WaitOne();
39.
40.
// 接收到信号后,重置“信号器”,信号关闭。
41.
_hasNew.Reset();
42.
43.
// 由于队列中的任务可能在极速地增加,这里等待是为了一次能处理更多的任务,减少对队列的频繁“进出”操作。
44.
Thread.Sleep(
100
);
45.
46.
// 开始执行队列中的任务。
47.
// 由于执行过程中还可能会有新的任务,所以不能直接对原来的 _queue 进行操作,
48.
// 先将_queue中的任务复制一份后将其清空,然后对这份拷贝进行操作。
49.
50.
Queue<Action> queueCopy;
51.
lock (_queue) {
52.
queueCopy =
new
Queue<Action>(_queue);
53.
_queue.Clear();
54.
}
55.
56.
foreach (var action in queueCopy) {
57.
action();
58.
}
59.
}
60.
}
61.
62.
private
void
WriteLog(string content) {
63.
lock (_queue) {
// todo: 这里存在线程安全问题,可能会发生阻塞。
64.
// 将任务加到队列
65.
_queue.Enqueue(() => File.AppendAllText(
‘log.txt‘
,
content));
66.
}
67.
68.
// 打开“信号”
69.
_hasNew.Set();
70.
}
71.
72.
// 公开一个Write方法供外部调用
73.
public
static
void
Write(string content) {
74.
// WriteLog 方法只是向队列中添加任务,执行时间极短,所以使用Task.Run。
75.
Task.Run(() => GetInstance().WriteLog(content));
76.
}
77.
}
类写好了,用上文“并发和异步的区别”中的代码测试一下这个Logger类,在我的电脑上运行的一次结果:
共3000条日志,结果没有问题。
上面的Logger类注释写得很详细,我就不再解析了。
通过这个示例,目的是让大家掌握线程和并发在开发中的基本应用和要注意的问题。
遗憾的是这个Logger类并不完美,而且存在线程安全问题(代码中用红色字体标出),虽然实际环境概率很小。可能上面代码多次运行都很难看到有异常发生(我多次运行未发生异常),但同时再添加几个线程可能就会有问题了。
标签:
原文地址:http://blog.csdn.net/liumeiq/article/details/42363741