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