并发队列C#中的线程安全

时间:2017-03-30 12:10:26

标签: c# multithreading

我有一个MessagesManager线程,不同的线程可以向其发送消息,然后这个MessagesManager线程负责在SendMessageToTcpIP()内发布这些消息(MessagesManager线程的起点)。

class MessagesManager : IMessageNotifier
{
    //private
    private readonly AutoResetEvent _waitTillMessageQueueEmptyARE = new AutoResetEvent(false);
    private ConcurrentQueue<string> MessagesQueue = new ConcurrentQueue<string>(); 

    public void PublishMessage(string Message)
    {
        MessagesQueue.Enqueue(Message);
        _waitTillMessageQueueEmptyARE.Set();
    }

    public void SendMessageToTcpIP()
    {
        //keep waiting till a new message comes
        while (MessagesQueue.Count() == 0)
        {
            _waitTillMessageQueueEmptyARE.WaitOne();
        }

        //Copy the Concurrent Queue into a local queue - keep dequeuing the item once it is inserts into the local Queue
        Queue<string> localMessagesQueue = new Queue<string>();

        while (!MessagesQueue.IsEmpty)
        {
            string message;
            bool isRemoved = MessagesQueue.TryDequeue(out message);
            if (isRemoved)
                localMessagesQueue.Enqueue(message);
        }

        //Use the Local Queue for further processing
        while (localMessagesQueue.Count() != 0)
        {
            TcpIpMessageSenderClient.ConnectAndSendMessage(localMessagesQueue.Dequeue().PadRight(80, ' '));
            Thread.Sleep(2000);
        }
    }
}

不同的线程(3-4)通过调用PublishMessage(string Message)(使用与MessageManager相同的对象)发送消息。消息传来后,我将该消息推送到 并发队列 ,并通过设置SendMessageToTcpIP()通知_waitTillMessageQueueEmptyARE.Set();SendMessageToTcpIP()内,我正在从本地队列中的并发队列中复制邮件,然后逐个发布。

问题:以这种方式进行排队和出列是否安全?可能会有一些奇怪的效果吗?

4 个答案:

答案 0 :(得分:3)

虽然这可能是线程安全的,但.NET中有内置的类可以帮助许多发布者和一个消费者#34;模式,如BlockingCollection。您可以像这样重写您的课程:

class MessagesManager : IDisposable {
    // note that your ConcurrentQueue is still in play, passed to constructor
    private readonly BlockingCollection<string> MessagesQueue = new BlockingCollection<string>(new ConcurrentQueue<string>());

    public MessagesManager() {
        // start consumer thread here
        new Thread(SendLoop) {
            IsBackground = true
        }.Start();
    }

    public void PublishMessage(string Message) {
        // no need to notify here, will be done for you
        MessagesQueue.Add(Message);
    }

    private void SendLoop() {
        // this blocks until new items are available
        foreach (var item in MessagesQueue.GetConsumingEnumerable()) {
            // ensure that you handle exceptions here, or whole thing will break on exception
            TcpIpMessageSenderClient.ConnectAndSendMessage(item.PadRight(80, ' '));
            Thread.Sleep(2000); // only if you are sure this is required 
        }
    }

    public void Dispose() {            
        // this will "complete" GetConsumingEnumerable, so your thread will complete
        MessagesQueue.CompleteAdding();
        MessagesQueue.Dispose();
    }
}

答案 1 :(得分:2)

.NET已提供ActionBlock< T>,允许将消息发布到缓冲区并异步处理它们。默认情况下,一次只处理一条消息。

您的代码可以重写为:

//In an initialization function
ActionBlock<string> _hmiAgent=new ActionBlock<string>(async msg=>{
        TcpIpMessageSenderClient.ConnectAndSendMessage(msg.PadRight(80, ' '));
        await Task.Delay(2000);
);

//In some other thread ...
foreach ( ....)
{
    _hmiAgent.Post(someMessage);
}

// When the application closes

_hmiAgent.Complete();
await _hmiAgent.Completion;

ActionBlock提供了许多好处 - 您可以指定它可以在缓冲区中接受的项目数限制,并指定可以并行处理多个消息。您还可以在处理管道中组合多个块。在桌面应用程序中,可以将消息发布到管道以响应事件,由单独的块处理并将结果发布到更新UI的最终块。

例如,

填充可以由中间人TransformBlock< TIn,TOut>执行。这种转变是微不足道的,使用块的成本大于方法,但这只是一个例子:

//In an initialization function
TransformBlock<string> _hmiAgent=new TransformBlock<string,string>(
    msg=>msg.PadRight(80, ' '));

ActionBlock<string> _tcpBlock=new ActionBlock<string>(async msg=>{
        TcpIpMessageSenderClient.ConnectAndSendMessage());
        await Task.Delay(2000);
);

var linkOptions=new DataflowLinkOptions{PropagateCompletion = true};
_hmiAgent.LinkTo(_tcpBlock);

发布代码根本不会改变

    _hmiAgent.Post(someMessage);

当应用程序终止时,我们需要等待_tcpBlock完成:

    _hmiAgent.Complete();
    await _tcpBlock.Completion;

Visual Studio 2015+本身将TPL数据流用于此类场景

Bar Arnon在TPL Dataflow Is The Best Library You're Not Using中提供了一个更好的示例,它显示了如何在块中使用同步和异步方法。

答案 2 :(得分:0)

这是我将如何实现此功能的重构代码片段:

class MessagesManager {
    private readonly AutoResetEvent messagesAvailableSignal = new AutoResetEvent(false);
    private readonly ConcurrentQueue<string> messageQueue = new ConcurrentQueue<string>();

    public void PublishMessage(string Message) {
        messageQueue.Enqueue(Message);
        messagesAvailableSignal.Set();
    }

    public void SendMessageToTcpIP() {
        while (true) {
            messagesAvailableSignal.WaitOne();
            while (!messageQueue.IsEmpty) {
                string message;
                if (messageQueue.TryDequeue(out message)) {
                    TcpIpMessageSenderClient.ConnectAndSendMessage(message.PadRight(80, ' '));
                }
            }
        }
    }
}

此处需要注意的事项:

  1. 这会完全排空队列:如果至少有一条消息,它将处理所有这些消息
  2. 删除了2000ms线程睡眠

答案 3 :(得分:0)

代码 是线程安全的,因为ConcurrentQueueAutoResetEvent都是线程安全的。你的字符串无论如何都被读取而且永远不会被写入,所以这段代码是线程安全的。

但是,您必须确保以某种循环方式呼叫SendMessageToTcpIP 否则,你有一个危险的竞争条件 - 一些消息可能会丢失:

while (!MessagesQueue.IsEmpty)
        {
            string message;
            bool isRemoved = MessagesQueue.TryDequeue(out message);
            if (isRemoved)
                localMessagesQueue.Enqueue(message);
        }

        //<<--- what happens if another thread enqueues a message here?

        while (localMessagesQueue.Count() != 0)
        {
            TcpIpMessageSenderClient.ConnectAndSendMessage(localMessagesQueue.Dequeue().PadRight(80, ' '));
            Thread.Sleep(2000);
        }

除此之外,AutoResetEvent是非常重的对象。它使用内核对象来同步线程。每次通话都是系统调用,可能成本很高。考虑使用用户模式同步对象(不提供某种条件变量?)

相关问题