目录:

1.线程简单使用

2.并发和异步的区别

3.并发控制—锁

4.线程的通信机制

5.线程池中的线程

6.案例:支持并发的异步日志组件

7.结束

1.线程的简单使用----------------------------------------------------

常见的并发和异步大多是基于线程来实现的,所以本文先讲线程的简单使用方法。

使用线程,我们需要引用System.Threading命名空间。创建一个线程最简单的方法就是在 new 一个 Thread,并传递一个ThreadStart委托(无参数)或ParameterizedThreadStart委托(带参数),如下:

class Program {
static void Main(string[] args) { // 使用无参数委托ThreadStart
Thread t = new Thread(Go);
t.Start(); // 使用带参数委托ParameterizedThreadStart
Thread t2 = new Thread(GoWithParam);
t2.Start("Message from main."); t2.Join();// 等待线程t2完成。 Console.WriteLine("Thread t2 has ended!");
Console.ReadKey();
} static void Go() {
Console.WriteLine("Go!");
} static void GoWithParam(object msg) {
Console.WriteLine("Go With Param! Message: " + msg);
Thread.Sleep();// 模拟耗时操作
}
}

运行结果:


线程的用法,我们只需要了解这么多。下面我们再来通过一段代码来讲讲并发和异步。

2.并发和异步的区别-----------------------------------------------------

关于并发和异步,我们先来写一段代码,模拟多个线程同时写10000条日志:

class Program {
static void Main(string[] args) { Thread t1 = new Thread(Working);
t1.Name = "Thread线程1:";
Thread t2 = new Thread(Working);
t2.Name = "Thread线程2:";
Thread t3 = new Thread(Working);
t3.Name = "Thread线程3:";
        Thread t4 = new Thread(Working);
t3.Name = "Thread线程4";
        // 依次启动3个线程。 
t1.Start();
t2.Start();
t3.Start();
t4.Start();
Console.ReadKey();
}
// 每个线程都同时在工作
static void Working() {
// 模拟1000次写日志操作
for (int i = ; i < ; i++) {
// 异步写文件 Logger.Write(Thread.CurrentThread.Name + " writes a log:写日志 " + i + ", on " + DateTime.Now.ToString() + ".\n"); }
// 做一些其它的事件 for (int i = ; i < ; i++) { }
}
}
代码很简单,相信大家都能看得懂。Logger 大家可以把它看做是一个写日志的组件,先不关心它的具体实现,只要知道它是一个提供了写日志功能的组件就行。

那么,这段代码跟并发和异步有什么关系呢?


我们先用一张图来描述这段代码:

观察上图,3个线程同时调用Logger写日志,对于Logger来说,3个线程同时交给了它任务,这种情况就是并发。对于其中一个线程来说,它在工作过程中,在某个时间请求Logger帮它写日志,同时又继续在自己的其它工作,这种情况就是异步

 经读者反馈,为不“误导”读者(尽管我个人不觉得是误导。之前我的定义和解释不全面,没有从操作系统和CPU层次去区分这两个概念。我的文章不喜欢搬教科书,只是想用通俗易读的白话让大家理解),为了知识的专业性和严谨,现已把我理解的对并发和异步的定义删除,感谢园友们的热心讨论)。

3.并发控制 - 锁--------------------------------------------------------------

CLR 会为每个线程分配自己的内存堆空间,以使他们的本地变量保持分离互不干扰。

线程之间也可以共享通用的数据,比如同一对象的某个属性或全局静态变量。但线程间共享数据是存在安全问题的。举个例子,下面的主线程和新线程共享了变量done,done用来标识某件事已经做过了(告诉其它线程不要再重复做了):

class Program {
static bool done;
static void Main(string[] args) { new Thread(Go).Start(); // 在新的线程上调用Go
Go(); // 在主线程上调用Go Console.ReadKey();
} static void Go() {
if (!done) {
Thread.Sleep(); // 模拟耗时操作
Console.WriteLine("Done");
done = false;
}
}
}
输出结果:

输出了两个“Done”,事件被做了两次。由于没有控制好并发,这就出现了线程的安全问题,无法保证数据的状态。

要解决这个问题,就需要用到锁(Lock,也叫排它锁或互斥锁)。使用lock语句,可以保证共享数据只能同时被一个线程访问。lock的数据对象要求是不能null的引用类型的对象,所以lock的对象需保证不能为空。为此需要创建一个不为空的对象来使用锁,修改一下上面的代码如下:


class Program {

    static bool done;
static object locker = new object(); // static void Main(string[] args) { new Thread(Go).Start(); // 在新的线程上调用Go
Go(); // 在主线程上调用Go Console.ReadKey();
} static void Go() {
lock (locker) {
if (!done) {
Thread.Sleep(); // Doing something.
Console.WriteLine("Done");
done = true;
}
}
}
}

再看结果:


使用锁,我们解决了问题。但使用锁也会有另外一个线程安全问题,那就是“死锁”,死锁的概率很小,但也要避免。保证“上锁”这个操作在一个线程上执行是避免死锁的方法之一,这种方法在下文案例中会用到。

这里我们就不去深入研究“死锁”了,感兴趣的朋友可以去查询相关资料。

 4.线程的信号机制---------------------------------------

 有时候你需要一个线程在接收到某个信号时,才开始执行,否则处于等待状态,这是一种基于信号的事件机制。.NET框架提供一个ManualResetEvent类来处理这类事件,它的 WaiOne 实例方法可使当前线程一直处于等待状态,直到接收到某个信号。它的Set方法用于打开发送信号。下面是一个信号机制的使用示例:
static void Main(string[] args) {

    var signal = new ManualResetEvent(false);//ManualResetEvent:用一个指示是否将初始状态设置为终止的布尔值初始化类的新实例。

    new Thread(() => {
Console.WriteLine("等待信号...");
signal.WaitOne();
signal.Dispose();
Console.WriteLine("Got signal!");
}).Start();
Thread.Sleep(); signal.Set();// 打开“信号” Console.ReadKey();
}
运行结果:

当执行Set方法后,信号保持打开状态,可通过Reset方法将其关闭,若不再需要,通过Dispose将其释放。如果预期的等待时间很短,可以用ManualResetEventSlim代替ManualResetEvent,前者在等待时间较短时性能更好。信号机制非常有用,后面的日志案例会用到它。

5.线程池中的线程----------------------------------------------------

线程池中的线程是由CLR来管理的。在下面两种条件下,线程池能起到最好的效用:

  • 任务运行的时候比较短(<250ms),这样CLR可以充分调配现有的空闲线程来处理该任务;
  • 大量时间处于等待(或阻塞)的任务不去支配线程池的线程。

要使用线程中的线程,主要有下面两种方式:

 方式1:Task.Run,.NET Framework 4.5 才有
Task.Run (() => Console.WriteLine ("Hello from the thread pool")); // 方式2:ThreadPool.QueueUserWorkItem
ThreadPool.QueueUserWorkItem (t => Console.WriteLine ("Hello from the thread pool"));

程池使得线程可以充分有效地被使用,减少了任务启动的延迟。但是不是所有的情况都适合使用线程池中的线程,比如下面要讲的日志案例 - 异步写文件。

这里讲线程池,是为了让大家大致了解什么时候用线程池中的线程,什么时候不用。即,耗时长或有阻塞情况的不用线程池中的线程。

创建不走线程池中的线程,可以直接通过new Thread来创建,也可以通过下面的代码来创建:

Task task = Task.Factory.StartNew (() => ...,TaskCreationOptions.LongRunning);// 注意必须带TaskCreationOptions.LongRunning参数

这里用到了Task,大家不用关心它,后续博文会详细讲。

关于线程的知识很多,这里不再深入了,因为这些已经足够让我们应付Web开发了。

6.案例:支持并发的异步日志组件-------------------------------------

上文的“并发和异步的区别”的代码中我们用到了一个Logger类,现在我们就来做一个这样的Logger。

基于上面的知识,我们可以实现应用程序的并发写日志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:

在后台异步执行,和其它线程互不影响。
根据上文线程池的两个最优使用条件,由写日志线程会长时间处于阻塞(或运行等待)状态,所以它不适合使用线程池。即不能使用Task.Run,而最好使用new Thread。 支持并发,即多个任务(分布在不同线程上)可同时调用写日志功能,但需保证线程安全。
支持并发,必然要用到锁,但要完全保证线程安全,那就要想办法避免“死锁”。只要我们把“上锁”的操作始终由同一个线程来做即可避免“死锁”问题,但这样的话,并发请求的任务只能放在队列中由该线程依次执行(因为是后台执行,无需即时响应用户,所以可以这么做)。 单个实例,单个线程。
任何地方调用写日志功能都调用的是同一个Logger实例(显然不能每次写日志都新建一个实例),即需使用单例模式。不管有多少任务调用写日志功能,都必须始终使用同一个线程来处理这些写日志操作,以保证不占用过多的线程资源和避免新建线程带来的延迟。
运用上面的知识,我们来写一个这样的类。简单理一下思路: 需要一个用来存放写日志任务的队列。
需要有一个信号机制来标识是否有新的任务要执行。
当有新的写日志任务时,将该任务加入到队列中,并发出信号。
用一个方法来处理队列中的任务,当接收新任务信号时,就依次调用队列中的任务。

开发一个功能前需要有个简单的思路,保证心里面有底。具体开发的时候会发现问题,然后再去补充扩展和完善等。刚开始很难想得太周全,先有个简单的思路,然后代码写起来!


下面是这样一个Logger类初步实现:


public class Logger {

    // 用于存放写日志任务的队列
private Queue<Action> _queue; // 用于写日志的线程
private Thread _loggingThread; // 用于通知是否有新日志要写的“信号器”
private ManualResetEvent _hasNew; // 构造函数,初始化。
private Logger() {
_queue = new Queue<Action>();
_hasNew = new ManualResetEvent(false); _loggingThread = new Thread(Process);
_loggingThread.IsBackground = true;
_loggingThread.Start();
} // 使用单例模式,保持一个Logger对象
private static readonly Logger _logger = new Logger();
private static Logger GetInstance() {
/* 不安全代码
lock (locker) {
if (_logger == null) {
_logger = new Logger();
}
}*/
return _logger;
} // 处理队列中的任务
private void Process() {
while (true) {
// 等待接收信号,阻塞线程。
_hasNew.WaitOne(); // 接收到信号后,重置“信号器”,信号关闭。
_hasNew.Reset(); // 由于队列中的任务可能在极速地增加,这里等待是为了一次能处理更多的任务,减少对队列的频繁“进出”操作。
Thread.Sleep(); // 开始执行队列中的任务。
// 由于执行过程中还可能会有新的任务,所以不能直接对原来的 _queue 进行操作,
// 先将_queue中的任务复制一份后将其清空,然后对这份拷贝进行操作。 Queue<Action> queueCopy;
lock (_queue) {
queueCopy = new Queue<Action>(_queue);
_queue.Clear();
} foreach (var action in queueCopy) {
action();
}
}
} private void WriteLog(string content) {
lock (_queue) { // todo: 这里存在线程安全问题,可能会发生阻塞。
// 将任务加到队列
_queue.Enqueue(() => File.AppendAllText("log.txt", content));
} // 打开“信号”
_hasNew.Set();
} // 公开一个Write方法供外部调用
public static void Write(string content) {
// WriteLog 方法只是向队列中添加任务,执行时间极短,所以使用Task.Run。
Task.Run(() => GetInstance().WriteLog(content));
}
}
类写好了,用上文“并发和异步的区别”中的代码测试一下这个Logger类,在我的电脑上运行的一次结果:

共3000条日志,结果没有问题。

上面的Logger类注释写得很详细,我就不再解析了。

通过这个示例,目的是让大家掌握线程和并发在开发中的基本应用和要注意的问题。

遗憾的是这个Logger类并不完美,而且存在线程安全问题(代码中用红色字体标出),虽然实际环境概率很小。可能上面代码多次运行都很难看到有异常发生(我多次运行未发生异常),但同时再添加几个线程可能就会有问题了。

那么,如何解决这个线程安全问题呢?


7.结语------------------------------------------------------------------------------------


     线程内容结束就到这里了,但留给了大家一个问题 - 如何解决上面的Logger类的线程安全问题。希望有兴趣的朋友们思考后都能参与讨论。


除此,大家还可一起讨论如何完善它,如何更好的封装它,使其更安全、更健壮、性能更优等等。

 

[ASP.NET 大牛之路]03 - C#高级知识点概要(2) - 线程和并发的更多相关文章

  1. [ASP.NET MVC 大牛之路]03 - C#高级知识点概要(2) - 线程和并发

    本人博客已转移至:http://www.exblr.com/liam  我也想过跳过C#高级知识点概要直接讲MVC,但经过前思后想,还是觉得有必要讲的.我希望通过自己的经验给大家一些指引,带着大家一起 ...

  2. [ASP.NET 大牛之路]02 - C#高级知识点概要(1) - 委托和事件

    在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知识点.照此,ASP.NET MVC 大牛之路系列也先给大家普及一下C#.NET中的高级知识点.每个知识点不太会过于详细,但 ...

  3. C#高级知识点概要(2) - 线程和并发

    原文地址:http://www.cnblogs.com/Leo_wl/p/4192935.html 我也想过跳过C#高级知识点概要直接讲MVC,但经过前思后想,还是觉得有必要讲的.我希望通过自己的经验 ...

  4. [ASP.NET MVC 大牛之路]02 - C#高级知识点概要(1) - 委托和事件

    在ASP.NET MVC 小牛之路系列中,前面用了一篇文章提了一下C#的一些知识点.照此,ASP.NET MVC 大牛之路系列也先给大家普及一下C#.NET中的高级知识点.每个知识点不太会过于详细,但 ...

  5. C#高级知识点概要(2) - 线程并发锁

    本文目录: 线程的简单使用 并发和异步的区别 并发控制 - 锁 线程的信号机制 线程池中的线程 案例:支持并发的异步日志组件 线程的简单使用 常见的并发和异步大多是基于线程来实现的,所以本文先讲线程的 ...

  6. C#高级知识点概要(3) - 特性、自动属性、对象集合初始化器、扩展方法、Lambda表达式和Linq查询

    1.特性(Attributes) 特性(Attributes),MSDN的定义是:公共语言运行时允许你添加类似关键字的描述声明,叫做attributes, 它对程序中的元素进行标注,如类型.字段.方法 ...

  7. C#高级知识点概要(1) - 委托和事件

    本文目录: 委托 委托的简单使用 用委托实现插件式编程 多播委托 静态方法和实例方法对于委托的区别 泛型委托 Func 和 Action 委托 委托的兼容 事件 事件的基本使用 事件的标准模式 委托 ...

  8. [ASP.Net] 转 > ASP.NET MVC 大牛之路

    URL: http://www.cnblogs.com/willick/ [ASP.NET MVC 大牛之路]01 - 开篇 [ASP.NET MVC 大牛之路]02 - C#高级知识点概要(1) - ...

  9. [ASP.NET MVC 大牛之路]01 - 开篇

    匆匆2014,转眼就到末尾了.在这一年,你还有哪事情些想做而没有做? 2014年在我身上发生了两件意义重大的事,一是正月初一宝宝出生,我升级成为了爸爸:二是进入了一家创业公司,成为了技术负责人. 去年 ...

随机推荐

  1. 关于Cocos2d-x有些头文件无法引入或者类显示无法打开

    选中工程右键“属性”->"配置属性“->"c/c++"->"常规”->"附加包含目录"中添加“”$(EngineRo ...

  2. JDK中的序列化和反序列化

    题外话:诸事缠身,不知不觉距离上一篇就将近一个月了,读书不易,学习不易,唯有坚持. 写来写去始终不满意,索性贴一个比较好的文章吧! 参考: [Java基础]序列化与反序列化深入分析

  3. e638. 向剪切板获取和粘贴图像

    // If an image is on the system clipboard, this method returns it; // otherwise it returns null. pub ...

  4. u3d调用自己的dll

    原文地址:http://blog.sina.com.cn/s/blog_62f7cb730100zhhf.html 首先用vc建立一个dll工程 然后在里面建立一个testunity.h文件.内容如下 ...

  5. PureMVC和Unity3D的UGUI制作一个简单的员工管理系统实例

    前言: 1.关于PureMVC: MVC框架在很多项目当中拥有广泛的应用,很多时候做项目前人开坑开了一半就消失了,后人为了填补各种的坑就遭殃的不得了.嘛,程序猿大家都不喜欢像文案策划一样组织文字写东西 ...

  6. 我的javascript心跳机

    li { list-style: none!important; padding:0; } .list_num{ list-style-type:decimal; } .list_inline{ ma ...

  7. HDOJ 4276 The Ghost Blows Light

    题意 1. 给定一棵树, 树上节点有 value, 节点之间 travel 有 cost. 给定起始节点和最大 cost, 求解最大 value 思路 1. 寻找最短路径 a. 题目描述中有两句话, ...

  8. swift开发之--UISearchBar的使用/UISearchController的使用

    记录下UISearchBar的基本用法,补充:ios 8.0以后,原来的UISearchDisplayController被官方废弃,建议使用UISearchController,下面就简单的记录下这 ...

  9. X-code 描述文件的位置

    不管是真机测试还是打包的过程中,都需要描述文件.在桌面上,按快捷键“commd+Shift+G”,就会显示一个要填的文件路径,如下图: 找到描述文件的路径: ~/Library/MobileDevic ...

  10. Windows7安装Mongodb

    1.安装mongodb-win32-x86_64-3.0.4-signed.msi 2.安装kb2731284 安装补丁:Windows6.1-KB2731284-v3-x64.msu 3.创建数据库 ...