结合时间紧密的事件,提高效率和公平性

时间:2013-02-20 15:32:22

标签: c# concurrency

我有一堆线程可以生成A类型的事件并输入B

我的程序接收这些事件,将它们包装在一条消息中并通过网络发送。邮件可以包含一个A个活动,一个B个活动或一个A个活动以及一个B个活动:

SendMessage(new Message(a: 1,    b: null));
SendMessage(new Message(a: null, b: 2   ));
SendMessage(new Message(a: 3,    b: 4   ));

A类型的事件经常发生,而B类型的事件发生的次数则少得多。因此,当一个线程生成一个B事件时,我的程序会稍等一下,看看另一个线程是否生成A事件,并将A事件与B事件结合起来。可能的。

这是我的代码:

object gate = new object();
int? pendingB;

Message WrapA(int a, int millisecondsTimeout)
{
    int? b;

    lock (gate)
    {
        b = pendingB;
        pendingB = null;
        Monitor.Pulse(gate);
    }

    return new Message(a, b);
}

Message WrapB(int b, int millisecondsTimeout)
{
    lock (gate)
    {
        if (pendingB == null)
        {
            pendingB = b;
            Monitor.Wait(gate, millisecondsTimeout);
            if (pendingB != b) return null;
            pendingB = null;
        }
    }

    return new Message(null, b);
}

到目前为止这是有效的。但是,有两个问题:

  • 如果有大量A个事件和大量B个事件,则算法效率不高:B事件中只有一定比例附加到A个事件1}}事件,即使有足够的A事件。

  • 如果一段时间内没有生成A个事件(不常见,但并非不可能),则该算法完全不公平:生成B个事件的一个线程必须每次都等待,而所有其他线程可以立即发送他们的B事件。

如何提高算法的效率和公平性?

<子> 限制:
WrapAWrapB必须在短暂的确定时间内终止 •必须在任何锁定之外调用SendMessage •除gate之外,没有可用的同步机制 •没有其他线程,任务,计时器等可用 •由于A类型的事件在正常情况下经常发生,因此WrapB中的忙等待是正常的。


这是一个可用作基准的测试程序:

public static class Program
{
    static int counter0 = 0;
    static int counterA = 0;
    static int counterB = 0;
    static int counterAB = 0;

    static void SendMessage(Message m)
    {
        if (m != null)
            if (m.a != null)
                if (m.b != null)
                    Interlocked.Increment(ref counterAB);
                else
                    Interlocked.Increment(ref counterA);
            else
                if (m.b != null)
                    Interlocked.Increment(ref counterB);
                else
                    Interlocked.Increment(ref counter0);
    }

    static Thread[] Start(int threadCount, int eventCount,
        int eventInterval, int wrapTimeout, Func<int, int, Message> wrap)
    {
        Thread[] threads = new Thread[threadCount * eventCount];
        for (int i = 0; i < threadCount; i++)
        {
            for (int j = 0; j < eventCount; j++)
            {
                int k = i * 1000 + j;
                int l = j * eventInterval + i;
                threads[i * eventCount + j] = new Thread(() =>
                {
                    Thread.Sleep(l);
                    SendMessage(wrap(k, wrapTimeout));
                });
                threads[i * eventCount + j].Start();
            }
        }
        return threads;
    }

    static void Join(params Thread[] threads)
    {
        for (int i = 0; i < threads.Length; i++)
        {
            threads[i].Join();
        }
    }

    public static void Main(string[] args)
    {
        var wrapper = new MessageWrapper();
        var sw = Stopwatch.StartNew();

        // Only A events
        var t0 = Start(10, 40, 7, 1000, wrapper.WrapA);
        Join(t0);

        // A and B events
        var t1 = Start(10, 40, 7, 1000, wrapper.WrapA);
        var t2 = Start(10, 10, 19, 1000, wrapper.WrapB);
        Join(t1);
        Join(t2);

        // Only B events
        var t3 = Start(10, 20, 7, 1000, wrapper.WrapB);
        Join(t3);

        Console.WriteLine(sw.Elapsed);

        Console.WriteLine("0:  {0}", counter0);
        Console.WriteLine("A:  {0}", counterA);
        Console.WriteLine("B:  {0}", counterB);
        Console.WriteLine("AB: {0}", counterAB);

        Console.WriteLine("Generated A: {0}, Sent A: {1}",
            10 * 40 + 10 * 40, counterA + counterAB);
        Console.WriteLine("Generated B: {0}, Sent B: {1}",
            10 * 10 + 10 * 20, counterB + counterAB);
    }
}

14 个答案:

答案 0 :(得分:7)

为了它的乐趣,这是一个无锁实现:

public sealed class MessageWrapper
{
    private int pendingB;

    public Message WrapA(int a, int millisecondsTimeout)
    {
        int b = Interlocked.Exchange(ref pendingB, -1);
        return new Message(a, b == -1 ? null : b);
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        var sw = new SpinWait();
        while (Interlocked.CompareExchange(ref pendingB, b, -1) != -1)
        {
            // Spin
            sw.SpinOnce();

            if (sw.NextSpinWillYield)
            {
                // Let us make progress instead of yielding the processor
                // (avoid context switch)
                return new Message(null, b);
            }
        }

        return null;
    }
}

<强>结果

原始实施:

00:00:02.0433298
0:  0
A:  733
B:  233
AB: 67
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

无锁实施:

00:00:01.2546310
0:  0
A:  717
B:  217
AB: 83
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

<强>更新

不幸的是,上面的实现有一个bug加上一些缺点。这是一个改进版本:

public class MessageWrapper
{
    private int pendingB = EMPTY;
    private const int EMPTY = -1;

    public Message WrapA(int a, int millisecondsTimeout)
    {
        int? b;
        int count = 0;
        while ((b = Interlocked.Exchange(ref pendingB, EMPTY)) == EMPTY)
        {
            if (count % 7 == 0)
            {
                Thread.Sleep(0);
            }
            else if (count % 23 == 0)
            {
                Thread.Sleep(1);
            }
            else
            {
                Thread.Yield();
            }
            if (++count == 480)
            {
                return new Message(a, null);
            }
        }
        return new Message(a, b);
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        int count = 0;
        while (Interlocked.CompareExchange(ref pendingB, b, EMPTY) != EMPTY)
        {
            // Spin
            Thread.SpinWait((4 << count++));
            if (count > 10)
            {
                // We didn't manage to place our payload.
                // Let's send it ourselves:
                return new Message(null, b);
            }
        }

        // We placed our payload. 
        // Wait some more to see if some WrapA snatches it.
        while (Interlocked.CompareExchange(ref pendingB, EMPTY, EMPTY) == b)
        {
            Thread.SpinWait((4 << count++));
            if (count > 20)
            {
                // No WrapA came along. Pity, we will have to send it ourselves
                int payload = Interlocked.CompareExchange(ref pendingB, EMPTY, b);
                return payload == b ? new Message(null, b) : null;
            }
        }
        return null;
    }
}

结果:

OP的实施

00:00:02.1389474
0:  0
A:  722
B:  222
AB: 78
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

第二次实施:

00:00:01.2752425
0:  0
A:  700
B:  200
AB: 100
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

答案 1 :(得分:5)

为了多样性,我尝试了一种基于并发集合的方法。对我来说,从发布的限制中不清楚这是否可以,但无论如何我都会回答我的答案:

这是我机器上原始代码的典型输出:

00:00:01.7835426
0:  0
A:  723
B:  223
AB: 77
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

这是我建议的典型输出,比原始代码慢了约20%,但它捕获了更多“AB”消息:

00:00:02.1322512
0:  0
A:  701
B:  201
AB: 99
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

MessageWrapper实现:

public class MessageWrapper
{
    private BlockingCollection<int?> messageA = new BlockingCollection<int?>();
    private BlockingCollection<int?> messageB = new BlockingCollection<int?>();

    public Message WrapA(int a, int millisecondsTimeout)
    {
        messageA.Add(a);
        return CreateMessage(0);
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        messageB.Add(b);
        return CreateMessage(millisecondsTimeout);
    }

    private Message CreateMessage(int timeout)
    {
        int? a, b;

        if (messageB.TryTake(out b) | messageA.TryTake(out a, timeout))
        {
            return new Message(a, b);
        }
        else
        {
            return null;
        }
    }
}

答案 2 :(得分:1)

似乎是Reactive Extesions的完美候选人。您可以使用Buffer方法对事件或其他类似扩展进行分组,以过滤和合并事件。

也许此解决方案与您的约束条件不匹配,但在我看来,这是最佳解决方案。 Reactive Extensions非常强大。

答案 3 :(得分:1)

我将给出另一个建议,即更严格地遵循给定的约束条件;在我的机器上,此实现在运行测试程序时始终捕获97条或更多“AB”消息,原始代码的性能降低约5%:

class MessageWrapper
{
    object gate = new object();
    int? pendingB;

    public Message WrapA(int a, int millisecondsTimeout)
    {
        Message returnMessage = null;
        bool lockTaken = false;

        Monitor.TryEnter(gate, 100, ref lockTaken);

        if (lockTaken)
        {
            returnMessage = new Message(a, pendingB);

            pendingB = null;
            Monitor.Pulse(gate);

            Monitor.Exit(gate);
        }
        else
        {
            returnMessage = new Message(a, null);
        }

        return returnMessage;
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        Message returnMessage = null;
        bool lockTaken = false;

        Monitor.TryEnter(gate, 100, ref lockTaken);

        if (lockTaken)
        {
            if (pendingB != null)
            {
                Monitor.Wait(gate, 100);
            }

            if (pendingB != null)
            {
                returnMessage = new Message(null, b);
            }
            else
            {
                pendingB = b;

                if (!Monitor.Wait(gate, millisecondsTimeout))
                {
                    pendingB = null;
                    Monitor.Pulse(gate);
                    returnMessage = new Message(null, b);
                }
            }

            Monitor.Exit(gate);
        }
        else
        {
            returnMessage = new Message(null, b);
        }

        return returnMessage;
    }
}

这里发生的事情与原始代码基本相同,但我们也在等待已有 pendingB 对象而不只是返回“B”消息时。这样可以以较低的性能成本改善我们可以找到的“AB”消息量。

它看起来有点乱,但主要是因为我选择使用更实时友好的构造 Monitor.TryTake 而不是原始的 lock 。此外,拥有一个 return 语句是一个巧妙的技巧,可以避免在调用 Monitor.Exit 之前意外返回死锁。

摆弄各种超时可能会以准确性为代价提高性能,反之亦然。 100毫秒是我对所有人的初步猜测,至少在我的机器上它看起来不错。


最后请注意,在 WrapB 的实现中,我们可以更改行

            if (pendingB != null)
            {
                Monitor.Wait(gate, 100);
            }

            while (pendingB != null)
            {
                Monitor.Wait(gate, 100);
            }

获得100%的准确度,但它严重地混淆了测试程序中的指标,因为它同步了'B'事件,当只有'B'消息流时,这些事件显然表现得非常糟糕。

如果我删除 t3 测试,则比原始代码运行大约5% ,同时始终在100条'AB'消息中找到100条消息。但是运行时当然不再具有确定性,因为我们无法判断我们将绕循环旋转多少次。

编辑:

好吧,除非我们做类似的事情

            int spinCount = 0;

            while (pendingB != null && spinCount < 5)
            {
                spinCount++;
                Monitor.Wait(gate, 100);
            }

这将给我们一个等待时间的上限。当我们只有一个“B”消息流时,它确实解决了性能问题,并且在与原始代码大致相同的时间运行,同时始终找到100条“AB”消息中的100条。

答案 4 :(得分:1)

好的,所以我尝试创建一个快速A和AB然后慢一个B.这意味着我的整体时间较慢(主要是因为只有b的流),但是合并时间和一个只有时间是快点。结果如下:

A's only: 00:00:00.3975499
Combine: 00:00:00.4234934
B's only: 00:00:02.0079422
Total: 00:00:02.8314751
0:  0
A:  700
B:  200
AB: 100
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300

以下是代码:

    class MessageWrapper
    {
        object bMessageLock = new object();
        object pendingBLock = new object();
        int? pendingB;

        ManualResetEvent gateOpen = new ManualResetEvent(true); // Gate is open initially.


        private bool IsGateOpen()
        {
            return gateOpen.WaitOne(0);
        }

        private void OpenGate()
        {
            gateOpen.Set();
        }

        private void CloseGate()
        {
            gateOpen.Reset();
        }


        public Message WrapA(int a, int millisecondsTimeout)
        {
            // check if the gate is open. Use WaitOne(0) to return immediately.
            if (IsGateOpen())
            {
                return new Message(a, null);
            }
            else
            {
                // This extra lock is to make sure that we don't get stale b's.
                lock (pendingBLock)
                {
                    // and reopen the gate.
                    OpenGate();

                    // there is a waiting b
                    // Send combined message
                    var message = new Message(a, pendingB);

                    pendingB = null;

                    return message;
                }
            }
        }

        public Message WrapB(int b, int millisecondsTimeout)
        {

            // Remove this if you don't have overlapping B's
            var timespentInLock = Stopwatch.StartNew();

            lock (bMessageLock) // Only one B message can be sent at a time.... may need to fix this.
            {
                pendingB = b;

                // Close gate
                CloseGate();


                // Wait for the gate to be opened again (meaning that the message has been sent)
                if (timespentInLock.ElapsedMilliseconds < millisecondsTimeout && 
                    gateOpen.WaitOne(millisecondsTimeout - (int)timespentInLock.ElapsedMilliseconds)) 
                // If you don't have overlapping b's use this clause instead.
                //if (gateOpen.WaitOne(millisecondsTimeout)) 
                {
                    lock (pendingBLock)
                    {
                        // Gate was opened, so combined message was sent.
                        return null;
                    }
                }
                else
                {
                    // Timeout expired, so send b-only message.
                    lock (pendingBLock)
                    {
                        // reopen gate.
                        OpenGate();
                        pendingB = null;
                        return new Message(null, b);
                    }
                }
            }
        }


    }

主要工作是使用手动重置事件完成的。这个想法是,如果门打开,那么你可以自由发送A.当'b'到达时,你关闭门并强制A将它组合起来。我必须说,只有一个pendingB字段会限制此操作。只有一个变量意味着只有一个线程可以将它存储在pendingB中。这就是为什么我有额外的bMessageLock

此外,需要控制对此变量的访问,因此需要pendingBLock

此代码中可能仍然存在错误,但是尽管我测试了它,但我仍然会收到所有100条消息。

最后,我将检查包含在WrapB等待的时间内。最初WrapB的排队总共需要200秒。如果您有重叠的呼叫,则可以添加支票。如果您不介意排队,请使用更简单的代码。

答案 5 :(得分:0)

好吧,我的第一个想法就是拥有一个也能处理优先级的信号量,但也许这篇文章会给你更多的洞察力.Net Mutex Question

基本上,如果没有收到类型为B的事件,则可以通过某种方式确定两种类型事件的优先级,以便类型A的事件可以尽可能快地运行。

我意识到这可能不是适合你的解决方案,因为你的第三个限制是除了Gate之外没有其他同步机制可用,但我希望我能指出你正确的方向。

答案 6 :(得分:0)

这是一种提高公平性的方法草图 - 这意味着所有 B - 发送的延迟最长可达100毫秒。但我不知道它是否适合你的约束。

  • 在全局上下文中,拥有MessageSender
  • 类型的单个IMessageSender对象
  • IMessageSender有两种实现,即DefaultMessageSenderBWrappingMessageSender(存储b值)

邮件发件人的行为如下:

  • DefaultMessageSender被要求发送A:只是发送
  • DefaultMessageSender被要求发送B:将全局MessageSender切换为新BWrappingMessageSender,知道刚刚传递的值{{1} }}

  • b被要求发送BWrappingMessageSender:发送包含已通过的A及其自己a的AB,并切换全局b成为MessageSender

  • DefaultMessageSender被要求发送BWrappingMessageSender:发送BB,并将全局b切换为新的MessageSender {1}}知道刚刚通过的值BWrappingMessageSender

我没有固定的是一种新创建的b知道在创建后100ms发送普通BWrappingMessageSender的方式,如果它在那段时间内没有被告知要做的话别的什么。

答案 7 :(得分:0)

经过一些实验后,这是我的解决方案:

  • 如果单元素队列为空,我们就是现场。
  • 如果现场已被拍摄,我们礼貌地推动乘客继续前进,等待一会儿再试一次。
  • 如果有人在我们等待的时候很粗鲁并且劫持了这个地方,我们会排队等候继续前进。

代码:

Message WrapA(int a, int millisecondsTimeout)
{
    bool lockTaken = false;
    int? b = null;

    try
    {
        Monitor.TryEnter(gate, millisecondsTimeout, ref lockTaken);
        if (lockTaken)
        {
            if (pendingB != null)
            {
                b = pendingB;
                pendingB = null;
                Monitor.Pulse(gate);
            }
        }
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(gate);
        }
    }

    return new Message(a, b);
}

Message WrapB(int b, int millisecondsTimeout)
{
    bool lockTaken = false;

    try
    {
        TimeoutHelper timeout = new TimeoutHelper(millisecondsTimeout);
        Monitor.TryEnter(gate, timeout.RemainingTime(), ref lockTaken);
        if (lockTaken)
        {
            if (pendingB == null)
            {
                pendingB = b;
                Monitor.Wait(gate, timeout.RemainingTime());
                if (pendingB == null) return null;
                pendingB = null;
            }
            else
            {
                Monitor.Pulse(gate);
                try { }
                finally { lockTaken = false; Monitor.Exit(gate); }
                Thread.Sleep(1);
                Monitor.TryEnter(gate, timeout.RemainingTime(), ref lockTaken);
                if (lockTaken)
                {
                    if (pendingB == null)
                    {
                        pendingB = b;
                        Monitor.Wait(gate, timeout.RemainingTime());
                        if (pendingB == null) return null;
                        pendingB = null;
                    }
                }
            }
        }
    }
    finally
    {
        if (lockTaken)
        {
            Monitor.Exit(gate);
        }
    }

    return new Message(null, b);
}

答案 8 :(得分:0)

不确定它能做到你想要的,但这是我的主张。它基本上尽可能地将任何B消息移交给A,并检查消息是否已经发送:

class MessageWrapper
{
    object gate = new object();
    int? pendingB;

    public Message WrapA(int a, int millisecondsTimeout)
    {
        int? b;

        lock (gate)
        {
            b = pendingB;
            pendingB = null;
            Thread.Sleep(1); // yield. 1 seems the best value after some testing
        }

        return new Message(a, b);
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        int? bb = b;

        lock (gate)
        {
            if (pendingB == null)
            {
                pendingB = b;
                bb = null;
            }
        }

        Thread.Sleep(3);

        if (bb == null)
        {
            lock (gate)
            {
                if (pendingB != null)
                {
                    bb = pendingB;
                    pendingB = null;
                }
            }
        }
        return new Message(null, bb);
    }
}

答案 9 :(得分:0)

这是另一种尝试。方法是等待生成A事件以附加到B事件,而不是等待B事件附加到A事件。

object gate = new object();
int? pendingA;

public Message WrapA(int a, int millisecondsTimeout)
{
    bool queued = false;

    lock (gate)
    {
        if (pendingA == null)
        {
            queued = true;
            pendingA = a;
            Monitor.Pulse(gate);
        }
    }

    if (queued)
    {
        Thread.Sleep(3);
        lock (gate)
        {
            if (pendingA == null)
                return null;

            a = pendingA.Value;
            pendingA = null;
        }
    }

    return new Message(a, null);
}

public Message WrapB(int b, int millisecondsTimeout)
{
    int? a;

    lock (gate)
    {
        if (pendingA == null)
            Monitor.Wait(gate, millisecondsTimeout);

        a = pendingA;
        pendingA = null;
    }

    return new Message(a, b);
}

答案 10 :(得分:0)

经过三个小时的尝试,我设法得到了以下结果:

00:00:01.8577304
0:  0
A:  741
B:  241
AB: 59
Generated A: 800, Sent A: 800
Generated B: 300, Sent B: 300
Total: 1100

我的方法:

(1)每当有消息B(从现在称为B)并且还没有B等待时,它将把它放到'队列'。如果给定的超时内没有其他数据包,它将发送消息。 (2)当队列中实际存在B时,它将撞击队列中的第一个B并发送此消息。这是为了确保公平。正在发送的新B将遵循与情况1相同的情况(它将排队,并在给定的时间内发送)。 (3)当有消息A(从现在称为A),并且没有待处理的B时,将立即发送A.没有实际等待。 (4)当发送A并且队列中有B时,它将从队列中“窃取”它。这两条消息都被包装,并一起发送。因为B等待在另一个线程上发送,而A偷了它,我们需要一个空检查。 A会通知B,但B通知它没有任何内容可以发送。 B将返回null。

要在代码中完成此操作:

public class MessageWrapper
{
    readonly object _gate = new object();
    int? _pendingB;

    public Message WrapA(int a, int millisecondsTimeout)
    {
        int? currentB;

        lock (_gate)
        {
            currentB = _pendingB;
            _pendingB = null;

            Monitor.Pulse(_gate); // B stolen, get rid of waiting threads
        }

        return new Message(a, currentB);
    }

    public Message WrapB(int b, int millisecondsTimeout)
    {
        lock (_gate)
        {
            if (_pendingB != null)
            {
                var currentB = _pendingB;
                _pendingB = b;

                Monitor.Pulse(_gate); // release for fairness
                Monitor.Wait(_gate, millisecondsTimeout); // wait for fairness

                return new Message(null, currentB);
            }
            else
            {
                _pendingB = b;

                Monitor.Pulse(_gate); // release for fairness
                Monitor.Wait(_gate, millisecondsTimeout); // wait for A

                if (_pendingB == null) return null;

                var currentB = _pendingB;
                _pendingB = null;
                return new Message(null, currentB);
            }
        }
    }
}

答案 11 :(得分:0)

很大的问题。我非常喜欢花一些时间在这上面。我使用的解决方案的重量是原始问题在我的计算机硬件上产生的匹配数的4倍。

也许比我更了解监视器和锁定的人可以改善这一点。

  1. 在进行匹配时释放另一个线程,而不是让该线程完全休眠,最后返回null。也许这真的不是那么昂贵。为了解决这个问题,我引入了AutoResetEvent,但由于我不理解的原因,AutoResetEvent没有按照我的意图行事,并将匹配从100减少到70.

  2. 线程的最终超时时间可以改善,因为一旦超时,它仍然需要传递有争议的锁。

  3. 它完全符合要求:

    1. 所有进程都将在指定的时间段内终止(最后一次锁定可能会增加几个周期,具体取决于锁定的争议程度)。
    2. 发送在锁之外。
    3. 使用gate同步
    4. 没有额外的计时器
    5. 偏好和线程得到平等对待
    6. 原始问题结果:

      1. 时间:4.5秒
      2. A:773
      3. B:273
      4. AB:27
      5. 此课程结果:

        1. 时间:5.4秒
        2. A:700
        3. B:300
        4. AB:100

          class MessageWrapper
          {
          object gate = new object();
          int EmptyThreadsToReleaseA = 0;
          int EmptyThreadsToReleaseB = 0;
          Queue<int> queueA = new Queue<int>();
          Queue<int> queueB = new Queue<int>();
          AutoResetEvent EmptyThreadEventA = new AutoResetEvent(false);
          AutoResetEvent EmptyThreadEventB = new AutoResetEvent(false);
          
          public Message WrapA(int a, int millisecondsTimeout)
          {
              lock (gate)
              {
                  if (queueB.Count > 0)
                  {
                      Interlocked.Increment(ref EmptyThreadsToReleaseB);
                      EmptyThreadEventB.Set();
                      return new Message(a, queueB.Dequeue());
                  }
                  else
                  {
                      queueA.Enqueue(a);
                  }
              }
          
              System.Threading.Thread.Sleep(millisecondsTimeout);
              //EmptyThreadEventA.WaitOne(millisecondsTimeout);
          
              lock (gate)
              {
                  if (EmptyThreadsToReleaseA > 0)
                  {
                      Interlocked.Decrement(ref EmptyThreadsToReleaseA);
                      return null;
                  }
          
                  return new Message(queueA.Dequeue(), null);
              }
          }
          
          public Message WrapB(int b, int millisecondsTimeout)
          {
              lock (gate)
              {
                  if (queueA.Count > 0)
                  {
                      Interlocked.Increment(ref EmptyThreadsToReleaseA);
                      EmptyThreadEventA.Set();
                      return new Message(queueA.Dequeue(), b);
                  }
                  else
                  {
                      queueB.Enqueue(b);
                  }
              }
          
              System.Threading.Thread.Sleep(millisecondsTimeout);
              //EmptyThreadEventB.WaitOne(millisecondsTimeout);
          
              lock (gate)
              {
                  if (EmptyThreadsToReleaseB > 0)
                  {
                      Interlocked.Decrement(ref EmptyThreadsToReleaseB);
                      return null;
                  }
          
                  return new Message(null, queueB.Dequeue());
              }
          }
          }
          

答案 12 :(得分:0)

我试图避免不必要的锁定,尤其是对于A类事件。此外,我还对包装类的逻辑进行了一些更改。我发现直接从此类发送消息而不仅仅返回消息会更方便,因为在我的实现中,对SendB的单个调用可能会发送两条B消息。 我在代码

中添加了一些解释性注释
public class MessageWrapper
{
    private readonly object _gate = new object();
    private object _pendingB;

    public void SendA(int a, int millisecondsTimeout, Action<Message> send)
    {
        var b = Interlocked.Exchange<object>(ref _pendingB, null);

        send(new Message(a, (int?)b));

        // this code will just release any pending "assure that B was sent" threads.
        // but everything works fine even without it
        lock (_gate)
        {
            Monitor.PulseAll(_gate);
        }
    }

    public void SendB(int b, int millisecondsTimeout, Action<Message> send)
    {
        // needed for Interlocked to function properly and to be able to chack that exatly this b event was sent.
        var boxedB = (object)(int?)b;

        // excange currently pending B event with newly arrived one
        var enqueuedB = Interlocked.Exchange(ref _pendingB, boxedB);

        if (enqueuedB != null)
        {
            // if there was some pending B event then just send it.
            send(new Message(null, (int?)enqueuedB));
        }

        // now we have to wait up to millisecondsTimeout to ensure that our message B was sent
        lock (_gate)
        {
            // release any currently waiting threads.
            Monitor.PulseAll(_gate);

            if (Monitor.Wait(_gate, millisecondsTimeout))
            {
                // if we there pulsed, then we have nothing to do, as our event was already sent 
                return;
            }
        }

        // check whether our event is still pending 
        enqueuedB = Interlocked.CompareExchange(ref _pendingB, null, boxedB);

        if (ReferenceEquals(enqueuedB, boxedB))
        {
            // if so, then just send it.
            send(new Message(null, (int?)enqueuedB));
        }
    }
}

此外,我还在测试类中进行了一些更改,这是我在评论中提到的一个原因 - 我们在测试AB子句时为所有测试线程添加了同步事件。此外,我将同时运行的线程数量从您的版本中的500减少到20(所有AB子句)。所有这些线程中的调用都被移动了线程数(在线程Start方法中作为参数传递),所以我希望测试仍然非常相关。

public static class Program
{
    private static int _counter0 = 0;
    private static int _counterA = 0;
    private static int _counterB = 0;
    private static int _counterAb = 0;
    private static object _lastA;
    private static object _lastB;

    private static object _firstA;
    private static object _firstB;

    public static void Main(string[] args)
    {
        var wrapper = new MessageWrapper();
        var sw = Stopwatch.StartNew();

        var threadsCount = 10;
        var a0called = 40;

        // Only A events
        var t0 = Start(threadsCount, a0called, 7, 1000, wrapper.SendA);
        Join(t0);

        var aJointCalled = 40;
        var bJointCalled = 10;

        var syncEvent = new CountdownEvent(threadsCount + threadsCount);
        _firstA = null;
        _firstB = null;
        // A and B events
        var t1 = Start(threadsCount, aJointCalled, 7, 1000, wrapper.SendA, syncEvent);
        var t2 = Start(threadsCount, bJointCalled, 19, 1000, wrapper.SendB, syncEvent);
        Join(t1);
        Join(t2);
        var lastA = _lastA;
        var lastB = _lastB;

        var b0called = 20;

        // Only B events
        var t3 = Start(threadsCount, b0called, 7, 1000, wrapper.SendB);
        Join(t3);

        Console.WriteLine(sw.Elapsed);

        Console.WriteLine("0:  {0}", _counter0);
        Console.WriteLine("A:  {0}", _counterA);
        Console.WriteLine("B:  {0}", _counterB);
        Console.WriteLine("AB: {0}", _counterAb);

        Console.WriteLine(
            "Generated A: {0}, Sent A: {1}",
            (threadsCount * a0called) + (threadsCount * aJointCalled),
            _counterA + _counterAb);
        Console.WriteLine(
            "Generated B: {0}, Sent B: {1}",
            (threadsCount * bJointCalled) + (threadsCount * b0called),
            _counterB + _counterAb);

        Console.WriteLine("First A was sent on {0: MM:hh:ss ffff}", _firstA);
        Console.WriteLine("Last A was sent on {0: MM:hh:ss ffff}", lastA);
        Console.WriteLine("First B was sent on {0: MM:hh:ss ffff}", _firstB);
        Console.WriteLine("Last B was sent on {0: MM:hh:ss ffff}", lastB);

        Console.ReadLine();
    }

    private static void SendMessage(Message m)
    {
        if (m != null)
        {
            if (m.A != null)
            {
                if (m.B != null)
                {
                    Interlocked.Increment(ref _counterAb);
                }
                else
                {
                    Interlocked.Increment(ref _counterA);
                    Interlocked.Exchange(ref _lastA, DateTime.Now);
                    Interlocked.CompareExchange(ref _firstA, DateTime.Now, null);
                }
            }
            else if (m.B != null)
            {
                Interlocked.Increment(ref _counterB);
                Interlocked.Exchange(ref _lastB, DateTime.Now);
                Interlocked.CompareExchange(ref _firstB, DateTime.Now, null);
            }
            else
            {
                Interlocked.Increment(ref _counter0);
            }
        }
    }

    private static Thread[] Start(
        int threadCount, 
        int eventCount, 
        int eventInterval, 
        int wrapTimeout, 
        Action<int, int, Action<Message>> wrap,
        CountdownEvent syncEvent = null)
    {
        var threads = new Thread[threadCount];
        for (int i = 0; i < threadCount; i++)
        {
            threads[i] = new Thread(
                (p) =>
                    {
                        if (syncEvent != null)
                        {
                            syncEvent.Signal();
                            syncEvent.Wait();
                        }

                        Thread.Sleep((int)p);

                        for (int j = 0; j < eventCount; j++)
                        {
                            int k = (((int)p) * 1000) + j;
                            Thread.Sleep(eventInterval);
                            wrap(k, wrapTimeout, SendMessage);
                        }
                    });

            threads[i].Start(i);
        }

        return threads;
    }

    private static void Join(params Thread[] threads)
    {
        foreach (Thread t in threads)
        {
            t.Join();
        }
    }
}

P.S。此外,感谢真正有趣的问题。

答案 13 :(得分:0)

对此的限制因素实际上是约束,特别是仅需要使用gate进行同步以及无法生成任何其他计时器/线程/任务等的要求。这最终将编程解决方案与使用Monitor个对象。例如,Christoffer的解决方案虽然优雅,但在技术上使用gate以外的同步,因为它包含在BlockingCollection的内部。 afrischke之前列出的另一个非常创新的解决方案也使用gate以外的同步。

经过大量的实验,阅读和研究后,我不得不说我不认为这个问题有一个更好(更快?)的解决方案,可以完全满足约束条件。我使用以下机制设法获得了边际性能提升。它并不漂亮,但它符合要求,平均至少在我的机器上快了1-5%;

object gate = new object();
ConcurrentDictionary<Guid, int> _bBag = new ConcurrentDictionary<Guid, int>();

public Message WrapA(int a, int millisecondsTimeout)
{
    Message message = null;
    int? b = null;
    lock (gate)
    {
        if (!_bBag.IsEmpty)
        {
            Guid key = _bBag.Keys.FirstOrDefault();
            int gotB = 0;
            if (_bBag.TryRemove(key, out gotB))
            {
                b = gotB;
                Monitor.PulseAll(gate);
            }
        }
    }

    message = new Message(a, b);
    return message;
}

public Message WrapB(int b, int millisecondsTimeout)
{
    Guid key = Guid.NewGuid();
    _bBag.TryAdd(key, b);
    lock (gate) { Monitor.Wait(gate, millisecondsTimeout); }
    int storedB = 0;
    if (_bBag.TryRemove(key, out storedB))
    {
        return new Message(null, b);
    }
    return null;    
}

放宽gate要求会稍微提高速度,特别是在未以调试模式运行时;

object gate = new object();
ManualResetEvent mre = new ManualResetEvent(false /*initialState*/);
ConcurrentDictionary<Guid, int> _bBag = new ConcurrentDictionary<Guid, int>();

public Message WrapA(int a, int millisecondsTimeout)
{
    Message message = null;
    int? b = null;
    lock (gate)
    {
        if (!_bBag.IsEmpty)
        {
            Guid key = _bBag.Keys.FirstOrDefault();
            int gotB = 0;
            if (_bBag.TryRemove(key, out gotB))
            {
                b = gotB;
                Monitor.PulseAll(gate);
            }
        }
    }

    message = new Message(a, b);
    return message;
}

public Message WrapB(int b, int millisecondsTimeout)
{
    Guid key = Guid.NewGuid();
    _bBag.TryAdd(key, b);
    mre.WaitOne(millisecondsTimeout);    // use a manual reset instead of Monitor
    int storedB = 0;
    if (_bBag.TryRemove(key, out storedB))
    {
        return new Message(null, b);
    }
    return null;
}

总而言之,我认为鉴于要求严格,您已经拥有了一个非常精细的解决方案。我实际上希望我错了,有人找到了更好的解决方案 - 这将是非常有用的信息!