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

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

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

3 ответа

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

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

Кроме того, без явной области, предоставленной блоком, который требуется для synchronizedКомпилятору сложно рассуждать о размере блока - это кажется простым в простых случаях, таких как одна область видимости lock_guard, но существует множество сложных случаев, например, если блокировка выходит за пределы функции, и в этот момент компилятор никогда не знает, где ее можно разблокировать.

В целом замки плохо сочетаются. Рассматривать:

//
// 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-эквивалентно, но подумайте об общем случае, когда кто-то использует стороннее программное обеспечение, не зная, где блокировка берется или освобождается. В реальных ситуациях синхронизация через блокировки становится очень сложной.

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

Я все еще считаю, что мутаи и локи во многих ситуациях лучше из-за их гибкости.

Например, вы можете сделать блокировки значениями R, чтобы блокировка могла существовать только на время выполнения выражения, что значительно снижает вероятность взаимоблокировки.

Вы также можете модернизировать потокобезопасность в классах, в которых отсутствует член mutai, используя «блокирующий интеллектуальный указатель», который удерживает мьютекс и блокирует его только в то время, пока референт удерживается блокирующим интеллектуальным указателем.

Ключевое слово Synchronized долгое время существовало в Windows с CRITICAL_SECTION. Прошли десятилетия с тех пор, как я работал в Windows, поэтому не знаю, актуально ли это до сих пор.

Другие вопросы по тегам