Mutex示例/教程?

Nav*_*Nav 165 c c++ multithreading synchronization mutex

我是多线程的新手,并试图了解互斥锁的工作原理.很多谷歌和我找到了一个体面的教程,但它仍然有一些怀疑它是如何工作的,因为我创建了自己的程序,其中锁定不起作用.

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

简而言之,您能否帮助解决最简单的互斥示例程序以及关于它如何工作的逻辑的最简单的解释?我相信这会帮助很多其他新手.

Nav*_*Nav 255

这是我谦虚的尝试向全世界的新手解释这个概念:( 我博客上的彩色编码版本)

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

一个线程是:每个人
互斥是:门把手
是:人的手
资源是:手机

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

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

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

使用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;
}
Run Code Online (Sandbox Code Playgroud)

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

如果您使用范围锁定它提供的优势,而不是显式使用lockunlock,您可以使用此处所示的括号.但是,Scoped锁具有轻微的性能开销.

使用TBB: 您需要TBB来运行以下程序,但是发布TBB代码的目的是通过查看简单代码来了解锁定和解锁的顺序(可以通过不使用获取和显示范围锁定)释放 - 这也是例外安全 - 但这更清楚).

#include <iostream>
#include "/tbb/mutex.h"
#include "/tbb/tbb_thread.h"
using namespace tbb;

typedef mutex myMutex;
static myMutex sm;
int i = 0;

void someFunction() 
{ 
      //Note: Since a scoped lock is used below, you should know that you 
      //can specify a scope for the mutex using curly brackets, instead of 
      //using lock.acquire() and lock.release(). The lock will automatically 
      //get released when program control goes beyond the scope.
      myMutex::scoped_lock lock;//create a lock
      lock.acquire(sm);//Method acquire waits until it can acquire a lock on the mutex
         //***only one thread can access the lines from here...***
         ++i;//incrementing i is safe (only one thread can execute the code in this scope) because the mutex locked above protects all lines of code until the lock release.
         sleep(1);//simply creating a delay to show that no other thread can increment i until release() is executed
         std::cout<<"In someFunction "<<i<<"\n";
         //***...to here***
      lock.release();//releases the lock (duh!)      
}

int main()
{
   tbb_thread my_thread1(someFunction);//create a thread which executes 'someFunction'
   tbb_thread my_thread2(someFunction);
   tbb_thread my_thread3(someFunction);

   my_thread1.join();//This command causes the main thread (which is the 'calling-thread' in this case) to wait until thread1 completes its task.
   my_thread2.join();
   my_thread3.join();
}
Run Code Online (Sandbox Code Playgroud)

请注意,tbb_thread.h不推荐使用.替换显示在这里.

  • 您的C++ 11线程示例[错误](http://kayari.org/cxx/antipatterns.html#locking-mutex).TBB也是如此,线索的名称是[_scoped_lock](https://software.intel.com/en-us/node/527509). (8认同)
  • 我很清楚这两个,@乔纳森.你好像错过了我写的那句话(可以通过不使用获取和释放来显示范围锁定 - 这也是例外安全 - 但是这更清楚.)至于使用范围锁定,它取决于开发人员,取决于他们正在构建什么样的应用程序.这个答案是为了解决对互斥锁概念的基本理解,而不是为了解决它的所有复杂性,所以你的评论和链接是受欢迎的,但有点超出了本教程的范围. (3认同)
  • @San:我会说实话; 是的,我确实喜欢你已经尽力向一个完整的新手解释细节(流程).但是,(请不要误解我)这篇文章的目的是将这个概念放在一个**简短的**解释中(因为其他答案指向长篇教程).我希望你不介意我是否要求你复制你的整个答案并将其作为一个单独的答案发布?这样我就可以回滚并编辑我的答案以指出你的答案. (2认同)
  • @Tom在这种情况下,您不应该访问该互斥锁.对它的操作应该被封装,以便保护它的任何东西免受这种愚蠢行为的影响.如果您使用库的公开API时,保证库是线程安全的,那么您可以安全地包含一个截然不同的互斥锁来保护您自己的共享项.否则,您确实添加了一个新的门把手,正如您所建议的那样. (2认同)
  • 为了扩展我的观点,您想要做的是在展位周围添加另一个更大的房间.客房还可能包含卫生间和淋浴.假设一次只允许一个人进入房间.你必须设计房间,以便这个房间应该有一个带把手的门,可以像电话亭一样保护房间的入口.所以现在,即使您有额外的互斥锁,您也可以在任何项目中重复使用电话亭.另一种选择是为房间中的每个设备公开锁定机制并管理房间类中的锁.无论哪种方式,您都不会向同一对象添加新锁. (2认同)
  • 为什么使用三条线而不是一条更清晰?显示您不应该在实际代码中使用的样式的教程不是一个有用的教程.使用范围锁更简单,更正确,并且应该是默认情况下教授的. (2认同)

San*_*nto 35

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

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}
Run Code Online (Sandbox Code Playgroud)

这个函数的内部看起来很简单.这只是一个声明.但是,典型的伪汇编语言等价物可能是:

load i from memory into a register
add 1 to i
store i back into memory
Run Code Online (Sandbox Code Playgroud)

因为在i上执行递增操作所需的等效汇编语言指令,我们说递增i是非atmoic操作.原子操作是可以在硬件上完成的操作,保证一旦指令执行开始就不会被中断.递增i由3个原子指令链组成.在多个线程正在调用该函数的并发系统中,当线程在错误的时间读取或写入时会出现问题.想象一下,我们有两个同时运行的线程,一个在另一个之后立即调用该函数.我们还说我们已初始化为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
Run Code Online (Sandbox Code Playgroud)

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

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

任何线程必须执行某些代码行,这些代码行可能同时被其他线程不安全地修改共享值(使用电话与他的妻子交谈),首先应该锁定互斥锁.这样,任何需要访问共享数据的线程都必须通过互斥锁.只有这样,线程才能执行代码.这部分代码称为关键部分.

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

当考虑人类寻求对真实物理对象的独占访问时,拥有互斥体的概念似乎有点奇怪,但在编程时,我们必须是有意的.并发线程和进程没有我们所做的社交和文化培养,因此我们必须强迫它们很好地共享数据.

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

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

注意:某些部分可能与早期答案类似.我接受了他的邀请编辑,他更喜欢它的原始方式,所以我保留了我所拥有的一点点他的措辞.

  • 非常感谢你,桑。我已链接到您的答案 :) 实际上,我本打算让您将我的答案 + 您的答案作为单独的答案发布,以保持流程。如果您重复使用我的答案的任何部分,我真的不介意。无论如何,我们不是为自己做这件事。 (2认同)

R..*_*R.. 12

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

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

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


小智 8

对于那些寻找 Shortex 互斥体示例的人:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}
Run Code Online (Sandbox Code Playgroud)


fis*_*ood 7

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

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

由于这是我第一次尝试使用std :: mutex和朋友,我很乐意看到评论,建议和改进!

#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;
}
Run Code Online (Sandbox Code Playgroud)


chr*_*ock 5

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

将互斥锁视为一个队列;每个尝试获取互斥体的线程都将被放置在队列的末尾。当一个线程释放互斥锁时,队列中的下一个线程就会退出并开始运行。

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


par*_*ish 5

信号量示例 ::

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

sem_wait(&m);
// critical section here
sem_post(&m);
Run Code Online (Sandbox Code Playgroud)

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