¿Qué ventaja ofrece la nueva característica, bloque "sincronizado" en C++?
multithreading transactional-memory (2)
A primera vista, la palabra clave synchronized
es similar a std::mutex
funcionalmente, pero al introducir una nueva palabra clave y una semántica asociada (como el bloque que contiene la región sincronizada), es mucho más fácil optimizar estas regiones para la memoria transaccional.
En particular, std::mutex
y los amigos son en principio más o menos opacos al compilador, mientras que la synchronized
tiene una semántica explícita. El compilador no puede estar seguro de lo que hace la biblioteca estándar std::mutex
y le costaría mucho transformarlo para usar TM. Se esperaría que un compilador de C ++ funcione correctamente cuando se cambie la implementación de la biblioteca estándar de std::mutex
, por lo que no puede hacer muchas suposiciones sobre el comportamiento.
Además, sin un alcance explícito provisto por el bloque que se requiere para la synchronized
, es difícil para el compilador razonar sobre la extensión del bloque; parece fácil en casos simples como un solo lock_guard
ámbito, pero hay muchos casos complejos como, por ejemplo, si el bloqueo escapa a la función en qué punto el compilador nunca sabe realmente dónde podría desbloquearse.
Hay una nueva función experimental (probablemente C ++ 20), que es el "bloque sincronizado". El bloque proporciona un bloqueo global en una sección de código. El siguiente es un ejemplo de 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();
}
Siento que es superfluo. ¿Hay alguna diferencia entre un bloque sincronizado desde arriba, y este:
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;
}
La única ventaja que encuentro aquí es que me ahorré la molestia de tener un bloqueo global. ¿Hay más ventajas de usar un bloque sincronizado? ¿Cuándo debería preferirse?
Las cerraduras no se componen bien en general. Considerar:
//
// 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();
}
En este caso, el hecho de que th0
, al mover dinero de acc0
a acc1
, esté tratando de tomar acc0.m
primero, acc1.m
segundo, mientras que th1
, al mover dinero de acc1
a acc0
, está tratando de tomar acc1.m
primero , acc0.m
segundo podría hacerlos punto muerto.
Este ejemplo está simplificado en exceso, y podría resolverse utilizando std::lock()
o un variadic lock_guard-C ++ 17 equivalente, pero piense en el caso general en el que uno usa software de terceros, sin saber dónde se realizan los bloqueos o liberado. En situaciones de la vida real, la sincronización a través de los bloqueos se vuelve muy rápida.
Las características de la memoria transaccional tienen como objetivo ofrecer una sincronización que se componga mejor que los bloqueos; es una característica de optimización de tipo, dependiendo del contexto, pero también es una característica de seguridad. Reescribiendo move_money_from()
siguiente manera:
void move_money_from(Cash amount, BankAccount &a, BankAccount &b) {
synchronized {
// oversimplified transaction, obviously
if (a.withdraw(amount))
b.deposit(amount);
}
}
... uno obtiene los beneficios de la transacción que se realiza en su totalidad o no se realiza, sin cargar a BankAccount
con un mutex y sin riesgo de interbloqueos debido a solicitudes en conflicto del código de usuario.