Semaphoreslim.Wait(0)(防止多次执行)导致不执行的可能性

时间:2015-05-15 19:52:56

标签: c# asynchronous async-await semaphore

我不确定的情况涉及使用"线程安全" PipeStream,其中多个线程可以添加要写入的消息。如果没有要写入的消息队列,则当前线程将开始写入读取方。如果有一个队列,并且在管道写入时队列增长,我希望开始写入的线程耗尽队列。

我"希望"这种设计(如下所示)阻止了SemaphoreSlim的连续进入/释放并减少了预定的任务数量。我说"希望"因为我应该测试这种并发症是否有任何积极的性能影响。但是,在测试之前我应该​​首先了解代码是否符合我的想法,所以请考虑以下类,并在其下面考虑一系列事件;

注意:我理解任务的执行与任何特定线程无关,但我发现这是解释的最简单方法。

class SemaphoreExample
{
    // Wrapper around a NamedPipeClientStream
    private readonly MessagePipeClient m_pipe =
        new MessagePipeClient("somePipe");

    private readonly SemaphoreSlim m_semaphore =
        new SemaphoreSlim(1, 1);

    private readonly BlockingCollection<Message> m_messages =
        new BlockingCollection<Message>(new ConcurrentQueue<Message>());

    public Task Send<T>(T content)
        where T : class
    {
        if (!this.m_messages.TryAdd(new Message<T>(content)))
            throw new InvalidOperationException("No more requests!");

        Task dequeue = TryDequeue();

        return Task.FromResult(true);
        // In reality this class (and method) is more complex.
        // There is a similiar pipe (and wrkr) in the other direction.
        // The "sent jobs" is kept in a dictionary and this method
        // returns a task belonging to a completionsource tied
        // to the "sent job". The wrkr responsible for the other
        // pipe reads a response and sets the corresponding
        // completionsource.
    }

    private async Task TryDequeue()
    {
        if (!this.m_semaphore.Wait(0))
            return; // someone else is already here

        try
        {
            Message message;
            while (this.m_messages.TryTake(out message))
            {
                await this.m_pipe.WriteAsync(message);
            }
        }
        finally { this.m_semaphore.Release(); }
    }
}
  1. Wrkr1完成写入管道。 (在TryDequeue中)
  2. Wrkr1确定队列为空。 (在TryDequeue中)
  3. Wrkr2将项目添加到队列中。 (发送中)
  4. Wrkr2确定Wrkr1占用信号量,返回。 (发送中)
  5. Wrkr1发布了Semaphore。 (在TryDequeue中)
  6. 队列中剩下的1个项目在x时间内不会被执行。
  7. 这一系列活动是否可行?我是否应该完全忘记这个想法,并且每次都要打电话给&#34;发送&#34;等待#34; TryDeque&#34;和它内的信号量?也许每个方法调用安排另一个任务的潜在性能影响可以忽略不计,即使是在一个&#34;高&#34;频率。

    更新

    按照 Alex 的建议,我正在做以下事情; 让&#34;发送&#34;的来电者指定&#34; maxWorkload&#34;一个整数,指定在将工作委托给另一个线程来处理任何额外工作之前调用者准备做多少项(对于其他调用者,在最坏的情况下)。但是,在创建新主题之前,其他来电者是&#34;发送&#34;有机会进入信号量,从而可能阻止使用额外的线程。

    为了不让任何工作留在队列中,任何成功进入信号量并做了一些工作的工作人员必须检查退出信号量后是否添加了新工作。如果这是真的,那么同一个工人将尝试重新输入(如果&#34; maxWorkload&#34;未达到)或委托上述工作。

    以下示例:现在发送设置&#34; TryPool&#34;作为&#34; TryDequeue&#34;的延续。 &#34; TryPool&#34;只有在&#34; TryDequeue&#34;返回true(即在输入信号量时做了一些工作)。

        // maxWorkload cannot be -1 for this method
        private async Task<bool> TryDequeue(int maxWorkload)
        {
            int currWorkload = 0;
            while (this.m_messages.Count != 0 && this.m_semaphore.Wait(0))
            {
                try
                {
                    currWorkload = await Dequeue(currWorkload, maxWorkload);
                    if (currWorkload >= maxWorkload)
                        return true;
                }
                finally
                {
                    this.m_semaphore.Release();
                }
            }
            return false;
        }
    
        private Task TryPool()
        {
            if (this.m_messages.Count == 0 || !this.m_semaphore.Wait(0))
                return Task<bool>.FromResult(false);
    
            return Task.Run(async () =>
            {
                do
                {
                    try
                    {
                        await Dequeue(0, -1);
                    }
                    finally
                    {
                        this.m_semaphore.Release();
                    }
                }
                while (this.m_messages.Count != 0 && this.m_semaphore.Wait(0));
            });
        }
    
        private async Task<int> Dequeue(int currWorkload, int maxWorkload)
        {
            while (currWorkload < maxWorkload || maxWorkload == -1)
            {
                Message message;
                if (!this.m_messages.TryTake(out message))
                    return currWorkload;
    
                await this.m_pipe.WriteAsync(message);
    
                currWorkload++;
            }
            return maxWorkload;
        }
    

1 个答案:

答案 0 :(得分:2)

我倾向于将这种模式称为“GatedBatchWriter”,即通过门的第一个线程处理一批任务;它自己和其他一些代表其他作家,直到它做了足够的工作。

由于与该工作相关的开销,当批处理工作更有效时,此模式主要是有用的。例如。将较大的块一次写入磁盘而不是多个小块。

是的,这个特定的模式有一个特定的竞争条件需要注意:“负责的作家”,即通过大门的那个,确定队列中没有更多的消息并在释放信号量之前停止(即它的写作责任)。第二位作家到达并在这两个决策点之间未能获得写作责任。现在队列中有一条消息将不会被传递(或者在下一位作者到达时传递得很晚)。

此外,就调度而言,您现在正在做的事情并不公平。如果有很多消息,那么队列可能永远不会是空的,并且通过门的作者将忙于代表其他人永久地写消息。您需要限制负责编写者的批量大小。

您可能想要改变的其他一些事情是:

  1. Message包含任务完成令牌。
  2. 让无法获得写入责任的作者将他们的消息排入队列并等待两个任务完成中的任何一个:与他们的消息相关联的任务完成,以及释放写入责任。
  3. 让负责的编写者为其处理的邮件设置完成。
  4. 让负责的作家在完成足够的工作后释放它的责任。
  5. 当等待的作家被两个任务完成之一唤醒时:
    • 如果是由于其消息上的完成令牌,它可以采用它的快乐方式。
    • 否则,尝试获得写作责任,冲洗,重复......
  6. 还有一点需要注意:如果有很多消息,即平均高消息负载,处理队列的专用线程/长时间运行任务通常会有更好的性能。