1.线程通信
起因:有时,当某一个线程进入同步方法之后,共享变量并不满足它所需要的状态,该线程需要等待其它线程将共享变量改为它所需要的状态后才能往下执行。由于此时其它线程无法进入临界区,所以就需要该线程放弃监视器,并返回到排队状态等待其它线程交回监视器。“生产者与消费者”问题就是这一类典型的问题,设计程序时必须解决:生产者比消费者快时,消费者会漏掉一些数据没有取到的问题;消费者比生产者快时,消费者取相同数据的问题。
(1)使用Monitor实现线程通信
示例:模拟吃苹果。爸爸妈妈不断往盘子放苹果,老大老二,老三不断从盘子取苹果吃。五个线程需要同步执行,并且要相互协调。爸爸和妈妈放苹果的时候,盘子里要有地方,而且两个人不能同时放。三个孩子取苹果的时候盘子里要有苹果,而且不能同时取。三个好孩子因为吃苹果速度不同,取苹果的频率也不一样,又因大小不一致,有不同的优先级。
理解:本问题类似于“生产者与消费者”问题,因此定义一个生产者线程类和一个消费者线程类。盘子是一个中间类,包含了共享数据区和放苹果,取苹果的方法。为了避免冲突,放苹果和取苹果两个方法使用了同步控制机制。
class Program
{
static void Main(string[] args)
{
EatApple eatApple = new EatApple();
}
}
class EatApple
{
public EatApple()
{
//创建5个线程
Thread t1, t2, t3, t4, t5;
Dish d = new Dish(this, 20);
//妈妈线程
Productor mather = new Productor("妈妈", d);
//爸爸线程
Productor father = new Productor("爸爸", d);
//老大线程
Consumer one = new Consumer("老大", d, 1000);
//老二线程
Consumer two = new Consumer("老二", d, 2000);
//老三线程
Consumer three = new Consumer("老三", d, 3000);
t1 = new Thread(mather.Run);
t2 = new Thread(father.Run);
t3 = new Thread(one.Run);
t4 = new Thread(two.Run);
t5 = new Thread(three.Run);
//设置优先级
t1.Priority = ThreadPriority.Highest;
t2.Priority = ThreadPriority.Normal;
t3.Priority = ThreadPriority.BelowNormal;
t4.Priority = ThreadPriority.Normal;
t5.Priority = ThreadPriority.Highest;
t1.Start();
t2.Start();
t3.Start();
t4.Start();
t5.Start();
Console.ReadKey();
}
}
/// <summary>
/// 盘子类
/// </summary>
class Dish
{
int f = 4;//一个盘子最多放4个苹果
EatApple eat;
int num = 0;//可放的苹果数。
int n = 0;
public Dish(EatApple eat, int num)
{
this.eat = eat;
this.num = num;
}
public void Put(string name)
{
lock (this)
{
while (f == 0)
{
Console.WriteLine($"{name}等待放苹果");
//释放当前线程对象的锁,让其他线程访问。
Monitor.Wait(this);
}
Random random = new Random();
//random.Next(f):获取在0到f之间的任意一个值。
int x = (int)random.Next(f) + 1;
f -= x;
n += x;
Console.WriteLine($"{name}放{x}个苹果");
//现在盘子里有苹果了,所以当前线程准备释放锁,向所有等待线程通知此消息。可以来拿苹果了
Monitor.PulseAll(this);
if (n >= num)
{
//如果放的个数已经大于盘子上限,停止线程。
Thread.CurrentThread.Abort();
}
}
}
/// <summary>
/// 获取苹果
/// </summary>
/// <param name="name"></param>
public void Get(string name)
{
//不管是那苹果还是取苹果,一次只能一个人,
lock (this)
{
while (f == 4)
{
try
{
Console.WriteLine($"{name}等待取苹果");
Monitor.Wait(this);
}
catch (ThreadInterruptedException e)
{
}
}
f += 1;
Console.WriteLine($"{name}取苹果吃");
Monitor.PulseAll(this);
}
}
}
/// <summary>
/// 生产者类
/// </summary>
class Productor
{
//盘子
private Dish dish;
private string name;
public Productor(string name, Dish dish)
{
this.dish = dish;
this.name = name;
}
public void Run()
{
while(true)
{
dish.Put(name);
Thread.Sleep(100);
}
}
}
/// <summary>
/// 消费者类
/// </summary>
class Consumer
{
//盘子
private Dish dish;
private string name;
private int time;//吃苹果所用的时间
public Consumer(string name, Dish dish, int time)
{
this.dish = dish;
this.name = name;
this.time = time;
}
public void Run()
{
while (true)
{
dish.Get(name);
Thread.Sleep(time);
}
}
}
结果:
老二等待取苹果
老大等待取苹果
老三等待取苹果
爸爸放3个苹果
老二取苹果吃
老大取苹果吃
老三取苹果吃
妈妈放3个苹果
爸爸放1个苹果
妈妈等待放苹果
爸爸等待放苹果
老大取苹果吃
妈妈放1个苹果
爸爸等待放苹果
.......
(2)使用AutoResetEvent类和MaunalResetEvent类进行线程通信
AutoResetEvent允许线程通过发信号相互通信。线程通过调用AutoResetEvent上的WaitOne()来等待信号。如果AutoResetEvent处于无信号状态,则当前线程阻塞,等待有信号才能继续运行。调用Set可终止阻塞并释放等待线程。AutoResetEvent将保持有信号状态,直到一个正在等待的线程被释放,然后自动返回无信号状态,如果没有任何线程等待,那么将一直保持有信号状态。
MaunalResetEvent和AutoResetEvent的使用基本类似。不同的是AutoResetEvent在释放某个线程之后会自动将信号转换成无信号状态(因为释放线程之后,共享资源将被占用,自动转为无信号状态告诉其他等待线程还不能进入)。但是MaunalResetEvent要调用Reset()方法来设置成无信号状态。
示例:五位哲学家坐在餐桌前,他们在思考并在感到饥饿时就吃东西。每两位哲学家之间只有一根筷子,为了吃东西,一个哲学家必须要用2根筷子。如果每位哲学家拿起右筷子,然后等着拿左筷子,问题就产生了,在这种情况下,就会发生死锁。当哲学家放下筷子时,要通知其他等待拿筷子的哲学家。
注意:为筷子单独创建一个类,筷子是共享资源,由一个信号量AutoResetEvent来指明是否可用。由于任何时候只允许一位哲学家能拿起一根特定的筷子,所以拿筷子的方法需要被同步。当吃完时候通过putdown()来放下筷子。哲学家思考时通过think()放下筷子。
class Dining
{
//一共5个筷子,5位哲学家
static ChopStick[] cp = new ChopStick[5];
static Philosopher[] ph = new Philosopher[5];
static void Main(string[] args)
{
for (int i = 0; i < 5; i++)
{
cp[i] = new ChopStick(i);
}
for (int i = 0; i < 5; i++)
{
ph[i] = new Philosopher("哲学家"+i,cp[i],cp[(i+1)%5]);
}
for (int i = 0; i < 5; i++)
{
Thread thread = new Thread(ph[i].Run);
thread.Start();
}
}
}
/// <summary>
/// 抽象出筷子类
/// 筷子为共享资源
/// </summary>
class ChopStick
{
AutoResetEvent eventX;
int n;
public ChopStick(int n)
{
this.n = n;
//设置一个初始状态true。表明现在共享资源可用,是有信号状态。
eventX = new AutoResetEvent(true);
}
/// <summary>
/// 拿起筷子方法
/// </summary>
/// <param name="name">哲学家</param>
public void TakeUp(string name)
{
//每个筷子只能同时由一个人使用
lock(this)
{
Console.WriteLine($"{name}等待拿起筷子");
//当前线程等待拿起筷子,在获取到信号之前是不能往下执行的。
eventX.WaitOne();
Console.WriteLine($"{name}拿起第{n}根筷子");
}
}
/// <summary>
/// 放下筷子
/// </summary>
public void PutDown(string name)
{
//释放信号,使得等待线程可继续往下执行。
eventX.Set();
Console.WriteLine($"{name}放下第{n}根筷子");
}
}
/// <summary>
/// 哲学家类
/// </summary>
class Philosopher
{
//左右筷子
ChopStick left, right;
string name;
public Philosopher(string name,ChopStick left,ChopStick right)
{
this.left = left;
this.right = right;
this.name = name;
}
public void Think()
{
left.PutDown(name);
right.PutDown(name);
Console.WriteLine($"{name}在思考");
}
public void Eat()
{
left.TakeUp(name);
right.TakeUp(name);
Console.WriteLine($"{name}在吃饭");
}
public void Run()
{
while(true)
{
Eat();
Thread.Sleep(1000);
Think();
Thread.Sleep(1000);
}
}
}
结果
哲学家0等待拿起筷子
哲学家2等待拿起筷子
哲学家4等待拿起筷子
哲学家3等待拿起筷子
哲学家3拿起第3根筷子
哲学家0拿起第0根筷子
哲学家2拿起第2根筷子
哲学家2等待拿起筷子
哲学家1等待拿起筷子
哲学家4拿起第4根筷子
哲学家4等待拿起筷子
哲学家1拿起第1根筷子
哲学家1等待拿起筷子
哲学家3等待拿起筷子
哲学家0等待拿起筷子