错过了来自Monitor.PulseAll的通知

时间:2017-03-15 01:45:28

标签: c# multithreading

我尝试使用Monitor.PulaseAll来通知用户线程新数据,但结果显示甚至没有一个线程成功接收到每个新数据通知。

public class CTest
{
    CDataProducer m_DataProducer = new CDataProducer();

    public void startTest()
    {
        for (int i = 0; i < 5; i++)
        {
            Thread th = new Thread(userThead);
            th.Name = "Thread" + (i + 1).ToString();
            th.Start();
        }

        for (int i = 0; i < 7; i++)
        {
            m_DataProducer.ProduceNewData();
            Thread.Sleep(1000);
        }

        DateTime dtBeg = DateTime.Now;
        while (true)
        {
            if ((DateTime.Now - dtBeg).TotalSeconds >= 10)
            {
                break;
            }
            Thread.Sleep(1);
        }

    }

    private void userThead()
    {
        int cProcessed = 0;
        while (true)
        {
            Thread.Sleep(1);

            double dData;
            if (m_DataProducer.WaitData(out dData))
            {
                Debug.WriteLine(Thread.CurrentThread.Name + ":" + dData.ToString());
                cProcessed++;
                if (dData >= 5)
                {
                    Debug.WriteLine(Thread.CurrentThread.Name + ": Exited at " + dData.ToString() + ", N=" + cProcessed.ToString());
                    break;
                }
            }
        }
    }

    [Fact]
    public void TEST_pulseall()
    {
        CTest test = new CTest();
        test.startTest();
    }
}


class CDataProducer
{
    private object m_NewObj = new object();

    private object m_LockData = new object();
    private double m_Data_d = 0;

    public CDataProducer()
    {
    }

    public void ProduceNewData()
    {
        lock (m_NewObj)
        {
            if (Monitor.TryEnter(m_LockData, 5000))
            {
                m_Data_d++;
                Monitor.Exit(m_LockData);
                Monitor.PulseAll(m_NewObj);
            }
        }
    }
    public bool WaitData(out double dData)
    {
        bool bRet = false;

        dData = 0;

        lock (m_NewObj)
        {
            try
            {
                if (Monitor.Wait(m_NewObj, 1))
                {
                    if (Monitor.TryEnter(m_LockData, 5000))
                    {
                        dData = m_Data_d;
                        Monitor.Exit(m_LockData);
                    }
                    //Monitor.PulseAll(m_NewObj);
                    bRet = true;
                }
            }
            finally
            {
            }
        }

        return bRet;
    }
}

我的一个结果显示:

Thread2:1
Thread3:1
Thread1:1
Thread1:2
Thread4:2
Thread5:4
Thread3:4
Thread4:4
Thread1:4
Thread4:6
Thread4: Exited at 6, N=3
thread  0x325c code 259 (0x103)  terminated.
Thread2:7
Thread2: Exited at 7, N=2

线程2仅处理数据&#34; 1&#34;。 并且线程1进程&#34; 1&#34;,&#34; 2&#34;,&#34; 4&#34;,它错过了&#34; 3&#34;和&#34; 5&#34;。 显然大多数数据都被遗漏了。

我的事件在每个新数据生成后延迟1秒,以便为用户提供时间以便能够接收通知和处理数据。

有什么不对吗?

2 个答案:

答案 0 :(得分:2)

  

有什么不对吗?

嗯,似乎根据你的问题,有。 :)这取决于你真正想要的行为,但我收集到你并没有得到你想要的东西。所以,是的......那可能有资格作为&#34;错误&#34;。 :)

来自the documentation

  

Monitor类不维护指示已调用Pulse方法的状态。因此,如果在没有线程等待时调用Pulse,则调用Wait块的下一个线程就像从未调用过Pulse一样。

这里的问题是你没有做任何事情来确保生产者只有当消费者等待时才调用Pulse(),更重要的是,确保它在产生下一个数据之前等待当前数据被消耗元件。

很难确切地知道你真正想要的是什么。您发布的代码,即使被视为&#34; work&#34;,因为所有数据元素都被观察到,仍然看起来有点奇怪,因为您有五个消费者,所有消费者将使用相同的数据元素。也就是说,根据这种理解,这里的代码版本是&#34;工作&#34;:

class CDataProducer
{
    private readonly object _writeMonitor = new object();
    private readonly object _readMonitor = new object();
    private readonly object _lock = new object();

    private double _data;

    public bool IsDone { get; private set; }

    public void ProduceNewData()
    {
        lock (_writeMonitor)
        {
            if (!IsDone)
            {
                lock (_lock)
                {
                    _data++;
                }
                lock (_readMonitor) Monitor.PulseAll(_readMonitor);
                Monitor.Wait(_writeMonitor);
            }
        }
    }

    public bool WaitData(out double result)
    {
        lock (_readMonitor)
        {
            Monitor.Wait(_readMonitor);
            lock (_lock)
            {
                result = _data;
            }
            lock (_writeMonitor) Monitor.Pulse(_writeMonitor);
        }

        return true;
    }

    public void Done()
    {
        lock (_writeMonitor)
        {
            IsDone = true;
            Monitor.Pulse(_writeMonitor);
        }
    }
}

public static void startTest()
{
    for (int i = 0; i < 5; i++)
    {
        Thread th = new Thread(userThead);
        th.Name = "Thread" + (i + 1).ToString();
        th.IsBackground = true;
        th.Start();
    }

    Func<Task> waitAndDone = async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(10));
        m_DataProducer.Done();
    };

    Task waitAndDoneTask = waitAndDone();

    for (int i = 0; i < 7 && !m_DataProducer.IsDone; i++)
    {
        m_DataProducer.ProduceNewData();
        Thread.Sleep(1000);
    }

    waitAndDoneTask.Wait();
}

我对userThead()方法没有做任何更改,所以没有把它包含在上面。

注意:

  • 这里的主要变化是有两个不同的监视器,这样生产者和消费者可以相互来回发信号。在写了一个价值之后,生产者不会继续进行,直到它被消费者发出信号(从技术上讲,它应该等到所有消费者已经发出信号,但是这有效并且#34;足够好& #34;为了说明的目的)。
  • 我还以两种方式修改了终止条件。首先,使用定时器而不是轮询时钟来完成延迟。其次,因为生产者将阻塞直到它发出信号,部分终止不仅涉及设置一个标志来告诉生产者循环停止,而且还要通知监视器将其唤醒以便它可以检查该标志。 / LI>

更典型的生产者/消费者场景,即使涉及多个消费者的生产者/消费者场景,每个生成的元素也仅由一个消费者消费。同时,通常生产者和消费者不会被迫以锁定步骤操作。相反,队列将充当缓冲区,以允许消费者独立于消费者生产,并允许消费者在提供新数据元素时使用它们。

这是生产者代码的一种版本:

class CDataProducer
{
    public bool IsDone { get; private set; }

    private double _data;
    private readonly Queue<double> _values = new Queue<double>();
    private readonly object _lock = new object();

    public void Done()
    {
        IsDone = true;
    }

    public void ProduceNewData()
    {
        lock (_lock)
        {
            _values.Enqueue(++_data);
            Monitor.Pulse(_lock);
        }
    }

    public bool WaitData(out double value)
    {
        lock (_lock)
        {
            while (_values.Count == 0)
            {
                Monitor.Wait(_lock);
            }

            value = _values.Dequeue();

            return true;
        }
    }
}

您可能会注意到这种方法实际上要简单得多。该队列不需要生产者和消费者交替执行,因此只需要一个监视器(而不是另一个版本中的三个)。而且由于生产者不等待,所以不需要发出信号只是为了关闭。

当然,在终止时,队列中仍然存在元素,消费者仍在运行。同样,上述内容足以说明,但真实场景可能包含解决此问题的其他逻辑。碰巧,.NET提供了一个内置类,可以非常优雅地处理所有这些,称为BlockingCollection<T>

以下是原始代码的一些不同变体,说明了如何使用它:

class CDataProducer
{
    private readonly BlockingCollection<double> _queue = new BlockingCollection<double>();
    private double _value;

    public bool IsDone { get; private set; }

    public void Done()
    {
        IsDone = true;
        _queue.CompleteAdding();
    }

    public void ProduceNewData()
    {
        _queue.Add(++_value);
    }

    public void Consumer()
    {
        foreach (double value in _queue.GetConsumingEnumerable())
        {
            Console.WriteLine(Thread.CurrentThread.Name + ":" + value);
        }
    }
}

public static void startTest()
{
    CDataProducer dataProducer = new CDataProducer();

    for (int i = 0; i < 5; i++)
    {
        Thread th = new Thread(dataProducer.Consumer);
        th.Name = "Thread" + (i + 1).ToString();
        th.Start();
    }

    Func<Task> waitAndDone = async () =>
    {
        await Task.Delay(TimeSpan.FromSeconds(10));
        dataProducer.Done();
    };

    Task waitAndDoneTask = waitAndDone();

    for (int i = 0; i < 7 && !dataProducer.IsDone; i++)
    {
        dataProducer.ProduceNewData();
        Thread.Sleep(1000);
    }

    waitAndDoneTask.Wait();
}

static void Main(string[] args)
{
    startTest();
}

在上面,我甚至不使用你写的消费者方法。相反,我向Consumer()类添加了CDataProducer方法,该方法处理消耗。

这里的关键是GetConsumingEnumerable()方法。这将返回一个IEnumerable<T>实例,该实例将阻塞直到数据可用,并且将在集合对象上调用CompleteAdding()方法时终止。

如果你不想在CDataProducer类本身中使用消费者代码,那么为该类添加一个方法以返回GetConsumingEnumerable()结果就足够了,所以外面的代码该类可以实现相同的循环。

(注意:上面的代码省略了一些通常很重要的并发结构。例如,IsDone属性没有使用声明为volatile的字段。在上面的代码中,在英特尔x86架构上运行,它不是必需的,但严格来说,如果没有它,代码就不正确。在上面,我试图只关注手头的具体任务,即对生产者的讨论/您最初发布的代码上下文中的消费者模式。)

答案 1 :(得分:0)

如果你没有等到它的话,你会错过Monitor.Pulse

您需要锁定m_NewObj,检查数据是否已准备好标记或计数,然后等待,如果没有准备好。同时,您的数据生成器将锁定m_NewObj,设置标志/计数,然后设置Pulse。如果您的消费者在等待,它会醒来并看到数据。如果它没有等待,当它接下来检查时,数据就会准备就绪。

我建议您使用SemaphoreSlim进行此类消费者 - 生产者线程同步而不是Monitor,这样更简单。