c++ - compile - llvm 3.7 1
Enlazar mutex al objeto (6)
Dado el siguiente código de ejemplo:
int var;
int mvar;
std::mutex mvar_mutex;
void f(){
mvar_mutex.lock();
mvar = var * var;
mvar_mutex.unlock();
}
Quiero expresar que mvar_mutex
está vinculado a la variable mvar
y protege solo esa variable. mvar_mutex
no debe proteger var
porque no está vinculado a ella. Por lo tanto, el compilador podría transformar el código anterior en el siguiente código:
int var;
int mvar;
std::mutex mvar_mutex;
void f(){
int r = var * var; //possible data race created if binding is not known
mvar_mutex.lock();
mvar = r;
mvar_mutex.unlock();
}
Esto podría reducir la contención en la cerradura ya que se está haciendo menos trabajo mientras se mantiene.
Para int
esto se puede hacer usando std::atomic<int> mvar;
y eliminar mvar_mutex
, pero esto no es posible para otros tipos como std::vector<int>
.
¿Cómo expreso el enlace mutex-variable de manera que los compiladores de C ++ lo entiendan y realicen la optimización? Se debe permitir reordenar cualquier variable hacia arriba o hacia abajo a través de los límites de exclusión para cualquier variable que no esté vinculada a esa exclusión.
Dado que el código se genera usando clang::ASTConsumer
y clang::RecursiveASTVisitor
, estoy dispuesto a usar extensiones no estándar y manipulaciones de AST siempre que clang (idealmente clang 4.0) las admita y el código resultante no necesite ser elegante o legible por humanos
Edite ya que esto parece estar causando confusión: la transformación anterior no es legal en C ++. La unión descrita de mutex a variable no existe. La pregunta es acerca de cómo implementar eso o lograr el mismo efecto.
Quiero expresar que mvar_mutex está vinculado a la variable mvar y protege solo esa variable.
Así no es como funciona un mutex. No se "une" a nada para protegerlo. Aún es libre de acceder a este objeto directamente, sin tener en cuenta ningún tipo de seguridad de subprocesos.
Lo que debe hacer es ocultar la "variable protegida" para que no esté directamente accesible, y escribir una interfaz que la manipule y que atraviese el mutex. De esta manera, se asegura de que el acceso a los datos subyacentes esté protegido por ese mutex. Puede ser un solo objeto, puede ser un grupo funcional de objetos, puede ser una colección de muchos objetos, mutexes y atómicos, diseñados para minimizar el bloqueo.
Quiero expresar que mvar_mutex está vinculado a la variable mvar y protege solo esa variable.
No puedes hacer esto. Un mutex en realidad protege la región crítica de las instrucciones de la máquina entre la adquisición y el lanzamiento. Solo por convenio está asociado con una instancia particular de datos compartidos.
Para evitar realizar pasos innecesarios dentro de la región crítica, mantenga las regiones críticas lo más simples posible. En una región crítica, solo con las variables locales que el compilador puede "ver" obviamente no se comparten con otros subprocesos, y con un conjunto de datos compartidos que pertenecen a esa exclusión mutua. Trate de no acceder a otros datos en la región crítica que se sospeche que se comparten.
Si pudiera tener su función de idioma propuesta, solo introduciría la posibilidad de error en un programa. Todo lo que hace es tomar el código que ahora es correcto y hacer que sea incorrecto (a cambio de la promesa de cierta velocidad: que un código permanece correcto y es más rápido, porque los cálculos extraños se sacan de la región crítica).
Es como tomar un lenguaje que ya tiene un buen orden de evaluación, en el que a[i] = i++
está bien definido, y arruinarlo con un orden de evaluación no especificado.
¿Qué tal una plantilla var bloqueada?
template<typename Type, typename Mutex = std::mutex>
class Lockable
{
public:
Lockable(_Type t) : var_(std::move(t));
Lockable(_Type&&) = default;
// ... could need a bit more
T operator = (const T& x)
{
std::lock_guard<Lockable> lock(*this);
var_ = x;
return x;
}
T operator *() const
{
std::lock_guard<Lockable> lock(*this);
return var_;
}
void lock() const { const_cast<Lockable*>(this)->mutex_.lock(); }
void unlock() const { const_cast<Lockable*>(this)->mutex_.unlock().; }
private:
Mutex mutex_;
Type var_;
};
bloqueado por el operador de asignación
Lockable<int>var;
var = mylongComputation();
Funciona muy bien con lock_guard
Lockable<int>var;
std::lock_guard<Lockable<int>> lock(var);
var = 3;
Práctico en contenedores.
Lockable<std::vector<int>> vec;
etc ...
Para los tipos de datos primitivos puede usar std::atomic
con std::memory_order_relaxed
. La documentación establece que:
no hay restricciones de sincronización o ordenamiento impuestas en otras lecturas o escrituras, solo se garantiza la atomicidad de esta operación
En el siguiente ejemplo, se garantiza la atomicidad de la asignación, pero el compilador debe poder mover las operaciones.
std::atomic<int> z = {0};
int a = 3;
z.store(a*a, std::memory_order_relaxed);
Para los objetos, pensé en varias soluciones, pero:
- No hay una forma estándar de eliminar los requisitos de pedido de
std::mutex
. - No es posible crear un
std::atomic<std::vector>
. - No es posible crear un spinlock usando
std::memory_order_relaxed
( ver el ejemplo ).
He encontrado algunas respuestas que afirman que:
- Si la función no es visible en la unidad de compilación, el compilador genera una barrera porque no sabe qué variables utiliza.
- Si la función es visible y hay un mutex, el compilador genera una barrera. Por ejemplo, vea this y this
Entonces, para expresar que mvar_mutex está vinculado a la variable, puede usar algunas clases como lo indican las otras respuestas, pero no creo que sea posible permitir la reordenación completa del código.
Puede usar folly::Synchronized para asegurarse de que solo se acceda a la variable bajo un bloqueo:
int var;
folly::Synchronized<int> vmar;
void f() {
*mvar.wlock() = var * var;
}
Si desea lograr que std::mutex
solo se mantenga hasta que se realice una operación en el objeto protegido, puede escribir una class
envoltura de class
siguiente manera:
#include <cstdio>
#include <mutex>
template<typename T>
class LockAssignable {
public:
LockAssignable& operator=(const T& t) {
std::lock_guard<std::mutex> lk(m_mutex);
m_protected = t;
return *this;
}
operator T() const {
std::lock_guard<std::mutex> lk(m_mutex);
return m_protected;
}
/* other stuff */
private:
mutable std::mutex m_mutex;
T m_protected {};
};
inline int factorial(int n) {
return (n > 1 ? n * factorial(n - 1) : 1);
}
int main() {
int var = 5;
LockAssignable<int> mvar;
mvar = factorial(var);
printf("Result: %d/n", static_cast<int>(mvar));
return 0;
}
En el ejemplo anterior, el factorial
se calculará por adelantado y el m_mutex
se adquirirá solo cuando se m_mutex
a la asignación o al operador de conversión implícito en mvar
.