想象一下,有一个国王和n个仆从提交给他的情况。当国王说'一个人!'#34;时,其中一个仆从说"两个!",但只有一个。也就是说,只有最快的小兵说话,而其他人必须等待国王的另一次召唤。
这是我的尝试:
using System;
using System.Threading;
class Program {
static bool leaderGO = false;
void Leader() {
do {
lock(this) {
//Console.WriteLine("? {0}", leaderGO);
if (leaderGO) Monitor.Wait(this);
Console.WriteLine("> One!");
Thread.Sleep(200);
leaderGO = true;
Monitor.Pulse(this);
}
} while(true);
}
void Follower (char chant) {
do {
lock(this) {
//Console.WriteLine("! {0}", leaderGO);
if (!leaderGO) Monitor.Wait(this);
Console.WriteLine("{0} Two!", chant);
leaderGO = false;
Monitor.Pulse(this);
}
} while(true);
}
static void Main() {
Console.WriteLine("Go!\n");
Program m = new Program();
Thread king = new Thread(() => m.Leader());
Thread minion1 = new Thread(() => m.Follower('#'));
Thread minion2 = new Thread(() => m.Follower('$'));
king.Start();
minion1.Start();
minion2.Start();
Console.ReadKey();
king.Abort();
minion1.Abort();
minion2.Abort();
}
}
预期的输出是这个(#和$代表两个不同的小兵):
> One!
# Two!
> One!
$ Two!
> One!
$ Two!
...
它们出现的顺序并不重要,它是随机的。但问题是,这段代码在编译时会产生这样的代码:
> One!
# Two!
$ Two!
> One!
# Two!
> One!
$ Two!
# Two!
...
也就是说,不止一个小兵在同一时间说话。这会引起更多仆从的骚动,一个国王不应该允许这种干涉。
什么是可能的解决方案?
对于未来的读者,这是最终的工作代码:
using System;
using System.Threading;
class Program {
static AutoResetEvent leader = new AutoResetEvent(false);
static AutoResetEvent follower = new AutoResetEvent(false);
void Leader() {
do {
Console.WriteLine(" One!");
Thread.Sleep(300);
follower.Set(); // Leader allows a follower speak
leader.WaitOne(); // Leader waits for the follower to finish speaking
} while(true);
}
void Follower (char emblem) {
do {
follower.WaitOne(); // Follower waits for the leader to allow speaking
Console.WriteLine("{0} Two!", emblem);
leader.Set(); // Follower finishes speaking
} while(true);
}
static void Main() {
Console.WriteLine("Go!\n");
Program m = new Program();
Thread king = new Thread(() => m.Leader());
Thread minion1 = new Thread(() => m.Follower('#'));
Thread minion2 = new Thread(() => m.Follower('$'));
Thread minion3 = new Thread(() => m.Follower('&'));
king.Start();
minion1.Start();
minion2.Start();
minion3.Start();
Console.ReadKey();
king.Abort();
minion1.Abort();
minion2.Abort();
minion3.Abort();
}
}
答案 0 :(得分:4)
尝试使用AutoResetEvent而不是锁定/监视器。它允许您创建一个“门”,一次只能有一个线程通过。
你的Follower()线程会调用event.WaitOne()
(可选择超时)。您的Leader()函数将调用event.Set()
,这将释放其中一个等待线程。
一旦等待线程通过,AutoResetEvent(与其他类型的等待句柄相对)将自动“关闭门”。
http://msdn.microsoft.com/en-us/library/system.threading.autoresetevent.aspx
答案 1 :(得分:2)
您没有锁定粉丝。所以两个线程都看到领导者是真实的,并做出回应。在写出之前让线程自己锁定,这应该解决它。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
namespace Threading
{
class Program
{
static bool leaderGO = false;
static bool followerGo = false;
void Leader()
{
do
{
lock (this)
{
//Console.WriteLine("? {0}", leaderGO);
if (leaderGO) Monitor.Wait(this);
Console.WriteLine("> One!");
Thread.Sleep(200);
leaderGO = true;
followerGo = true;
Monitor.Pulse(this);
}
} while (true);
}
void Follower(char chant)
{
do
{
lock (this)
{
//Console.WriteLine("! {0}", leaderGO);
if (!leaderGO) Monitor.Wait(this);
if(followerGo)
{
followerGo = false;
Console.WriteLine("{0} Two!", chant);
leaderGO = false;
}
Monitor.Pulse(this);
}
} while (true);
}
static void Main()
{
Console.WriteLine("Go!\n");
Program m = new Program();
Thread king = new Thread(() => m.Leader());
Thread minion1 = new Thread(() => m.Follower('#'));
Thread minion2 = new Thread(() => m.Follower('$'));
king.Start();
minion1.Start();
minion2.Start();
Console.ReadKey();
king.Abort();
minion1.Abort();
minion2.Abort();
}
}
}
答案 2 :(得分:2)
您遇到的是竞争条件。你有两个独立的线程在一个未锁定的资源上运行(leaderGo),它控制着他们对关键部分的访问(打印出“Two!”)。
在打印出“Two!”之前,在leaderGo上放置一个互斥锁(由manman推荐)是一个开始。在打印之前,你还需要检查以确保leaderGo的值仍然是真的,因为两个线程最终将获得锁定,但只有其中一个将获得锁定而leaderGo为真。
类似的东西:
lock(leaderGo)
{
if (leaderGo)
Console.WriteLine("{0} Two!", chant);
leaderGo = false;
}
这将确保只有一个跟随者能够响应(因为它需要锁定)。它不能保证哪个线程获得锁,特定线程获得锁的频率,或类似的东西。但是,在每次传递中,每个线程都将获得锁定 - 重要的是谁是第一个。
答案 3 :(得分:1)
一些提示:
lock(this)
。通过从内部锁定对象,任何使用对象作为锁定焦点的东西都会干扰您自己的代码的同步能力。Thread.Abort()
。这是邪恶的;它通过注入异常来杀死正在运行的线程,这是不可预测的,因此很难或不可能正常捕获和处理。相反,尝试使用布尔属性IsCancelled传递类的实例,并使用!IsCancelled
作为循环的条件。您的代码的实际问题是,如果该线程认为其他人必须先行,那么您的Monitor和锁的组合会导致锁定从获取锁定的线程在关键部分内释放。你有三个线程,每个线程都可以获取,然后释放并等待,然后重新获取锁并继续进行,好像它等待的条件现在是假的。
一种可能的情况:
这些线程有很多种可能的方式可以根据你设置的方式“竞争”。
这可能会有所改善;它被称为双重检查锁定,虽然它不是万无一失的,但它比你拥有的要好得多:
private static readonly object syncObj = new object();
void Leader() {
do {
if(leaderGo)
{
Thread.Sleep(200);
continue;
}
lock(syncObj) {
//the "double-check"; here it's not necessary because there's
//only one King to set leaderGo to true,
//but it doesn't hurt anything.
if(leaderGo) continue;
//we won't get here unless we have control of
//the critical section AND must do something.
Console.WriteLine("> One!");
Thread.Sleep(200);
leaderGO = true;
}
} while(true);
}
void Follower (char chant) {
do {
if(!leaderGo)
{
Thread.Yield();
continue;
}
lock(syncObj) {
//this double-check is critical;
//if we were waiting on the other follower to release
//the lock, they have already shouted out and we must not do so.
if (!leaderGO) continue;
//we only get here if we have
//control of the lock and should shout out
Console.WriteLine("{0} Two!", chant);
leaderGO = false;
}
} while(true);
}
编辑:正如评论中所提到的,这个模型并不依赖于运气,但它并非万无一失,因为.NET为了提高性能,可以允许多个leaderGO副本存在于各种线程的缓存,并在后台同步它们。如果.NET不是那个同步的johnny-on-the-spot,那么一个线程执行的仔细检查可能会看到标志的旧的“陈旧”状态,并且当它应该转出时错误地继续运行。
您可以通过以下两种简单方法之一来解决这个问题:
volatile
。 .NET无法优化volatile变量;它保证在内存中的一个位置可以被任何运行该代码的线程访问,但效率低下。因此,任何其他线程都会立即看到对其值的任何更新。