c++ - ¿Por qué std:: lock_guard/std:: unique_lock usa el borrado de tipo?
multithreading type-erasure (2)
Porque esto complica la implementación sin ningún beneficio significativo, y oculta el hecho de que std::lock_guard
y std::unique_lock
son conscientes del tipo de bloqueo que están protegiendo en el momento de la compilación.
Su solución es una solución para el hecho de que la deducción de parámetros de la plantilla de clase no se produce durante la construcción; esto se aborda en la próxima norma.
La necesidad de especificar el tipo de bloqueo es un fastidioso problema que se resolverá en C ++ 17 (no solo para protecciones de bloqueo) gracias a la propuesta de deducción de parámetros de plantilla para constructores (P0091R3) .
La propuesta (que fue aceptada) , permite que los parámetros de la plantilla se deduzcan de los constructores, eliminando la necesidad de las funciones de ayuda make_xxx(...)
o especifique explícitamente los nombres que el compilador debería poder deducir:
// Valid C++17
for(size_t i = 0; i < num; ++i) {
std::mutex m;
std::unique_lock l(m);
}
¿Por qué std::lock_guard
y std::unique_lock
requieren que se especifique el tipo de bloqueo como parámetro de plantilla?
Considere la siguiente alternativa. Primero, en un espacio de nombres detail
, hay clases de borrado de tipo (una clase base abstracta sin plantilla y una clase derivada de plantilla):
#include <type_traits>
#include <mutex>
#include <chrono>
#include <iostream>
namespace detail {
struct locker_unlocker_base {
virtual void lock() = 0;
virtual void unlock() = 0;
};
template<class Mutex>
struct locker_unlocker : public locker_unlocker_base {
locker_unlocker(Mutex &m) : m_m{&m} {}
virtual void lock() { m_m->lock(); }
virtual void unlock() { m_m->unlock(); }
Mutex *m_m;
};
}
Ahora te_lock_guard
, la protección de bloqueo de borrado de tipo, simplemente te_lock_guard
un objeto del tipo correcto cuando se construye (sin asignación de memoria dinámica):
class te_lock_guard {
public:
template<class Mutex>
te_lock_guard(Mutex &m) {
new (&m_buf) detail::locker_unlocker<Mutex>(m);
reinterpret_cast<detail::locker_unlocker_base *>(&m_buf)->lock();
}
~te_lock_guard() {
reinterpret_cast<detail::locker_unlocker_base *>(&m_buf)->unlock();
}
private:
std::aligned_storage<sizeof(detail::locker_unlocker<std::mutex>), alignof(detail::locker_unlocker<std::mutex>)>::type m_buf;
};
He comprobado el rendimiento frente a las clases de la biblioteca estándar:
int main() {
constexpr std::size_t num{999999};
{
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
for(size_t i = 0; i < num; ++i) {
std::mutex m;
te_lock_guard l(m);
}
std::chrono::steady_clock::time_point end= std::chrono::steady_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << std::endl;
}
{
std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now();
for(size_t i = 0; i < num; ++i) {
std::mutex m;
std::unique_lock<std::mutex> l(m);
}
std::chrono::steady_clock::time_point end= std::chrono::steady_clock::now();
std::cout << std::chrono::duration_cast<std::chrono::microseconds>(end - begin).count() << std::endl;
}
}
Al usar g ++ con -O3
, no hay pérdida de rendimiento estadísticamente significativa.
Roll en C++17 ... Mientras tanto, no hay necesidad de borrar el tipo. La función de plantilla de deducción de argumentos nos permite un ayudante fácil:
template<class Mutex>
auto make_lock(Mutex& m)
{
return std::unique_lock<Mutex>(m);
}
...
std::mutex m;
std::recursive_mutex m2;
auto lock = make_lock(m);
auto lock2 = make_lock(m2);