我是多线程的新手,并试图了解互斥锁的工作原理。做了很多谷歌搜索和I found a decent tutorial,但它仍然对它是如何工作有所怀疑,因为我创建了自己的程序,其中锁定不起作用。
互斥体的一个绝对非直观的语法是pthread_mutex_lock( &mutex1 );
,它看起来像互斥锁被锁定,当我真正想要锁定的是其他变量时。这种语法是否意味着锁定互斥锁会锁定代码区域,直到互斥锁被解锁?然后线程如何知道该区域被锁定? [更新:线程知道该区域已被锁定, Memory Fencing]。这种现象应该被称为临界区吗? [更新:关键部分对象仅在Windows中可用,其中对象比互斥锁更快,并且仅对实现它的线程可见。否则,关键部分仅指由互斥锁保护的代码区域]
简而言之,您能否帮助解决最简单的互斥示例程序以及最简单的解释关于其工作原理的逻辑?我相信这会有助于 充足 其他新手。
答案 0 :(得分:240)
这是我向全世界新手解释这个概念的谦虚尝试:(我的博客上也是color coded version)
许多人跑到一个单独的电话亭(没有手机)与亲人交谈。第一个抓住展位门把手的人是允许使用手机的人。只要他使用手机,他就必须继续握住门把手,否则别人会抓住把手,把他扔出去和他的妻子说话:)这样就没有排队系统了。当该人结束通话时,从展位出来并离开门把手,下一个拿到门把手的人将被允许使用电话。
主题是:每个人
互斥锁是:门把手
锁定是:该人的手
资源是:手机
任何必须执行某些代码行的线程(不应该被其他线程同时修改)(使用手机与妻子交谈),必须先获取锁定互斥锁(抓住门把手)的展位)。只有这样,一个线程才能运行这些代码行(拨打电话)。
一旦线程执行了该代码,它就应该释放互斥锁上的锁,以便另一个线程可以锁定互斥锁(其他人可以访问电话亭)。
[在考虑真实世界的独占访问时,拥有互斥体的概念有点荒谬,但在编程世界中,我想没有其他方法可以让其他线程“看到”线程已经存在执行一些代码行。有递归互斥体等概念,但这个例子只是为了向您展示基本概念。希望这个例子可以让你清楚地了解这个概念。]
使用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
如果您使用的是作用域锁as shown here,则可以使用方括号for the advantage it provides,而不是显式使用lock
和unlock
。但是,Scoped锁具有轻微的性能开销。
使用TBB: 您需要TBB来运行以下程序,但发布TBB代码的目的是通过查看简单代码来了解锁定和解锁的顺序(可以通过不使用获取来显示范围锁定并释放 - which also is exception safe - ,但这更清楚。)
#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();
}
请注意,tbb_thread.h
已弃用。替换显示为here。
答案 1 :(得分:35)
虽然可以使用互斥锁来解决其他问题,但它们存在的主要原因是提供互斥,从而解决所谓的竞争条件。当两个(或多个)线程或进程同时尝试访问同一个变量时,我们可能会遇到竞争条件。请考虑以下代码
//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是非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
发生的事情是我们有两个线程同时递增,我们的函数被调用两次,但结果与这个事实不一致。看起来这个函数只被调用一次。这是因为原子性在机器级别被“破坏”,这意味着线程可以互相中断或在错误的时间一起工作。
我们需要一种机制来解决这个问题。我们需要对上述说明施加一些排序。一种常见的机制是阻止除一个之外的所有线程。 Pthread互斥锁使用这种机制。
任何线程必须执行某些代码行,这些代码行可能会同时不安全地修改其他线程的共享值(使用手机与他的妻子交谈),首先应该锁定互斥锁。这样,任何需要访问共享数据的线程都必须通过互斥锁。只有这样,线程才能执行代码。这部分代码称为关键部分。
一旦线程执行了临界区,它应该释放互斥锁上的锁,以便另一个线程可以获取互斥锁上的锁。
当考虑人类寻求对真实物理对象的独占访问时,拥有互斥体的概念似乎有点奇怪,但在编程时,我们必须是有意的。并发线程和流程没有我们所做的社交和文化培养,所以我们必须强迫他们很好地共享数据。
从技术上讲,互斥体是如何工作的?它不会受到我们之前提到的相同竞争条件的影响吗? pthread_mutex_lock()不是一个简单的变量增量复杂吗?
从技术上讲,我们需要一些硬件支持来帮助我们。硬件设计人员给我们的机器指令做了不止一件事,但保证是原子的。这种指令的典型例子是测试和设置(TAS)。当尝试获取资源上的锁时,我们可能会使用TAS检查以查看内存中的值是否为0.如果是,那将是我们的信号,即资源正在使用中,我们什么也不做(或更准确地说我们等待一些机制。一个pthreads互斥体会把我们放到操作系统的一个特殊队列中,并在资源可用时通知我们.Dumber系统可能要求我们做一个紧密的自旋循环,一遍又一遍地测试条件) 。如果内存中的值不为0,则TAS将位置设置为0以外的值而不使用任何其他指令。这就像将两个汇编指令合并为1来为我们提供原子性。因此,测试和更改值(如果更改是合适的)一旦开始就不能中断。我们可以在这样的指令之上构建互斥体。
注意:某些部分可能与之前的答案类似。我接受了他的邀请编辑,他更喜欢它的原始方式,所以我保留了我所拥有的一些注入了他的措辞。
答案 2 :(得分:11)
我所知道的最好的主题教程就在这里:
https://computing.llnl.gov/tutorials/pthreads/
我喜欢它是关于API的,而不是关于特定的实现,它提供了一些很好的简单示例来帮助您理解同步。
答案 3 :(得分: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;
}
答案 4 :(得分:4)
函数pthread_mutex_lock()
获取调用线程的互斥锁或阻塞线程,直到可以获取互斥锁。相关的pthread_mutex_unlock()
会释放互斥锁。
将互斥锁视为队列;尝试获取互斥锁的每个线程都将放在队列的末尾。当一个线程释放互斥锁时,队列中的下一个线程就会关闭,现在正在运行。
关键部分是指可能存在非确定性的代码区域。通常这是因为多个线程试图访问共享变量。在某种同步到位之前,关键部分是不安全的。互斥锁是一种同步形式。
答案 5 :(得分:3)
在使用受互斥锁保护的区域之前,您应该检查互斥变量。所以你的pthread_mutex_lock()可以(取决于实现)等到mutex1被释放,或者返回一个值,表明如果其他人已经锁定了锁,则无法获得锁。
Mutex实际上只是一个简化的信号量。如果您阅读并理解它们,您就会理解互斥体。关于SO中的互斥量和信号量有几个问题。 Difference between binary semaphore and mutex,When should we use mutex and when should we use semaphore等等。第一个环节中的厕所示例与人们可以想到的一样好。所有代码都是检查密钥是否可用,如果是,则保留密钥。请注意,您并没有真正预留厕所本身,而是钥匙。
答案 6 :(得分:2)
SEMAPHORE EXAMPLE ::
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
答案 7 :(得分:0)
对于那些寻找shortex互斥锁示例的人:
#include <mutex>
using namespace std;
int main() {
mutex m;
m.lock();
// do thread-safe stuff
m.unlock();
return 0;
}