简单的定制互斥锁失败

Posted

技术标签:

【中文标题】简单的定制互斥锁失败【英文标题】:simple custom made mutex failing 【发布时间】:2011-04-28 06:33:45 【问题描述】:

你能发现代码中的错误吗?门票最终低于 0 导致长时间停顿。

struct SContext 
    volatile unsigned long* mutex;
    volatile long* ticket;
    volatile bool* done;
;

static unsigned int MyThreadFunc(SContext* ctxt) 

    // -- keep going until we signal for thread to close
    while(*ctxt->done == false) 

        while(*ctxt->ticket)  // while we have tickets waiting
            unsigned int lockedaquired = 0;
            do 
                if(*ctxt->mutex == 0)  // only try if someone doesn't have mutex locked
                    // -- if the compare and swap doesn't work then the function returns
                    // -- the value it expects
                    lockedaquired = InterlockedCompareExchange(ctxt->mutex, 1, 0);
                
             while(lockedaquired !=  0); // loop while we didn't aquire lock
            // -- enter critical section

            // -- grab a ticket
            if(*ctxt->ticket > 0);
                     (*ctxt->ticket)--;

            // -- exit critical section
            *ctxt->mutex = 0; // release lock
        
     

     return 0;

调用函数等待线程完成

    for(unsigned int loops = 0; loops < eLoopCount; ++loops) 
        *ctxt.ticket = eNumThreads; // let the threads start!

        // -- wait for threads to finish
        while(*ctxt.ticket != 0)
            ; 
    
    done = true;

编辑:

这个问题的答案很简单,不幸的是,在我花时间精简示例以发布简化版本后,我在发布问题后立即找到了答案。叹息..

我将 lockaquired 初始化为 0。然后作为不占用总线带宽的优化,如果使用互斥体,我不执行 CAS。

不幸的是,在这种情况下,当锁被占用时,while 循环会让第二个线程通过!

很抱歉有额外的问题。我以为我不了解 Windows 低级同步原语,但实际上我只是犯了一个简单的错误。

【问题讨论】:

能否提供第二个代码示例的变量声明。 @ereOn 会的,谢谢提醒! @coderdave:对你的问题表示赞同:) 另外一个问题是,如果你有很多线程,那些等待锁空闲的线程会随机看到下一次解锁。不幸的线程可能不得不永远等待。 @Bo:据我所知,他们在示例中的共享票证池上运行,所以饥饿并不重要。 【参考方案1】:

我在您的代码中看到另一场比赛:一个线程可能导致 *ctxt.ticket 达到 0,从而允许父循环返回并重新设置 *ctxt.ticket = eNumThreads 而无需持有 *ctxt.mutex。其他一些线程现在可能已经持有互斥锁(事实上,它可能确实如此)并在*ctxt.ticket 上运行。对于您的简化示例,这只会阻止“批次”被完全分离,但如果您在 loops 循环的顶部有更复杂的初始化(如比单个单词写入更复杂),您可能会看到奇怪的行为。

【讨论】:

是的,这是真的!在这个简化的示例中,虽然它是安全的,但这不是复制和粘贴到真实代码的好模型。感谢您的精彩评论。【参考方案2】:

我发布了一个错误,我认为这是一个合法的多线程问题,但实际上这只是糟糕的逻辑。我一发布就解决了这个错误。以下是问题线和答案

unsigned int lockedaquired = 0;

我将 lockaquired 初始化为 0,然后在我添加了一个 if 语句以跳过执行 CAS 的昂贵操作之后。这种优化导致它退出 while 循环并进入关键部分。将代码更改为

unsigned int lockedaquired = 1;

解决问题。我发现代码中还有另一个隐藏的问题(我真的不应该再深夜编码了)。有人注意到关键部分中 if 语句后的分号吗?叹息……

if(*ctxt->ticket > 0);
    (*ctxt->ticket)--;

应该是这样的

if(*ctxt->ticket > 0)

此外,Ben Jackson 指出,当我们将票证重置为 eNumThreads 时,一个线程可能会在临界区中。虽然这在此示例代码中非常好,但如果您要将它应用于需要执行更多操作的问题,它可能不安全,因为线程没有以同步方式运行,因此如果您将其应用于您的代码。

最后一点,如果有人决定将这段代码用于他们自己的互斥锁实现,请记住您的主驱动线程正在空闲。如果您在关键部分执行需要大量时间的大型操作并且您的票数很高,请考虑让出您的线程以让其他软件在等待时使用 CPU。此外,如果临界区很大,请考虑使用自旋锁。

谢谢

【讨论】:

以上是关于简单的定制互斥锁失败的主要内容,如果未能解决你的问题,请参考以下文章

锁详解区分 互斥锁⾃旋锁读写锁乐观锁悲观锁

尝试获取 QSemaphore 时互斥锁解锁失败

C# 互斥量处理

互斥锁因参数无效而失败是啥意思?

互斥锁概念简单说明和举例

线程互斥锁