锁如何在并行编程中起作用?

时间:2019-07-14 10:05:44

标签: c++ parallel-processing

我正在为考试做准备,遇到了这个问题,询问以下代码的潜在输出。我感到困惑,为什么使用.lock()时,另一个正在运行的线程仍然可以与“锁定”线程。

void print(char c); // output character c
std::mutex m; // global variable useable by all threads

void A(){
    m.lock();
    print(’A’);
    print(’B’);
    m.unlock();
}

void B(){
    print(’C’);
    std::thread t(A);
    print(’D’);
    t.join();
}

int main(){
    std::thread t(B);
    t.join();
}

我认为,由于使用了锁,预期的输出应仅为“ CABD”,但答案是“ CABD”,“ CDAB”和“ CADB”。请您解释一下,谢谢。

2 个答案:

答案 0 :(得分:4)

  

我很困惑,为什么使用.lock()时,另一个正在运行的线程仍然可以与“锁定”线程竞争资源。

的确,您很困惑。没有“锁定线程”之类的东西,或者至少意味着其他东西。锁是一个同步对象。当两个线程尝试在同一对象上 m.lock();上调用std::mutex m;时,其中一个将获得并获得锁,而另一个将等待直到锁被释放。因此,m.lock();是同步点,并且所有线程必须命中相同的同步点,才能进行任何锁定。

因此,在您的代码中,只有void A()函数会获取并释放锁。因此,要实现同步,必须从多个线程中调用此函数。但是,您创建了两个线程:main中的一个调用void B(),另一个线程void B()中的一个调用void A()。因此,我们总共有3个线程(包括主线程),但是其中只有一个调用void A()。因此根本没有同步。

因此,在C必须在A B D之后发生的约束下,将发生的第一件事是(打印)B然后A以任何顺序进行(因为顺便说一句,我强烈建议使用void A()的命名方式,以免混淆。因此,可能的结果就是您所描述的。

这当然是基于void print(char c);是线程安全的假设,因为如果不是print,那么注定会发生任何事情:段错误(如果幸运的话),系统崩溃,正确的结果,错误的结果,一个黑洞吞噬着大地,我失去了重量,任何东西。在这种情况下,您甚至无法在没有同步的情况下呼叫print

旁注::您可能想使用std::lock_guard来代替手动锁定和解锁。目前,您的代码还不是异常安全的,除非const displayCartTotal= ({results})=>{ const {0: data} = results; const {itemsInCart, buyerCountry} = data; } 不抛出异常。无论如何,手动锁定(解锁)是一个坏习惯。

答案 1 :(得分:2)

您误解了互斥体的工作原理。互斥锁仅保护其lock()unlock()中的代码区域,因此它仅保护A()。在多个线程中多次调用A()会显示此信息。

让我们看看您的代码在起作用:

Main thread:                               | Thread t(B):
// create a new thread and run `B()` on it.|
std::thread t(B);                          |
/* wait for the new thread to finish */    | //Come alive.
t.join();                                  | //print 'C'.
                                           | print('C');
                                           | //start a new thread t(A)
                                           | std::thread t(A);
                                           | //print 'D'
                                           | print('D');
                                           | t.join(); //wait for thread t(B) to finish.

现在我们的输出看起来像C,因为t(B)刚刚打印了C,在此之前什么也没打印。让我们展示t(A)将会做什么:

Thread t(A):

//lock our global mutex `m`.
m.lock();
// print 'A'
print('A');
// print 'B'
print('B');
//unlock our global mutex `m`.
m.unlock();

由于没有人持有当前的互斥锁m,我们可以立即将其拿走,锁定它并继续执行我们的代码。

输出的差异是由于std::thread并不总是在您每次使用它创建对象时都立即运行。当涉及到线程调度时,有很多因素在起作用,这是另一个答案。当看到输出时,这确实给了我们摆动的空间。在t(B)的线程产生的那一刻,让t(A)t(A)一起匹配:

Thread t(B):                  |  Thread t(A):
/*start a new thread t(A)*/   |
std::thread t(A);             |  //come alive.

从现在开始,可能会发生以下事情:

  • 线程t(A)会生成并启动,但t(B)更快,并且在D有机会打印t(A)A之前打印B

  • 产生并启动线程t(A),它比t(B)快,并打印出AB,然后t(B)打印出{{1} }。

  • 产生并启动线程D,它比t(A)快并打印出t(B),但随后A赶上并打印t(B),然后{ {1}}可以打印出D

这意味着您看到的组合都是有效的。在线程t(B)甚至可以开始打印之前,您的互斥体无法阻止线程B打印t(B)

要使用互斥锁并在运行中查看它,请尝试生成2个调用D的线程:

t(A)

现在,其中一个线程必须在另一个线程上等待互斥体才能解锁,然后才能进入锁定区域。

这意味着这两个线程都可以如下运行:

A

这也可能以其他方式发生,t2在t之前获取互斥量,从而在t2之前打印。

由于互斥锁,您总是会看到print(’C’); std::thread t(A); std::thread t2(A); print(’D’); t.join(); t2.join(); 的结果,并且永远不会看到t(A): | t2(A): | | /*takes lock.*/ | m.lock(); | /*tries to take lock but fails because t(A) already holds it and thus has to wait*/ | m.lock(); print('A'); | //..still waiting.. print(’B’); | // zzz.. m.unlock(); | //now that the mutex is free we can lock() and print! //done | // print A & B and unlock mutex.. ABAB,因为两个线程不能同时位于关键互斥锁部分中。