交替线程#2

时间:2012-11-20 22:18:18

标签: c# multithreading

想象一下,有一个国王和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();
    }
}

4 个答案:

答案 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和锁的组合会导致锁定从获取锁定的线程在关键部分内释放。你有三个线程,每个线程都可以获取,然后释放并等待,然后重新获取锁并继续进行,好像它等待的条件现在是假的

一种可能的情况:

  • 追随者1进入追随者的关键部分(锁定()块)。
  • 追随者2接近追随者的关键部分并被告知等待。
  • King接近Leader的关键部分并被告知等待。
  • 追随者1看到leaderGO是假的并等待,释放了关键部分的锁定。
  • King,尽管排在第二位,但在“追随者2”之前“进入”关键部分。
  • King继续(领队Go是假的,所以King从不等待()s),称之为“One!”并在临界区末尾释放锁之前设置标志。
  • 追随者2现在“跟随”进入追随者1之前的关键部分,看到标志已设置,并继续,呼叫“两个!”退出关键部分。
  • 追随者1现在转弯,重新获得其关键部分中间的锁定。 它不再关心领导者是假的;它已经过了那个检查。所以,它继续,也称为“两个!”,设置标志(它已经是的值)并退出。

这些线程有很多种可能的方式可以根据你设置的方式“竞争”。

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

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变量;它保证在内存中的一个位置可以被任何运行该代码的线程访问,但效率低下。因此,任何其他线程都会立即看到对其值的任何更新。