20

有一个新的实验特性(可能是 C++20),它是“同步块”。该块提供了对一段代码的全局锁定。以下是来自cppreference的示例。

#include <iostream>
#include <vector>
#include <thread>
int f()
{
    static int i = 0;
    synchronized {
        std::cout << i << " -> ";
        ++i;       
        std::cout << i << '\n';
        return i; 
    }
}
int main()
{
    std::vector<std::thread> v(10);
    for(auto& t: v)
        t = std::thread([]{ for(int n = 0; n < 10; ++n) f(); });
    for(auto& t: v)
        t.join();
}

我觉得是多余的。上面的同步块和这个块之间有什么区别:

std::mutex m;
int f()
{
    static int i = 0;
    std::lock_guard<std::mutex> lg(m);
    std::cout << i << " -> ";
    ++i;       
    std::cout << i << '\n';
    return i; 
}

我在这里发现的唯一优点是我省去了拥有全局锁的麻烦。使用同步块有更多优点吗?什么时候应该首选?

4

2 回答 2

8

从表面上看,synchronized关键字与功能相似std::mutex,但通过引入新关键字和相关语义(例如包含同步区域的块),可以更轻松地优化这些区域以用于事务内存。

特别是,std::mutex和朋友在原则上或多或少对编译器不透明,同时synchronized具有明确的语义。编译器无法确定标准库std::mutex做了什么,并且很难将其转换为使用 TM。当标准库实现发生变化时,C++ 编译器应该能够正常工作std::mutex,因此不能对行为做出很多假设。

此外,如果没有为 所需的块提供明确的范围,synchronized编译器就很难推断出块的范围 - 在简单的情况下(例如单个范围)似乎很容易lock_guard,但有很多复杂的情况例如,如果锁逃脱了函数,编译器永远不会真正知道它可以在哪里解锁。

于 2017-08-03T18:29:04.187 回答
4

锁通常不能很好地组合。考虑:

//
// includes and using, omitted to simplify the example
//
void move_money_from(Cash amount, BankAccount &a, BankAccount &b) {
   //
   // suppose a mutex m within BankAccount, exposed as public
   // for the sake of simplicity
   //
   lock_guard<mutex> lckA { a.m };
   lock_guard<mutex> lckB { b.m };
   // oversimplified transaction, obviously
   if (a.withdraw(amount))
      b.deposit(amount);
}

int main() {
   BankAccount acc0{/* ... */};
   BankAccount acc1{/* ... */};
   thread th0 { [&] {
      // ...
      move_money_from(Cash{ 10'000 }, acc0, acc1);
      // ...
   } };
   thread th1 { [&] {
      // ...
      move_money_from(Cash{ 5'000 }, acc1, acc0);
      // ...
   } };
   // ...
   th0.join();
   th1.join();
}

在这种情况下,事实上th0,通过将资金从acc0转移到acc1,试图acc0.m首先获得acc1.m第二个,而th1通过将资金从acc1转移到acc0,试图acc1.m首先acc0.m获得第二个,这可能会使它们陷入僵局。

这个例子过于简单了,可以通过使用std::lock() C++17 variadic lock_guard-equivalent 来解决,但考虑一下使用第三方软件的一般情况,不知道在哪里获取或释放锁。在现实生活中,通过锁进行同步会很快变得棘手。

事务内存功能旨在提供比锁更好的同步;它是一种优化功能,具体取决于上下文,但它也是一个安全功能。改写move_money_from()如下:

void move_money_from(Cash amount, BankAccount &a, BankAccount &b) {
   synchronized {
      // oversimplified transaction, obviously
      if (a.withdraw(amount))
         b.deposit(amount);
   }
}

...人们可以从整个事务或根本不完成事务中获得好处,而无需BankAccount使用互斥锁,也不会因用户代码的请求冲突而导致死锁。

于 2017-08-06T16:59:04.017 回答