互斥量示例/教程? [关闭]

Posted

技术标签:

【中文标题】互斥量示例/教程? [关闭]【英文标题】:Mutex example / tutorial? [closed] 【发布时间】:2011-06-26 17:50:08 【问题描述】:

我是多线程的新手,并试图了解互斥锁的工作原理。做了很多谷歌搜索,但仍然对它的工作原理产生了一些疑问,因为我创建了自己的程序,其中锁定不起作用。

互斥锁的一个绝对不直观的语法是pthread_mutex_lock( &mutex1 );,看起来互斥锁被锁定了,而我真正想要锁定的是其他变量。这种语法是否意味着锁定互斥锁会锁定代码区域,直到互斥锁解锁?那么线程是如何知道该区域被锁定的呢? [更新:线程知道该区域被锁定,由 Memory Fencing ]。这种现象不应该被称为临界区吗? [更新:关键部分对象仅在 Windows 中可用,其中对象比互斥锁更快,并且仅对实现它的线程可见。否则,临界区只是指受互斥体保护的代码区域]

简而言之,您能否提供最简单的互斥锁示例程序和最简单的解释来说明其工作原理?我相信这会对很多其他新手有所帮助。

【问题讨论】:

继续强调需要一个简单的教程(无论是 boost 线程、tbb 还是 pthreads): 混淆示例:1.***.com/questions/3528877/… 2.***.com/questions/2979525/… 3.***.com/questions/2095977/to-mutex-or-not-to-mutex 4 .***.com/questions/3931026/… 5.***.com/questions/1525189/… 我的意思不是冒犯性的,但是您最后的评论对我的建议是,我们需要更少的类比和更好的技术解释来解释互斥锁的工作原理以及我们为什么需要它们。 @San:没有冒犯 :) 我的 cmets 只是为了建议新手可以获得关于互斥锁的最短、最清晰的解释。许多类比可能会让新手感到困惑,所以不同的类比应该分开保存。我发布问题和答案的全部原因是因为作为一个新手,我发现阅读冗长的解释和代码示例很痛苦。我不希望其他人经历痛苦。 @Cory:如果这个答案可以改进,我很乐意接受你的建议。我很高兴很多其他人发现它很有帮助。如果它对您没有帮助,那么其他人也有指向其他互斥体教程的答案。为什么这么消极? 【参考方案1】:

这是我向世界各地的新手解释这个概念的谦虚尝试:(color coded version 也在我的博客上)

很多人跑到一个单独的电话亭(他们没有手机)与亲人交谈。第一个抓住展位门把手的人,就是被允许使用电话的人。只要他使用电话,他就必须一直抓住门把手,否则别人会抓住把手,把他扔出去和他的妻子说话:) 没有这样的排队系统。当这个人打完电话,走出电话亭并离开门把手时,下一个抓住门把手的人将被允许使用电话。

线程是:每个人mutex 是:门把手是:人的手资源是:电话

任何线程必须执行一些不应被其他线程同时修改的代码行(使用电话与他的妻子交谈),必须首先获得互斥锁上的锁(抓住门把手展位的)。只有这样,一个线程才能运行这些代码行(拨打电话)。

一旦线程执行了该代码,它应该释放互斥锁上的锁,以便另一个线程可以获得互斥锁上的锁(其他人可以访问电话亭)。

[在考虑现实世界的独占访问时,拥有互斥锁的概念有点荒谬,但在编程世界中,我想没有其他方法可以让其他线程“看到”一个线程已经存在执行一些代码行。有递归互斥锁等概念,但这个例子只是为了向你展示基本概念。希望这个例子能让你清楚地了解这个概念。]

使用 C++11 线程:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 

    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door


int main() 

    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;

使用g++ -std=c++0x -pthread -o thread thread.cpp;./thread编译并运行

如果您使用范围锁for the advantage it provides,则可以使用括号as shown here,而不是显式使用lockunlock。作用域锁虽然有轻微的性能开销。

【讨论】:

@San:老实说;是的,我确实喜欢你已经尽力向一个完整的新手解释细节(有流程)的事实。但是,(请不要误解我)这篇文章的目的是将这个概念放在 short 解释中(因为其他答案指向长教程)。如果我要求您复制整个答案并将其作为单独的答案发布,我希望您不会介意?这样我就可以回滚并编辑我的答案以指向您的答案。 @Tom 在这种情况下,您不应该访问该互斥体。应该封装对它的操作,以便保护它所保护的任何东西免受这种愚蠢的影响。如果当您使用库的公开 API 时,保证库是线程安全的,那么您可以安全地包含一个截然不同的互斥锁来保护您自己的共享项。否则,您确实按照您的建议添加了一个新的门把手。 延伸我的观点,您想要做的是在展位周围增加另一个更大的房间。房间可能还包含厕所和淋浴。假设一次只允许 1 人进入房间。您必须设计房间,以便这个房间应该有一个带把手的门,可以像电话亭一样保护进入房间。所以现在,即使您有额外的互斥锁,您也可以在任何项目中重复使用电话亭。另一种选择是公开房间中每个设备的锁定机制并管理房间类中的锁。无论哪种方式,您都不会向同一个对象添加新锁。 您的 C++11 线程示例 is wrong。 TBB 也是如此,线索在名称scoped lock。 我很清楚这两个,@Jonathan。你好像漏掉了我写的那句话(could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer。至于使用范围锁定,这取决于开发人员,具体取决于他们正在构建的应用程序类型。此答案旨在解决对互斥锁概念的基本理解,而不是深入了解它的所有复杂性,因此欢迎您的 cmets 和链接,但有点超出本教程的范围。【参考方案2】:

虽然互斥体可用于解决其他问题,但它们存在的主要原因是提供互斥,从而解决所谓的竞争条件。当两个(或更多)线程或进程试图同时访问同一个变量时,我们就有可能出现竞争条件。考虑以下代码

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()

  i++;

这个函数的内部看起来很简单。这只是一种说法。然而,一个典型的伪汇编语言等价物可能是:

load i from memory into a register
add 1 to i
store i back into memory

因为对 i 执行递增操作都需要等效的汇编语言指令,所以我们说递增 i 是非原子操作。原子操作是一种可以在硬件上完成的操作,并保证一旦指令执行开始就不会被中断。递增 i 由 3 个原子指令链组成。在多个线程调用函数的并发系统中,当线程在错误的时间读取或写入时会出现问题。想象一下,我们有两个线程同时运行,一个在另一个之后立即调用该函数。假设我们已经将 i 初始化为 0。还假设我们有很多寄存器,并且两个线程使用完全不同的寄存器,所以不会发生冲突。这些事件的实际发生时间可能是:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

发生的情况是我们有两个线程同时递增 i,我们的函数被调用了两次,但结果与事实不一致。看起来该函数只被调用了一次。这是因为原子性在机器级别被“破坏”了,这意味着线程可以相互中断或在错误的时间一起工作。

我们需要一种机制来解决这个问题。我们需要对上面的说明进行一些排序。一种常见的机制是阻塞除一个之外的所有线程。 Pthread 互斥锁使用这种机制。

任何必须执行一些代码行的线程可能会同时不安全地修改其他线程的共享值(使用电话与他的妻子交谈),应首先获取互斥锁上的锁。这样,任何需要访问共享数据的线程都必须通过互斥锁。只有这样一个线程才能执行代码。这部分代码称为临界区。

一旦线程执行了临界区,它应该释放互斥锁上的锁,以便另一个线程可以获取互斥锁上的锁。

考虑到人类寻求对真实物理对象的独占访问权时,拥有互斥锁的概念似乎有点奇怪,但在编程时,我们必须是有意识的。并发线程和进程不像我们那样受社会和文化熏陶,因此我们必须强制它们很好地共享数据。

所以从技术上讲,互斥锁是如何工作的?它不会受到我们前面提到的相同竞争条件的影响吗? pthread_mutex_lock() 是不是比简单的变量增量复杂一点?

从技术上讲,我们需要一些硬件支持来帮助我们。硬件设计者给我们的机器指令不仅做一件事,而且保证是原子的。这种指令的一个典型例子是测试和设置(TAS)。当尝试获取资源的锁时,我们可能会使用 TAS 可能会检查内存中的值是否为 0。如果是,这将是我们的信号,表明资源正在使用中,我们什么也不做(或更准确地说, 我们通过某种机制等待。pthreads 互斥体会将我们放入操作系统中的一个特殊队列中,并在资源可用时通知我们。笨蛋系统可能需要我们进行紧密的自旋循环,一遍又一遍地测试条件) .如果内存中的值不为 0,则 TAS 将位置设置为 0 以外的值,而不使用任何其他指令。这就像将两个汇编指令组合成 1 以赋予我们原子性。因此,测试和更改值(如果更改是适当的)一旦开始就不能中断。我们可以在这样的指令之上构建互斥锁。

注意:某些部分可能与之前的答案相似。我接受了他的编辑邀请,他更喜欢原来的方式,所以我保留了我所拥有的内容,其中注入了一点他的措辞。

【讨论】:

非常感谢,桑。我已经链接到您的答案:) 实际上,我原本打算让您接受我的答案+您的答案并将其作为单独的答案发布,以保持流畅。我真的不介意您是否重复使用我的答案的任何部分。无论如何,我们不是为自己这样做。【参考方案3】:

我知道的最好的线程教程在这里:

https://computing.llnl.gov/tutorials/pthreads/

我喜欢它是关于 API 的,而不是关于特定实现的,它提供了一些很好的简单示例来帮助您理解同步。

【讨论】:

我同意这绝对是一个很好的教程,但它是一个页面上的大量信息并且程序很长。我发布的问题是“我有一个梦想”演讲的互斥版本,新手会找到一种简单的方法来了解互斥锁并了解非直观语法的工作原理(这是所有教程中都缺乏的一种解释) .【参考方案4】:

我最近偶然发现了这篇文章,并认为它需要标准库的 c++11 互斥锁(即 std::mutex)的更新解决方案。

我在下面粘贴了一些代码(我使用互斥锁的第一步 - 我在 win32 上使用 HANDLE、SetEvent、WaitForMultipleObjects 等学习了并发)。

由于这是我与 std::mutex 和朋友的第一次尝试,我很乐意看到 cmets、建议和改进!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        
    );


    std::thread thrProducer(
        [&]()
        
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                
            
           
    );

    std::thread thrConsumer(
        [&]()
        
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                               
            
        
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;

【讨论】:

超级!感谢您发布。尽管正如我之前提到的,我的目的只是简单地解释互斥锁的概念。所有其他教程都很难添加生产者消费者和条件变量等概念,这让我很难理解到底发生了什么。【参考方案5】:

函数pthread_mutex_lock() 要么获取调用线程的互斥锁,要么阻塞线程直到可以获取互斥锁。相关的pthread_mutex_unlock() 释放互斥体。

把互斥体想象成一个队列;每个尝试获取互斥锁的线程都将被放置在队列的末尾。当一个线程释放互斥锁时,队列中的下一个线程将关闭并开始运行。

临界区是指可能存在非确定性的代码区域。这通常是因为多个线程试图访问一个共享变量。在某种同步到位之前,临界区是不安全的。互斥锁是同步的一种形式。

【讨论】:

是否保证下一个尝试线程会准确进入? @Arsen 不保证。这只是一个有用的类比。【参考方案6】:

对于那些寻找shortex mutex 示例的人:

#include <mutex>

int main() 
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();

【讨论】:

【参考方案7】:

您应该在使用受互斥锁保护的区域之前检查互斥锁变量。因此,您的 pthread_mutex_lock() 可以(取决于实现)等到 mutex1 被释放或返回一个值,表明如果其他人已经锁定它,则无法获得该锁。

Mutex 实际上只是一个简化的信号量。如果您阅读并了解它们,您就会了解互斥锁。 SO中有几个关于互斥锁和信号量的问题。 Difference between binary semaphore and mutex、When should we use mutex and when should we use semaphore 等等。第一个链接中的厕所示例是人们能想到的最好的示例。所有代码所做的就是检查密钥是否可用,如果可用,则保留它。请注意,您并没有真正保留厕所本身,而是钥匙。

【讨论】:

pthread_mutex_lock 如果其他人持有锁,则无法返回。在这种情况下它会阻塞,这就是重点。 pthread_mutex_trylock 是如果持有锁将返回的函数。 是的,一开始我并没有意识到这是什么实现。【参考方案8】:

信号量示例 ::

sem_t m;
sem_init(&m, 0, 0); // initialize semaphore to 0

sem_wait(&m);
// critical section here
sem_post(&m);

参考:http://pages.cs.wisc.edu/~remzi/Classes/537/Fall2008/Notes/threads-semaphores.txt

【讨论】:

以上是关于互斥量示例/教程? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

21.互斥量

C# 互斥量处理

提升互斥量抛出(奇怪?)异常

(转载)pThreads线程 线程同步--互斥量/锁

RT-Thread快速入门-互斥量

多线程相关------互斥量