a:link{/正常的未被访问过的标签;/
text-decoration:none;
font-family:#000000;
color:red;
}
a:hover{
/指鼠标在连接/
text-decoration:underline;
font-size:50px;
color:blue;
}
a:active{
/正在点的链接/
}
a:visited {} / 已访问的链接 /
a:hover {} / 当有鼠标悬停在链接上 /
a:active {} / 被选择的链接 /
a:link {} / 未访问的链接 /
***线程的简单使用
需要引用System.Threading命名空间
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(1000);// 模拟耗时操作
}
}
***并发和异步的区别
class Program {
static void Main(string[] args) {
Thread t1 = new Thread(Working);
t1.Name = "Thread1";
Thread t2 = new Thread(Working);
t2.Name = "Thread2";
Thread t3 = new Thread(Working);
t3.Name = "Thread3";
// 依次启动3个线程。
t1.Start();
t2.Start();
t3.Start();
Console.ReadKey();
}
// 每个线程都同时在工作
static void Working() {
// 模拟1000次写日志操作
for (int i = 0; i < 1000; i++) {
// 异步写文件
Logger.Write(Thread.CurrentThread.Name + " writes a log: " + i + ", on " + DateTime.Now.ToString() + ".\n");
}// 做一些其它的事件
for (int i = 0; i < 1000; i++) { }
}
}
***并发控制 - 锁
done用来标识某件事已经做过了(告诉其它线程不要再重复做了)
class Program {
static bool done;
static void Main(string[] args) {
new Thread(Go).Start(); // 在新的线程上调用Go
Go(); // 在主线程上调用Go
Console.ReadKey();
}
输出了两个Done;
static void Go() {
if (!done) {
Thread.Sleep(500); // 模拟耗时操作
Console.WriteLine("Done");
done = true;
}
}
}
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(500); // Doing something.
Console.WriteLine("Done");
done = true;
}
}
}
}
*线程的信号机制
有时候你需要一个线程在接收到某个信号时,才开始执行,
否则处于等待状态,这是一种基于信号的事件机制。
.NET框架提供一个ManualResetEvent类来处理这类事件,
它的 WaiOne 实例方法可使当前线程一直处于等待状态,
直到接收到某个信号。它的Set方法用于打开发送信号。
下面是一个信号机制的使用示例:
static void Main(string[] args) {
var signal = new ManualResetEvent(false);
new Thread(() => {
Console.WriteLine("Waiting for signal...");
signal.WaitOne();
signal.Dispose();
Console.WriteLine("Got signal!");
}).Start();
Thread.Sleep(2000);
signal.Set();// 打开“信号”
Console.ReadKey();
}
当执行Set方法后,信号保持打开状态,可通过Reset方法将其关闭,
若不再需要,通过Dispose将其释放。如果预期的等待时间很短,
可以用ManualResetEventSlim代替ManualResetEvent,
前者在等待时间较短时性能更好。
信号机制非常有用,后面的日志案例会用到它。
*线程池中的线程
线程池中的线程是由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参数
案例:支持并发的异步日志组件
上文的“并发和异步的区别”的代码中我们用到了一个Logger类,现在我们就来
做一个这样的Logger。基于上面的知识,我们可以实现应用程序的并发写日
志日志功能。在应用程序中,写日志是常见的功能,简单分析一下该功能的需求:
在后台异步执行,和其它线程互不影响。
根据上文线程池的两个最优使用条件,由写日志线程会长时间处于阻塞
(或运行等待)状态,所以它不适合使用线程池。
即不能使用Task.Run,而最好使用new Thread。
支持并发,即多个任务(分布在不同线程上)
可同时调用写日志功能,但需保证线程安全。
支持并发,必然要用到锁,但要完全保证线程安全,那就要想办法避免“死锁”。
只要我们把“上锁”的操作始终由同一个线程来做即可避免“死锁”问题,但这样的话,
并发请求的任务只能放在队列中由该线程依次执行(因为是后台执行,无需即时响应用户,所以可以这么做)。
单个实例,单个线程。
任何地方调用写日志功能都调用的是同一个Logger实例(显然不能每次写日志都新建一个实例),
即需使用单例模式。不管有多少任务调用写日志功能,都必须始终使用同一个线程来处
理这些写日志操作,以保证不占用过多的线程资源和避免新建线程带来的延迟。
运用上面的知识,我们来写一个这样的类。简单理一下思路:
1.需要一个用来存放写日志任务的队列。
2.需要有一个信号机制来标识是否有新的任务要执行。
3.当有新的写日志任务时,将该任务加入到队列中,并发出信号。
4.用一个方法来处理队列中的任务,当接收新任务信号时,就依次调用队列中的任务。
开发一个功能前需要有个简单的思路,保证心里面有底。
具体开发的时候会发现问题,然后再去补充扩展和完善等。
刚开始很难想得太周全,先有个简单的思路,然后代码写起来!
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(100);
// 开始执行队列中的任务。
// 由于执行过程中还可能会有新的任务,所以不能直接对原来的 _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));
}
}