尝试测试FIFO互斥体-如果我在一个循环中启动测试线程,则不起作用,但是如果我以1 ms的间隔启动它们,则它确实起作用

凯泽·哈姆

我一直在使用此答案中排队的锁定代码,并为此编写了单元测试。作为参考,锁定代码为:

public sealed class FifoMutex
{
    private readonly object innerLock = new object();
    private volatile int ticketsCount = 0;
    private volatile int ticketToRide = 1;
    private readonly ThreadLocal<int> reenter = new ThreadLocal<int>();

    public void Enter()
    {
        reenter.Value++;
        if (reenter.Value > 1)
            return;
        int myTicket = Interlocked.Increment(ref ticketsCount);
        Monitor.Enter(innerLock);
        while (true)
        {
            if (myTicket == ticketToRide)
            {
                return;
            }
            else
            {
                Monitor.Wait(innerLock);
            }
        }
    }

    public void Exit()
    {
        if (reenter.Value > 0)
            reenter.Value--;
        if (reenter.Value > 0)
            return;
        Interlocked.Increment(ref ticketToRide);
        Monitor.PulseAll(innerLock);
        Monitor.Exit(innerLock);
    }
}

而我的测试代码:

[TestClass]
public class FifoMutexTests
{
    public static ConcurrentQueue<string> Queue;

    [TestInitialize]
    public void Setup()
    {
        Queue = new ConcurrentQueue<string>();
    }

    [TestCleanup]
    public void TearDown()
    {
        Queue = null;
    }

    [TestMethod]
    public void TestFifoMutex()
    {
        int noOfThreads = 10;
        int[] threadSleepTimes = new int[noOfThreads];
        string[] threadNames = new string[noOfThreads];
        Random r = new Random();
        for (int i = 0; i < noOfThreads; i++)
        {
            threadSleepTimes[i] = r.Next(0, 250);
            threadNames[i] = "Thread " + i;
        }

        for (int i = 0; i < noOfThreads; i++)
        {
            FifoMutexTestUser user = new FifoMutexTestUser();
            Thread newThread = new Thread(user.DoWork);
            newThread.Name = threadNames[i];
            newThread.Start(threadSleepTimes[i]);
        }
        Thread.Sleep(3000);

        var receivedThreadNamesInOrder = Queue.ToArray();
        Assert.AreEqual(threadNames.Length, receivedThreadNamesInOrder.Length);
        for (int i = 0; i < receivedThreadNamesInOrder.Length; i++)
        {
            Assert.AreEqual(threadNames[i], receivedThreadNamesInOrder[i]);
        }
    }
}

使用此测试互斥锁用户:

public class FifoMutexTestUser
{
    private readonly static FifoMutex fifoMutex = new FifoMutex();

    public void DoWork(object sleepTime)
    {
        try
        {
            fifoMutex.Enter();
            Thread.Sleep((int)sleepTime);
            FifoMutexTests.Queue.Enqueue(Thread.CurrentThread.Name);
        }
        finally
        {
            fifoMutex.Exit();
        }
    }
}

本质上,我正在创建十个线程,每个线程将休眠一段随机的时间,然后将其名称放入主测试类的静态并发队列中。线程是从同一用户类的不同实例构建的,该类具有静态fifo互斥属性。这种情况类似于我自己的用例(我有多个消费者类,它们从不同的地方接收消息,并且我需要后端严格按顺序处理它们,但也要严格按照它们到达的顺序进行处理)。

但是此测试无效。所有线程都排队其所有名称,但顺序不正确。从第二个片段的最后一个for循环中,我读到它们实际上是按随机顺序执行的,这正是fifo互斥锁要防止的内容。

但是,这就是事情。对我的测试代码进行一次小的调整,所有这些工作都像一个魅力。

        for (int i = 0; i < noOfThreads; i++)
        {
            FifoMutexTestUser user = new FifoMutexTestUser();
            Thread newThread = new Thread(user.DoWork);
            Thread.Sleep(1);
            newThread.Name = threadNames[i];
            newThread.Start(threadSleepTimes[i]);
        }

现在,我在启动所有线程的循环(第二个片段的第二个循环)中睡眠了一毫秒,这是最小的可能间隔。如果这样做,则所有线程都以正确的顺序排入它们的名称,并且我的测试成功100%的时间。

所以我想知道为什么这么小的睡眠时间会有所作为。我对编译不是很了解,但是我的第一个猜测是,启动所有线程的循环正在由编译器编译或优化,并且在此过程中线程的顺序会改变吗?

或者,(也许更可能)替代方案,我的测试代码(或互斥代码)是否有错误?

EVK

看来(如果我正确理解问题),您认为线程实际上会启动(DoWork在这种情况下将执行),并以与调用Thread.Start它们时相同的顺序获取互斥体但是,这不是(必要的)情况。

假设你有10个线程(用“IDS”从1到10),然后您拨打Thead.Start为了他们-这并不能意味着他们将依次真正开始。您在线程1上调用start,然后在线程2上调用start,然后可能DoWork首先执行线程2(而非1)。您可以通过以下方式更改测试代码来观察此情况:

public class FifoMutexTestUser {
    private readonly int _id;
    public FifoMutexTestUser(int id) {
        _id = id;
    }
    private readonly static FifoMutex fifoMutex = new FifoMutex();

    public void DoWork(object sleepTime)
    {
        Console.WriteLine("Thread started: " + _id);
        try
        {
            fifoMutex.Enter();
            Thread.Sleep((int)sleepTime);
            FifoMutexTests.Queue.Enqueue(Thread.CurrentThread.Name);
        }
        finally
        {
            fifoMutex.Exit();
        }
    }
}

然后在其中传递循环变量(与threadNames执行断言的对象相关):

for (int i = 0; i < noOfThreads; i++)
{
    FifoMutexTestUser user = new FifoMutexTestUser(i);
    Thread newThread = new Thread(user.DoWork);
    newThread.Name = threadNames[i];
    newThread.Start(threadSleepTimes[i]);
}

您会看到类似以下内容(结果可能会有所不同):

Thread started: 9
Thread started: 1
Thread started: 0
Thread started: 2
Thread started: 3
Thread started: 4
Thread started: 5
Thread started: 6
Thread started: 7
Thread started: 8

因此,在此运行中,您Thread.Start上次调用的线程实际上首先启动。但更重要的-如果线程已经第一次开始(由开始我们的意思是在这里DoWork开始执行) -这并不意味着它会先抢互斥,因为线程并行和代码之外执行fifoMutex.EnterfifoMutex.Exit(和这些函数中之前和之后的互斥(实际上是已获得\已释放)不受任何同步结构的保护-任何线程都可以先获取互斥体。

有时(并非总是)添加延迟会给以前的(在循环中)线程带来好处,因此它有更多的机会首先实际抓住互斥体。如果您很幸运,以便线程尝试以正确的顺序获取互斥体,那么请FifoMutex确保它们随后将按该顺序解除阻塞。但是,您获取互斥量的顺序在您的测试代码中不确定。

本文收集自互联网,转载请注明来源。

如有侵权,请联系[email protected] 删除。

编辑于
0

我来说两句

0条评论
登录后参与评论

相关文章

Related 相关文章

热门标签

归档