C ++ 0x no tiene semáforos? ¿Cómo sincronizar hilos?


135

¿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 hiciera 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 el mutex primero (dado que el mismo thread debería bloquear y desbloquear un mutex, tampoco puede bloquear event1 antes de que se inicien thread0 y thread1).

Entonces, dado que boost tampoco tiene semáforos, ¿cuál es la forma más simple de lograr lo anterior?


¿Quizás use la condición mutex y std :: promise y std :: future?
Yves

Respuestas:


179

Puede crear 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::lock_guard<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::lock_guard<decltype(mutex_)> lock(mutex_);
        if(count_) {
            --count_;
            return true;
        }
        return false;
    }
};

96
alguien debe presentar una propuesta al

77
un comentario aquí que me desconcertó inicialmente es el bloqueo en espera, uno podría preguntarse cómo puede pasar un hilo para notificar si el bloqueo está en espera. la respuesta un tanto oscuramente mal documentado es que condition_variable.wait impulsos de la cerradura, permitiendo que otro hilo para conseguir más allá de comunicar de forma atómica, al menos así es como yo lo entiendo

31
Fue deliberadamente excluido de Boost sobre la base de que un semáforo es demasiada soga para que los programadores puedan ahorcarse. Las variables de condición supuestamente son más manejables. Entiendo su punto pero me siento un poco condescendiente. Supongo que la misma lógica se aplica a C ++ 11: se espera que los programadores escriban sus programas de una manera que "naturalmente" use condvas u otras técnicas de sincronización aprobadas. Proporcionar un semáforo funcionaría contra eso independientemente de si se implementa sobre condvar o de forma nativa.
Steve Jessop

55
Nota: consulte en.wikipedia.org/wiki/Spurious_wakeup para conocer la lógica detrás del while(!count_)ciclo.
Dan Nissenbaum

3
@Maxim Lo siento, no creo que tengas razón. sem_wait y sem_post solo syscall en la contención también (consulte sourceware.org/git/?p=glibc.git;a=blob;f=nptl/sem_wait.c ) por lo que el código aquí termina duplicando la implementación de libc, con posibles errores. Si tiene la intención de portabilidad en cualquier sistema, podría ser una solución, pero si solo necesita compatibilidad con Posix, use el semáforo de Posix.
xryl669

107

Basado en la respuesta de Maxim Yegorushkin , traté de hacer el ejemplo en 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;
};

34
Puede hacer que wait () también sea un trineo:cv.wait(lck, [this]() { return count > 0; });
Domi

2
Agregar otra clase en el espíritu de lock_guard también es útil. En la forma RAII, el constructor, que toma el semáforo como referencia, llama a la llamada wait () del semáforo, y el destructor llama a la llamada notify (). Esto evita que las excepciones no puedan liberar el semáforo.
Jim Hunziker

¿no hay un punto muerto, si digamos N hilos llamados wait () y count == 0, entonces cv.notify_one (); nunca se llama, ya que el mtx no se ha lanzado?
Marcello

1
@ Marcello Los hilos de espera no mantienen el bloqueo. El punto completo de las variables de condición es proporcionar una operación atómica de "desbloqueo y espera".
David Schwartz

3
Debe liberar el bloqueo antes de llamar a notify_one () para evitar el bloqueo inmediato de la activación ... vea aquí: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Decidí escribir el semáforo C ++ 11 más robusto / genérico que pude, en el estilo del estándar tanto como pude (tenga en cuenta using semaphore = ...que normalmente usaría el nombre semaphoresimilar a stringno usar normalmente 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();
}

Esto funciona, con una edición menor. Las llamadas al método wait_fory wait_untilcon el predicado devuelven un valor booleano (no un `std :: cv_status).
jdknight

lamento molestarme tan tarde en el juego. std::size_tno está firmado, por lo que disminuirlo por debajo de cero es UB, y siempre lo será >= 0. En mi humilde opinión countdebería ser un int.
Richard Hodges

3
@RichardHodges no hay forma de disminuir por debajo de cero, así que no hay problema, y ​​¿qué significaría un recuento negativo en un semáforo? Eso ni siquiera tiene sentido IMO.
David

1
@David ¿Qué pasaría si un hilo tuviera que esperar a que otros inicializaran las cosas? por ejemplo, 1 hilo lector para esperar 4 hilos, llamaría al constructor del semáforo con -3 para hacer que el hilo lector espere hasta que todos los otros hilos hayan publicado. Supongo que hay otras formas de hacerlo, pero ¿no es razonable? Creo que de hecho es la pregunta que hace el OP pero con más "thread1" s.
jmmut

2
@RichardHodges es muy pedante, decrementar un tipo entero sin signo por debajo de 0 no es UB.
jcai

15

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 usar un mecanismo de sincronización a un nivel conveniente de abstracción, en lugar de siempre copiar y pegar una versión unida usando operadores más básicos.


9

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 subprocesamiento que complementan el subproceso de c ++ 11.


8

Puede trabajar con mutex y variables de condición. Obtiene acceso exclusivo con el mutex, verifique si desea continuar o si necesita esperar al otro extremo. Si necesita esperar, espere en una condición. Cuando el otro hilo determina que puede continuar, señala la condición.

Hay un breve ejemplo en la biblioteca boost :: thread que probablemente puedas copiar (las bibliotecas C ++ 0x y boost thread son muy similares).


¿Las señales de condición solo a hilos en espera, o no? Entonces, si thread0 no está esperando cuando thread1 señala que se bloqueará más tarde. Además: no necesito la cerradura adicional que viene con la condición, es sobrecarga.
tauran

Sí, la condición solo señala subprocesos en espera. El patrón común es tener una variable con el estado y una condición en caso de que necesite esperar. Piense en un productor / consumidor, habrá un recuento de los elementos en el búfer, el productor bloquea, agrega el elemento, incrementa el recuento y las señales. El consumidor bloquea, verifica el contador y si no es cero consume, mientras que si el cero espera en la condición.
David Rodríguez - dribeas

2
Puede simular un semáforo de esta manera: Inicialice una variable con el valor que le daría al semáforo, luego wait()se traduce a "bloquear, verificar el conteo si no es cero y continuar; si el cero espera en condición" mientras postsería "bloquear", contador de incremento, señal si fue 0 "
David Rodríguez - dribeas

Si suena bien. Me pregunto si los semáforos posix se implementan de la misma manera.
tauran

@tauran: No estoy seguro (y podría depender de qué sistema operativo Posix), pero creo que es poco probable. Los semáforos son tradicionalmente una primitiva de sincronización de "nivel inferior" que los mutexes y las variables de condición, y en principio pueden hacerse más eficientes de lo que serían si se implementan sobre un condvar. Por lo tanto, lo más probable en un sistema operativo dado es que todas las primitivas de sincronización a nivel de usuario se construyan sobre algunas herramientas comunes que interactúan con el programador.
Steve Jessop

3

También puede ser útil envoltorio 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();

3

C ++ 20 finalmente tendrá semáforos - std::counting_semaphore<max_count> .

Estos tendrán (al menos) los siguientes métodos:

  • acquire() (bloqueo)
  • try_acquire() (sin bloqueo, devuelve inmediatamente)
  • try_acquire_for() (sin bloqueo, toma una duración)
  • try_acquire_until() (sin bloqueo, toma un tiempo para dejar de intentarlo)
  • release()

Esto aún no figura en cppreference, pero puede leer estas diapositivas de presentación de CppCon 2019 o ver el video . También está la propuesta oficial P0514R4 , pero no estoy seguro de que sea la versión más actualizada.


2

Encontré que shared_ptr y weak_ptr, un largo con una lista, hicieron el trabajo que necesitaba. Mi problema era que tenía varios clientes que querían interactuar con los datos internos de un host. Por lo general, el host actualiza los datos por sí mismo, sin embargo, si un cliente lo solicita, el host debe dejar de actualizarse hasta que ningún cliente acceda a los datos del host. Al mismo tiempo, un cliente puede solicitar acceso exclusivo, de modo que ningún otro cliente, ni el host, puedan modificar los datos del host.

Cómo hice esto fue, creé una estructura:

struct UpdateLock
{
    typedef std::shared_ptr< UpdateLock > ptr;
};

Cada cliente tendría un miembro de tales:

UpdateLock::ptr m_myLock;

Luego, el host tendría un miembro weak_ptr para exclusividad y una lista de weak_ptrs para 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;

Compruebo 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 los débiles_ptr han expirado, y eliminar cualquier vencido de la lista m_locks (solo hago esto durante la actualización del host), puedo verificar si la lista está vacía; Al mismo tiempo, obtengo desbloqueo automático cuando un cliente restablece el shared_ptr en el que están colgando, 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 (generalmente reservado solo para adiciones y eliminaciones), la mayoría de las veces una solicitud a LockUpdate (falso), es decir, no exclusiva, tiene éxito siempre que (! M_exclusiveLock). Y un LockUpdate (verdadero), una solicitud de exclusividad, tiene éxito solo cuando ambos (! M_exclusiveLock) y (m_locks.empty ()).

Se podría agregar una cola para mitigar entre bloqueos exclusivos y no exclusivos, sin embargo, no he tenido colisiones hasta ahora, por lo que tengo la intención de esperar hasta que eso suceda para agregar la solución (principalmente para tener una condición de prueba del mundo real).

Hasta ahora esto está funcionando bien para mis necesidades; Puedo imaginar la necesidad de expandir esto, y algunos problemas que podrían surgir sobre el uso expandido, sin embargo, esto fue rápido de implementar y requirió muy poco código personalizado.


-4

En caso de que alguien esté interesado en la versión atómica, aquí está la implementación. El rendimiento se espera mejor que la versión mutex y la variable de 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};
};

44
Esperaría que el rendimiento fuera mucho peor. Este código hace casi literalmente todos los errores posibles. Como solo el ejemplo más obvio, suponga que el waitcódigo tiene que repetirse varias veces. Cuando finalmente se desbloquee, tomará a la madre de todas las ramas mal predichas, ya que la predicción de bucle de la CPU ciertamente predecirá que se repetirá nuevamente. Podría enumerar muchos más problemas con este código.
David Schwartz

1
Aquí hay otro obvio asesino de rendimiento: el waitbucle consumirá recursos de microejecución de la CPU a medida que gira. Supongamos que está en el mismo núcleo físico que el hilo que se supone que notifylo tiene: ralentizará terriblemente ese hilo.
David Schwartz

1
Y aquí hay uno más: en las CPU x86 (las CPU más populares de la actualidad), una operación compare_exchange_weak es siempre una operación de escritura, incluso si falla (vuelve a escribir el mismo valor que leyó si falla la comparación). Supongamos que dos núcleos están en un waitbucle para el mismo semáforo. Ambos escriben a toda velocidad en la misma línea de caché, lo que puede ralentizar otros núcleos al saturar los buses entre núcleos.
David Schwartz

@DavidSchwartz Me alegro de ver sus comentarios. No estoy seguro de entender la parte '... predicción del bucle de la CPU ...'. Acordado el 2do. Aparentemente su tercer caso puede suceder, pero en comparación con mutex que causa el cambio de modo de usuario a modo de núcleo y la llamada del sistema, la sincronización entre núcleos no es peor.
Jeffery

1
No hay tal cosa como un semáforo sin cerradura. La idea de no tener bloqueos es no escribir código sin usar mutexes, sino escribir código donde un hilo nunca se bloquea en absoluto. ¡En este caso, la esencia del semáforo es bloquear los hilos que llaman a la función wait ()!
Carlo Wood
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.