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