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