semaforos - C++ 0x no tiene semáforos? ¿Cómo sincronizar hilos?
sincronizar hilos en c (9)
¿Es cierto que C ++ 0x vendrá sin semáforos? Ya hay algunas preguntas sobre Stack Overflow con respecto al uso de semáforos. Los uso (semáforos posix) todo el tiempo para dejar que un hilo espere algún evento en otro hilo:
void thread0(...)
{
doSomething0();
event1.wait();
...
}
void thread1(...)
{
doSomething1();
event1.post();
...
}
Si pudiera hacer eso con un mutex:
void thread0(...)
{
doSomething0();
event1.lock(); event1.unlock();
...
}
void thread1(...)
{
event1.lock();
doSomethingth1();
event1.unlock();
...
}
Problema: es feo y no se garantiza que thread1 bloquee primero el mutex (dado que el mismo hilo debe bloquear y desbloquear un mutex, tampoco puede bloquear el evento1 antes de que thread0 y thread1 se inicien).
Entonces, dado que boost tampoco tiene semáforos, ¿cuál es la forma más sencilla de lograr lo anterior?
Basado en la respuesta de "Maxim Yegorushkin", traté de dar el ejemplo en el estilo C ++ 11.
#include <mutex>
#include <condition_variable>
class Semaphore {
public:
Semaphore (int count_ = 0)
: count(count_) {}
inline void notify()
{
std::unique_lock<std::mutex> lock(mtx);
count++;
cv.notify_one();
}
inline void wait()
{
std::unique_lock<std::mutex> lock(mtx);
while(count == 0){
cv.wait(lock);
}
count--;
}
private:
std::mutex mtx;
std::condition_variable cv;
int count;
};
Decidí escribir el semáforo C ++ 11 más robusto / genérico que pude, en el estilo de la norma tanto como pude (tenga en cuenta que si using semaphore = ...
, normalmente usaría el nombre semaphore
similar al que normalmente usa una string
not basic_string
):
template <typename Mutex, typename CondVar>
class basic_semaphore {
public:
using native_handle_type = typename CondVar::native_handle_type;
explicit basic_semaphore(size_t count = 0);
basic_semaphore(const basic_semaphore&) = delete;
basic_semaphore(basic_semaphore&&) = delete;
basic_semaphore& operator=(const basic_semaphore&) = delete;
basic_semaphore& operator=(basic_semaphore&&) = delete;
void notify();
void wait();
bool try_wait();
template<class Rep, class Period>
bool wait_for(const std::chrono::duration<Rep, Period>& d);
template<class Clock, class Duration>
bool wait_until(const std::chrono::time_point<Clock, Duration>& t);
native_handle_type native_handle();
private:
Mutex mMutex;
CondVar mCv;
size_t mCount;
};
using semaphore = basic_semaphore<std::mutex, std::condition_variable>;
template <typename Mutex, typename CondVar>
basic_semaphore<Mutex, CondVar>::basic_semaphore(size_t count)
: mCount{count}
{}
template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::notify() {
std::lock_guard<Mutex> lock{mMutex};
++mCount;
mCv.notify_one();
}
template <typename Mutex, typename CondVar>
void basic_semaphore<Mutex, CondVar>::wait() {
std::unique_lock<Mutex> lock{mMutex};
mCv.wait(lock, [&]{ return mCount > 0; });
--mCount;
}
template <typename Mutex, typename CondVar>
bool basic_semaphore<Mutex, CondVar>::try_wait() {
std::lock_guard<Mutex> lock{mMutex};
if (mCount > 0) {
--mCount;
return true;
}
return false;
}
template <typename Mutex, typename CondVar>
template<class Rep, class Period>
bool basic_semaphore<Mutex, CondVar>::wait_for(const std::chrono::duration<Rep, Period>& d) {
std::unique_lock<Mutex> lock{mMutex};
auto finished = mCv.wait_for(lock, d, [&]{ return mCount > 0; });
if (finished)
--mCount;
return finished;
}
template <typename Mutex, typename CondVar>
template<class Clock, class Duration>
bool basic_semaphore<Mutex, CondVar>::wait_until(const std::chrono::time_point<Clock, Duration>& t) {
std::unique_lock<Mutex> lock{mMutex};
auto finished = mCv.wait_until(lock, t, [&]{ return mCount > 0; });
if (finished)
--mCount;
return finished;
}
template <typename Mutex, typename CondVar>
typename basic_semaphore<Mutex, CondVar>::native_handle_type basic_semaphore<Mutex, CondVar>::native_handle() {
return mCv.native_handle();
}
En caso de que alguien esté interesado en la versión atómica, aquí está la implementación. Se espera que el rendimiento sea mejor que la versión variable mutex y condición.
class semaphore_atomic
{
public:
void notify() {
count_.fetch_add(1, std::memory_order_release);
}
void wait() {
while (true) {
int count = count_.load(std::memory_order_relaxed);
if (count > 0) {
if (count_.compare_exchange_weak(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
break;
}
}
}
}
bool try_wait() {
int count = count_.load(std::memory_order_relaxed);
if (count > 0) {
if (count_.compare_exchange_strong(count, count-1, std::memory_order_acq_rel, std::memory_order_relaxed)) {
return true;
}
}
return false;
}
private:
std::atomic_int count_{0};
};
Encontré el shared_ptr y el weak_ptr, un largo con una lista, hice el trabajo que necesitaba. Mi problema era que tenía varios clientes que querían interactuar con los datos internos de un host. Normalmente, el host actualiza los datos por sí mismo; sin embargo, si un cliente lo solicita, el host debe dejar de actualizarse hasta que los clientes no tengan acceso a los datos del host. Al mismo tiempo, un cliente podría solicitar acceso exclusivo, de modo que ningún otro cliente, ni el host, pueda modificar esos datos de host.
Cómo lo hice fue crear una estructura:
struct UpdateLock
{
typedef std::shared_ptr< UpdateLock > ptr;
};
Cada cliente tendría un miembro de tal:
UpdateLock::ptr m_myLock;
Entonces, el host tendría un miembro weak_ptr para la exclusividad y una lista de weak_ptrs para los bloqueos no exclusivos:
std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;
Hay una función para habilitar el bloqueo y otra función para verificar si el host está bloqueado:
UpdateLock::ptr LockUpdate( bool exclusive );
bool IsUpdateLocked( bool exclusive ) const;
Pruebo los bloqueos en LockUpdate, IsUpdateLocked y periódicamente en la rutina de actualización del host. Probar un bloqueo es tan simple como verificar si el archivo weak_ptr expiró y eliminar cualquier expirado de la lista m_locks (solo lo hago durante la actualización del host), puedo verificar si la lista está vacía; al mismo tiempo, obtengo el desbloqueo automático cuando un cliente restablece el shared_ptr en el que se encuentran, lo que también ocurre cuando un cliente se destruye automáticamente.
El efecto general es que, dado que los clientes rara vez necesitan exclusividad (normalmente reservada para adiciones y eliminaciones solamente), la mayoría de las veces una solicitud de LockUpdate (falso), es decir no exclusiva, tiene éxito siempre que (! M_exclusiveLock). Y un LockUpdate (verdadero), una solicitud de exclusividad, tiene éxito solo cuando tanto (! M_exclusiveLock) como (m_locks.empty ()).
Se podría agregar una cola para mitigar bloqueos exclusivos y no exclusivos, sin embargo, no he tenido colisiones hasta el momento, por lo que tengo la intención de esperar hasta que ocurra para agregar la solución (sobre todo porque tengo una condición de prueba en el mundo real).
Hasta ahora, esto está funcionando bien para mis necesidades; Me imagino la necesidad de expandir esto, y algunos problemas que podrían surgir con el uso ampliado, sin embargo, esto fue rápido de implementar y requirió muy poco código personalizado.
Puede trabajar con mutex y variables de condición. Obtiene acceso exclusivo con el mutex, comprueba si desea continuar o necesita esperar al otro extremo. Si necesita esperar, espera en una condición. Cuando el otro hilo determina que puede continuar, señala la condición.
Hay un pequeño example en la biblioteca boost :: thread que probablemente solo pueda copiar (las librerías C ++ 0x y boost thread son muy similares).
Puedes construir fácilmente uno a partir de un mutex y una variable de condición:
#include <mutex>
#include <condition_variable>
class semaphore
{
private:
std::mutex mutex_;
std::condition_variable condition_;
unsigned long count_ = 0; // Initialized as locked.
public:
void notify() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
++count_;
condition_.notify_one();
}
void wait() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
while(!count_) // Handle spurious wake-ups.
condition_.wait(lock);
--count_;
}
bool try_wait() {
std::unique_lock<decltype(mutex_)> lock(mutex_);
if(count_) {
--count_;
return true;
}
return false;
}
};
También puede consultar cpp11-on-multicore : tiene una implementación de semáforo portátil y óptima.
El repositorio también contiene otros objetos de enhebrado que complementan el enhebrado c ++ 11.
También puede ser útil envoltura de semáforo RAII en hilos:
class ScopedSemaphore
{
public:
explicit ScopedSemaphore(Semaphore& sem) : m_Semaphore(sem) { m_Semaphore.Wait(); }
ScopedSemaphore(const ScopedSemaphore&) = delete;
~ScopedSemaphore() { m_Semaphore.Notify(); }
ScopedSemaphore& operator=(const ScopedSemaphore&) = delete;
private:
Semaphore& m_Semaphore;
};
Ejemplo de uso en la aplicación multiproceso:
boost::ptr_vector<std::thread> threads;
Semaphore semaphore;
for (...)
{
...
auto t = new std::thread([..., &semaphore]
{
ScopedSemaphore scopedSemaphore(semaphore);
...
}
);
threads.push_back(t);
}
for (auto& t : threads)
t.join();
de acuerdo con los semáforos posix, agregaría
class semaphore
{
...
bool trywait()
{
boost::mutex::scoped_lock lock(mutex_);
if(count_)
{
--count_;
return true;
}
else
{
return false;
}
}
};
Y prefiero utilizar un mecanismo de sincronización en un nivel conveniente de abstracción, en lugar de copiar siempre pegando una versión cosida utilizando operadores más básicos.