【问题标题】:What advantage does the new feature, "synchronized" block, in C++ provide?C++ 中的新功能“同步”块有什么优势?
【发布时间】:2017-08-03 14:28:25
【问题描述】:

有一个新的实验性功能(可能是 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; 
}

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

【问题讨论】:

  • 不确定是否确实如此,但 cppreference 听起来第一个版本保证在第一个示例中按顺序打印,而 AFAIK 第二个版本则没有。
  • "虽然同步块在全局锁下执行,但实现应该检查每个块中的代码并使用乐观并发(由硬件事务内存在可用的情况下备份)以实现事务安全非事务安全代码的代码和最小锁定。"
  • @TheQuantumPhysicist、std::lock_guardstd::mutex 不是 C++ 语言的一部分:它们只是在库中定义的类。特别是,编译器无法知道mutex 的含义——互斥操作如何与操作系统交互,或者它们对线程有什么影响。 synchronized 关键字在这方面会有很大不同。
  • 不,优化不能移除显式锁;锁定有明显的副作用。
  • 我明白了。谢谢你的解释。请在 SO 平台上提供答案。

标签: c++ multithreading transactional-memory


【解决方案1】:

从表面上看,synchronized 关键字在功能上与std::mutex相似,但是通过引入一个新关键字和相关语义(例如包含同步区域的块),它使它成为为事务内存优化这些区域要容易得多。

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

此外,如果没有synchronized 所需的块提供的显式范围,编译器很难推断块的范围 - 在简单 情况下似乎很容易例如单个作用域的lock_guard,但也有很多复杂的情况,例如如果锁逃脱了函数,编译器永远不知道它可以在哪里解锁。

【讨论】:

    【解决方案2】:

    锁的组合通常不太好。考虑:

    //
    // 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 可变参数 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 增加互斥量,也不会因用户代码的请求冲突而导致死锁。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-09-19
      相关资源
      最近更新 更多