Какое преимущество дает новая функция "синхронизированный" блок на С++?

Появилась новая экспериментальная функция (возможно, С++ 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; 
}

Единственное преимущество, которое я нахожу здесь, это то, что я избавил от необходимости иметь глобальный замок. Есть ли больше преимуществ использования синхронизированного блока? Когда это должно быть предпочтительным?

Ответы

Ответ 1

На первый взгляд, ключевое слово synchronized функционально схож с std::mutex, но, введя новое ключевое слово и связанную с ним семантику (такой блок, охватывающий синхронизированную область), упрощает оптимизацию этих областей для транзакционная память.

В частности, std::mutex и друзья в принципе более или менее непрозрачны для компилятора, а synchronized имеет явную семантику. Компилятор не может быть уверен в том, что делает стандартная библиотека std::mutex, и будет трудно преобразовать ее для использования TM. Предполагается, что компилятор С++ будет корректно работать, когда стандартная реализация библиотеки std::mutex будет изменена и поэтому не может сделать много предположений о поведении.

Кроме того, без явной области, предоставляемой блоком, который требуется для 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 second, тогда как th1, переместив деньги с acc1 на acc0, пытается взять acc1.m во-первых, acc0.m второй может сделать их тупиковыми.

Этот пример упрощен и может быть разрешен с помощью std::lock() или С++ 17 variadic lock_guard -эквивалент, но подумайте об общем случае где используется стороннее программное обеспечение, не зная, где находятся блокировки взят или освобожден. В реальных ситуациях синхронизация через блокировки очень сложно.

Функции транзакционной памяти предназначены для обеспечения синхронизации, которая составляет лучше, чем замки; это функция оптимизации в зависимости от контекста, но также и функция безопасности. Переписывая 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 мьютексом и не рискуя взаимоблокировками из-за противоречивых запросов от кода пользователя.