一般在写多线程的程序的时候,都会碰到线程同步的问题。什么是线程同步?就是让多个干活的线程在访问到同一个变量(或者一堆变量)时有个先后次序。如果这几个线程没有同步,会发生什么情况呢?我们看看下面这个程序:
public class Program1
{
public int key;
public string value;
public Program1()
{
key = 5;
value = "伍";
}
public void SetOdd()
{
while (true)
{
key = 5;
Thread.Sleep(2000); // 挂起线程,让其它线程执行,增加key和value不一致的机会
value = "伍";
}
}
public void SetEven()
{
while (true)
{
key = 4;
Thread.Sleep(1000); // 挂起线程,让其它线程执行,增加key和value不一致的机会
value = "肆";
}
}
public void start()
{
// 线程t1
Thread t1 = new Thread(this.SetOdd);
// 线程t2
Thread t2 = new Thread(this.SetEven);
t1.Start();
t2.Start();
int i = 1;
while (true)
{
Thread.Sleep(50);
if ((key == 5 && value.Equals("肆"))
|| (key == 4 && value.Equals("伍")))
{
Console.WriteLine(i + "\t" + key + ":" + value);
i++;
}
}
}
}
上面的程序可以通过下面两句代码执行:
Program1 x = new Program1();
x.start();
这个程序在实际执行过程中,启动了三个线程。线程t1将key的值设为5,并把value的值设为“伍”;线程t2将key的值设为4,并把value的值设为“肆”。主线程则每隔一段时间(50毫秒左右,线程在sleep后不一定能马上占用cpu)查看key和value是否一致,如果key为5而value是“肆”或者key为4而value是“伍”,则把key和value打印在控制台上。上面的程序执行结果如下:
我们来看看为什么会出现这种情况,key和value的初始值非别是5和“伍”。在t1.Start()这句话执行后,线程t1被加到线程等待队列中,随后在t2.Start()执行后,线程t2被加到等待队列中,主线程继续往下走,不断轮询key和value的状态,如果发现他们两个不一致,就把他们在控制台打印出来。因为线程t1排在t2的前面,所以当cpu空闲的时候,t1先执行,但是t1启动后,在把key的值设为5后,就被挂起来了。这时系统发现cpu又空闲着,就告诉t2,说:“cpu空着,你可以去用了”,于是线程t2开始执行,但是在线程t2把key的值设为4后,它也被挂起来了。这个时候主线程检查key和value的状态,发现key为4,但是value却是“伍”。于是就把key和value在控制台上打印了出来。
实际上我们要求key和value必须保持一致,也就是说key和value的赋值操作必须连续被执行,中间不能被截断。怎么才能做到呢?这个就要用到.Net平台提供的线程同步机制。.Net平台的System.Threading 命名空间提供了用于同步线程的类,这些类包括 Mutex、Moniter、Interlocked、AutoResetEvent 和 ManualResetEvent,这些类怎么用不是本文的重点,这些知识大家可以阅读MSDN来获取。这里我们用lock关键字(其实是对moniter的封装)来实现同步机制。
public class Program2
{
public int key;
public string value;
public Program2()
{
key = 5;
value = "伍";
}
public void SetOdd()
{
while (true)
{
lock (this)
{
key = 5;
Thread.Sleep(2000); // 挂起线程,让其它线程执行,增加key和value不一致的机会
value = "伍";
}
}
}
public void SetEven()
{
while (true)
{
lock (this)
{
key = 4;
Thread.Sleep(1000); // 挂起线程,让其它线程执行,增加key和value不一致的机会
value = "肆";
}
}
}
public void start()
{
// 线程t1
Thread t1 = new Thread(this.SetOdd);
// 线程t2
Thread t2 = new Thread(this.SetEven);
t1.Start();
t2.Start();
int i = 1;
while (true)
{
Thread.Sleep(50);
lock (this)
{
if ((key == 5 && value.Equals("肆"))
|| (key == 4 && value.Equals("伍")))
{
Console.WriteLine(i + "\t" + key + ":" + value);
i++;
}
}
}
}
}
看过上面的程序后,大家可以发现程序中所有读写key和value的部分(已经绿色字体标识出来)都被下面这样一段代码包着:
lock(this)
{
/* 这里是实际运算部分 */
}
这段是什么意思呢?打个比方,假如有一间屋子,一次只能进去一个人,屋前有一扇门,门上有一把锁,每个人进去后就把们锁起来,这样其他人就进不去了,只有等这个人出来后,其它人才能进去。上面的代码段就是一间屋子,this对象就是一把锁,每个线程就是一个人。当一个线程执行到“lock(this)”的时候,会检查屋子有没有被锁起来,如果没有锁上,就进去,否则在门口等待。
在Program2中,所有的读写key和value的部分都被锁了起来,只要有一个线程在读写key和value——进了屋子,那么其它线程必须等待,这样就保证了key和value的一致性。
Program2的三个线程不会打架了,但是Program2却看上去不太美观:
Ø Key和value暴露在外面,直接供线程访问,如果key和value有很多,那么肯定让人看花眼,因为它们太散了
Ø 线程t1和线程t2的锁都是Program2对象本身,如果有很多线程,那么它们肯定会在“屋子”外面排起长队。比如有key1和value1、key2和value2,我们只需要保证key1和value1一致、key2和value2一致,但是如果访问它们的线程都是用的同一把锁,那么在key1和value1被修改的时候,访问key2和value2的线程不得不等待
为了解决第一个问题,我们需要拿个包裹把key和value包起来,线程如果要访问key和value,那么从包裹里面拿就可以了。第二个问题,也好解决,一把锁不行,用两把锁。两个问题的解决方案能不能结合起来呢?当然可以,我们在一个包裹里面放一把锁,如果线程正在访问包裹里面的东西,包裹自动给加上锁,直到线程离开。下面我们看看这个包裹是什么样子的:
using System;
using System.Collections.Generic;
using System.Text;
namespace ConsoleApplication2
{
class Package
{
public delegate void SynchronizedDelegate(); // 不带参数的同步代理
public delegate void ParamsSynchronizedDelegate(params object[] objs); // 带参数的同步代理
// 公共区域
private Dictionary<string, object> package = new Dictionary<string, object>();
// 非线程安全
public object this[string key]
{
get { return package[key]; }
set
{
package[key] = value;
}
}
// 设置公共区域变量,线程安全操作
public void SynSetAttribute(string key, string obj)
{
lock(this)
{
package[key] = obj;
}
}
// 取出公共区域变量,线程安全操作
public object SynGetAttribute(string key)
{
lock (this)
{
return package[key];
}
}
// 如果用户想让一系列连续的操作保持原子性
// 那么最好定义一个代理,然后通过此方法执行
public void SynchronizedExecute(SynchronizedDelegate method)
{
lock (this)
{
method();
}
}
public void SynchronizedExecute(ParamsSynchronizedDelegate method)
{
lock (this)
{
method();
}
}
}
}
这个package是个哈希表,专门用来存放东西。为了给让外界能够访问这个哈希表里面的东西,我们提供了访问单个值的接口,也提供了执行代码段的接口。有了个包裹,我们就可以着手改造我们的Program2了。
public class Program3
{
Package package = new Package();
public Program3()
{
// 将线程要访问的变量都塞到包裹里
package["key"] = 5;
package["value"] = "伍";
}
public void SetOdd()
{
while (true)
{
// 用包裹来执行我们代码段,这样我们就不必关心加锁的问题
package.SynchronizedExecute(delegate()
{
/* 包裹里执行的代码不能是线程安全的
* 比如,这里的代码不能是
* package.SynSetAttribute("key", 5)
* package.SynSetAttribute("value", "伍")
* 否则会形成死锁
*/
package["key"] = 5;
package["value"] = "伍";
});
}
}
public void SetEven()
{
while (true)
{
package.SynchronizedExecute(delegate()
{
package["key"] = 4;
package["value"] = "肆";
});
}
}
public void start()
{
// 线程t1
Thread t1 = new Thread(this.SetOdd);
// 线程t2
Thread t2 = new Thread(this.SetEven);
t1.Start();
t2.Start();
int i = 1;
while (true)
{
package.SynchronizedExecute(delegate()
{
int key = (int)package["key"];
string value = (string)package["value"];
if ((key == 5 && value.Equals("肆"))
|| (key == 4 && value.Equals("伍")))
{
Console.WriteLine(i + "\t" + key + ":" + value);
i++;
}
});
}
}
}
到这里,我们可以去喝杯咖啡,然后舒服在椅子上靠会了。
文章出处:http://www.diybl.com/course/4_webprogram/asp.net/asp_netshl/2007125/90632.html