4

想象这样一种情况,有一个国王和 n 个仆从提交给他。当国王说“一!”时,其中一个仆从说“二!”,但只有一个。也就是说,只有最快的仆从说话,而其他仆从必须等待国王的另一次召唤。

这是我的尝试:

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();
    }
}
4

4 回答 4

4

尝试使用 AutoResetEvent 而不是锁/监视器。它允许您创建一个“门”,一次只能通过一个线程。

您的 Follower() 线程将调用event.WaitOne()(可选超时)。您的 Leader() 函数将调用event.Set(),这将释放其中一个等待线程。

一旦等待线程通过,AutoResetEvent(与其他类型的等待句柄相反)将自动“关闭门”。

http://msdn.microsoft.com/en-us/library/system.threading.autoresetevent.aspx

于 2012-11-20T22:23:58.207 回答
2

你没有锁定追随者。所以两个线程都看到 leaderGo 是真的,并且响应。在写出之前让线程自己锁定,这应该可以解决它。

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();
    }
}

}

于 2012-11-20T22:35:55.050 回答
2

你正在经历的是一个竞争条件。你有两个独立的线程在一个未锁定的资源(leaderGo)上运行,它控制着它们对临界区的访问(打印出“Two!”)。

在打印出“两个!”之前,在 leaderGo 上放置一个互斥锁(由 manman 推荐)是一个开始。您还需要在打印之前检查以确保 leaderGo 的值仍然为真,因为两个线程最终都会获得锁,但只有一个线程会在 leaderGo 为真时获得锁。

就像是:

lock(leaderGo)
{
     if (leaderGo)
         Console.WriteLine("{0} Two!", chant);
     leaderGo = false;
}

这将确保只有一个追随者能够响应(因为它需要锁定)。它不会保证哪个线程获得锁,特定线程获得锁的频率,或类似的东西。然而,在每一次通过中,每个线程都会获得锁——重要的是谁是第一个。

于 2012-11-20T22:40:40.383 回答
1

一些提示:

  • 永远不要使用lock(this). 通过从内部锁定对象,任何使用你的对象作为锁定焦点的东西都会干扰你自己代码的同步能力。
  • 永远不要使用Thread.Abort(). 这是邪恶的;它通过注入异常来杀死正在运行的线程,这是不可预测的,因此很难或不可能优雅地捕获和处理。相反,尝试传递具有布尔属性 IsCancelled 的类的实例,并将其!IsCancelled用作保持循环的条件。

您的代码的实际问题是您的监视器和锁的组合导致锁被获取锁的线程从临界区释放,如果该线程认为其他人必须先行。您有三个线程,每个线程都可以获取,然后释放并等待,然后重新获取锁并继续进行,就好像它等待的条件现在是 false 一样

一种可能的情况:

  • Follower 1 进入 Follower 的临界区(lock() 块)。
  • Follower 2 接近 Follower 的临界区并被告知等待。
  • King接近Leader的关键部分,并被告知等待。
  • Follower 1 看到 leaderGO 为 false 并等待,释放临界区的锁。
  • King,尽管排在第二位,但在 Follower 2 之前“竞速”进入了关键部分。
  • King 继续(leaderGo 为假,因此 King 永远不会 Wait()s),调用“One!” 并在关键部分结束时释放锁之前设置标志。
  • 追随者 2 现在在追随者 1 之前“竞速”进入临界区,看到标志已设置,然后继续喊“二!” 并退出临界区。
  • 追随者 1 现在开始转弯,重新获得关键部分中间的锁。它不再关心 leaderGo 是否为 false;它已经通过了检查。因此,它继续,也调用“Two!”,设置标志(设置为它已经存在的值)并退出。

根据您的设置方式,这些线程可以通过多种可能的方式“竞争”。

这可能会更好一些;它被称为双重检查锁定,虽然它不是万无一失的,但它比你拥有的要好得多:

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 与该同步,则由一个线程执行的双重检查可能会看到标志的旧的“陈旧”状态,并在它应该退出时错误地继续前进。

您可以通过以下两种简单方法之一来解决此问题:

  • 在任何更新 leaderGO 之后和任何读取 leaderGO 之前放置一个 MemoryBarrier。内存屏障,或者用其他语言称为“内存栅栏”,基本上将每个正在运行的线程阻塞在内存屏障上,直到所有线程都处于内存屏障(或以其他方式阻塞),确保所有指令发生在内存屏障之前内存屏障在运行后的任何指令之前已被执行。
  • 将 leaderGO 声明为volatile. .NET 无法优化 volatile 变量;它保证在内存中的一个位置,任何运行该代码的线程都可以访问,但是效率很低。因此,任何其他线程都会立即看到对其值的任何更新。
于 2012-11-20T22:50:20.290 回答