Reputation: 26256
There's a new experimental feature (probably C++20), which is the "synchronized block". The block provides a global lock on a section of code. The following is an example from 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();
}
I feel it's superfluous. Is there any difference between the a synchronized block from above, and this one:
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;
}
The only advantage I find here is that I'm saved the trouble of having a global lock. Is there more advantages of using a synchronized block? When should it be preferred?
Upvotes: 22
Views: 8800
Reputation: 8188
I still think that mutai and locks are better in many situations due to their flexibility.
For example, you can make locks Rvalues so that a lock can only exist for the duration of an expression, greatly diminishing the possibility of deadlock.
You can also retrofit thread safety on classes missing member mutai by using a "locking smart pointer" that holds the mutex and locks only during the time the referent is being held by the locking smart pointer.
The synchronized keyword existed for a long time in Windows, with the CRITICAL_SECTION. It's been decades since I worked in Windows so I don't know if that is still a thing.
Upvotes: 0
Reputation: 64895
On the face of it, the synchronized
keyword is similar to std::mutex
functionally, but by introducing a new keyword and associated semantics (such the block enclosing the synchronized region) it makes it much easier to optimize these regions for transactional memory.
In particular, std::mutex
and friends are in principle more or less opaque to the compiler, while synchronized
has explicit semantics. The compiler can't be sure what the standard library std::mutex
does and would have a hard time transforming it to use TM. A C++ compiler would be expected to work correctly when the standard library implementation of std::mutex
is changed, and so can't make many assumptions about the behavior.
In addition, without an explicit scope provided by the block that is required for synchronized
, it is hard for the compiler to reason about the extent of the block - it seems easy in simple cases such as a single scoped lock_guard
, but there are plenty of complex cases such as if the lock escapes the function at which point the compiler never really knows where it could be unlocked.
Upvotes: 9
Reputation: 101
Locks do not compose well in general. Consider:
//
// 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();
}
In this case, the fact that th0
, by moving money from acc0
to acc1
, is
trying to take acc0.m
first, acc1.m
second, whereas th1
, by moving money from acc1
to acc0
, is trying to take acc1.m
first, acc0.m
second could make them deadlock.
This example is oversimplified, and could be solved by using std::lock()
or a C++17 variadic lock_guard
-equivalent, but think of the general case
where one is using third party software, not knowing where locks are being
taken or freed. In real-life situations, synchronization through locks gets
tricky really fast.
The transactional memory features aim to offer synchronization that composes
better than locks; it's an optimization feature of sorts, depending on context, but it's also a safety feature. Rewriting move_money_from()
as follows:
void move_money_from(Cash amount, BankAccount &a, BankAccount &b) {
synchronized {
// oversimplified transaction, obviously
if (a.withdraw(amount))
b.deposit(amount);
}
}
... one gets the benefits of the transaction being done as a whole or not at
all, without burdening BankAccount
with a mutex and without risking deadlocks due to conflicting requests from user code.
Upvotes: 5