C ++ 0x не має семафорів? Як синхронізувати потоки?


135

Чи правда, що C ++ 0x вийде без семафорів? Про стек переповнення вже є кілька питань щодо використання семафорів. Я використовую їх (posix semaphores) весь час, щоб потік чекав на якусь подію в іншій потоці:

void thread0(...)
{
  doSomething0();

  event1.wait();

  ...
}

void thread1(...)
{
  doSomething1();

  event1.post();

  ...
}

Якби я зробив це з мютекс:

void thread0(...)
{
  doSomething0();

  event1.lock(); event1.unlock();

  ...
}

void thread1(...)
{
  event1.lock();

  doSomethingth1();

  event1.unlock();

  ...
}

Проблема: це некрасиво, і не гарантується, що thread1 спочатку заблокує мютекс (враховуючи, що той самий потік повинен заблокувати та розблокувати mutex, ви також не можете заблокувати event1 перед тим, як потік0 і thread1 розпочався).

Отже, оскільки прискорення не має семафорів, який найпростіший спосіб досягти вищезазначеного?


Можливо, використовуйте умову mutex та std :: обережно та std :: майбутнє?
Ів

Відповіді:


180

Ви можете легко створити його з mutex та змінної умови:

#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
хтось повинен подати пропозицію до комітету зі стандартів

7
коментар тут, що мене спантеличив спочатку, це замок у очікуванні, можна запитати, як потік може пройти попередження, якщо блокування утримується в режимі очікування? дещо погано неясно задокументована відповідь полягає в тому, що condition_variable.wait імпульсує замок, дозволяючи іншій потоці пройти попереднє повідомлення атомним способом, принаймні так я розумію це

31
Навмисно було виключено з Boost, виходячи з того, що семафор - це занадто багато мотузки, щоб програмісти могли повіситись. Змінні умови нібито є більш керованими. Я бачу їхню думку, але відчуваю себе трохи покровительською. Я припускаю, що така ж логіка стосується і C ++ 11 - очікується, що програмісти пишуть свої програми таким чином, що "природним чином" використовують конвертори або інші затверджені методи синхронізації. Поставити семафор буде протилежно тому, незалежно від того, реалізовано він поверх конверра або вродженого.
Стів Джессоп

5
Примітка - Дивіться en.wikipedia.org/wiki/Spurious_wakeup щодо обґрунтування за while(!count_)циклом.
Дан Ніссенбаум

3
@Maxim Вибачте, я не думаю, що ви праві. sem_wait і sem_post також систематизують лише суперечки (перевірити sourceware.org/git/?p=glibc.git ; Якщо ви маєте намір переносимість в будь-якій системі, це може бути рішенням, але якщо вам потрібна лише сумісність Posix, використовуйте семафор Posix.
xryl669

107

На основі відповіді Максима Єгорушкіна я спробував зробити приклад у стилі 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
Ви можете змусити зачекати () також трилінійну:cv.wait(lck, [this]() { return count > 0; });
Домі

2
Додавання ще одного класу в дусі lock_guard також є корисним. У RAII моді конструктор, який сприймає семафор як посилання, викликає виклик семафора wait (), а деструктор викликає його call (notify ()). Це запобігає тому, що винятки не зможуть випустити семафор.
Jim Hunziker

чи не існує мертвого блокування, якщо скажімо, N потоків називається wait () і count == 0, то cv.notify_one (); ніколи не викликається, оскільки mtx не вийшов?
Марчелло

1
@Marcello Нитки очікування не містять блокування. Вся суть змінних умов полягає в тому, щоб забезпечити атомну операцію "розблокувати і чекати".
Девід Шварц

3
Ви повинні звільнити блокування перед тим, як зателефонувати notify_one (), щоб уникнути негайного блокування пробудження ... дивіться тут: en.cppreference.com/w/cpp/thread/condition_variable/notify_all
kylefinn

38

Я вирішив написати найбільш надійний / загальний C ++ 11 семафор, який я міг, у стилі стандарту, наскільки це міг (зауважте using semaphore = ..., ви зазвичай просто використовуєте ім'я, semaphoreподібне до звичайного, що stringне використовує 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();
}

Це працює, з незначною редагуванням. В wait_forі wait_untilвиклики методу предиката повертають логічне значення ( а НЕ `станд :: cv_status).
jdknight

вибачте, що пізніше в грі вибираємо ніт. std::size_tне підписаний, тому декрементуючи його нижче нуля - це UB, і це завжди буде >= 0. ІМХО countмає бути int.
Річард Ходжес

3
@RichardHodges немає способу зменшення рівня нижче нуля, тому немає проблем, а що означатиме негативний підрахунок семафору? Це навіть не має сенсу IMO.
Девід

1
@David Що робити, якщо нитку довелося чекати, щоб інші ініціалізували речі? наприклад, 1 нитка читача, щоб чекати 4 потоки, я б назвав конструктор семафору з -3, щоб змусити читацький потік до тих пір, поки всі інші потоки не зробили публікацію. Я думаю, є й інші способи, але чи не розумно? Я думаю, що насправді питання, яке задає ОП, але з більшою кількістю "ниток1".
jmmut

2
@RichardHodges бути дуже педантичним, декрементуючи неподписаний цілий тип нижче 0, це не UB.
jcai

15

відповідно до позіксальних семафорів, додам

class semaphore
{
    ...
    bool trywait()
    {
        boost::mutex::scoped_lock lock(mutex_);
        if(count_)
        {
            --count_;
            return true;
        }
        else
        {
            return false;
        }
    }
};

І я дуже вважаю за краще використовувати механізм синхронізації на зручному рівні абстракції, а не завжди копіювати обклеювання зшитої версії за допомогою більш основних операторів.


9

Ви також можете перевірити cpp11-on-multicore - він має портативну та оптимальну реалізацію семафору.

Репозиторій також містить інші смаколики для нарізки, що доповнюють c ++ 11 нарізку.


8

Ви можете працювати зі змінними mutex та умовами. Ви отримуєте ексклюзивний доступ з mutex, перевіряєте, чи хочете ви продовжувати чи потрібно чекати іншого кінця. Якщо вам потрібно чекати, ви чекаєте в умові. Коли інший потік визначає, що можна продовжувати, він сигналізує про стан.

У бібліотеці boost :: thread є короткий приклад, який ви, швидше за все, просто можете скопіювати (C ++ 0x та boost потоки lib дуже схожі).


Стан сигналізує лише для очікуючих потоків, чи ні? Отже, якщо нитка0 не чекає, коли потік сигналу1 вона буде заблокована пізніше? Плюс: мені не потрібен додатковий замок, який поставляється з умовою - це накладні витрати.
тауран

Так, умова сигналізує лише про очікування потоків. Загальна модель - це наявність змінної зі станом та умовою, якщо вам потрібно чекати. Подумайте про виробника / споживача, буде кількість рахунків у буфері, виробник блокується, додає елемент, збільшує кількість і сигнали. Споживач блокує, перевіряє лічильник і якщо не нульовий витрачається, тоді як, якщо нуль чекає в умові.
Девід Родрігес - дрибес

2
Ви можете імітувати семафор таким чином: ініціалізуйте змінну зі значенням, яке ви б дали семафору, а потім wait()переводиться на "заблокувати, перевірити підрахунок, якщо ненульове зменшення і продовжувати; якщо нуль чекати за умовою", а post"замок", приріст лічильника, сигнал, якщо він був 0 "
David Rodríguez - dribeas

Так, добре звучить. Цікаво, чи реалізовані семафори posix так само.
тауран

@tauran: Я точно не знаю (і це може залежати, яка Posix OS), але я вважаю малоймовірною. Традиційно семафори є примітивом синхронізації "нижчого рівня", ніж мутекси та змінні умови, і в принципі їх можна зробити більш ефективними, ніж вони були б застосовані поверх конвар. Отже, більш імовірно, що в даній ОС є те, що всі примітивні синхроністичні рівні на рівні користувача побудовані на основі деяких загальних інструментів, які взаємодіють із планувальником.
Стів Джессоп

3

Також може бути корисна серіяфорна обгортка RAII в нитках:

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;
};

Приклад використання в багатопотоковому додатку:

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 нарешті матиме семафори - std::counting_semaphore<max_count>.

Вони матимуть (принаймні) такі методи:

  • acquire() (блокування)
  • try_acquire() (не блокує, повертає негайно)
  • try_acquire_for() (не блокує, триває)
  • try_acquire_until() (не блокуючи, потрібен час, щоб припинити спроби)
  • release()

Це ще не вказано на cppreference, але ви можете прочитати ці слайди презентації CppCon 2019 або переглянути відео . Також є офіційна пропозиція P0514R4 , але я не впевнений, що це найсучасніша версія.


2

Я знайшов shared_ptr і слабкий_ptr, довгий зі списком, зробив потрібну мені роботу. Моя проблема полягала в тому, що у мене було кілька клієнтів, які бажають взаємодіяти з внутрішніми даними хоста. Зазвичай хост оновлює дані самостійно, однак, якщо клієнт цього вимагає, хосту потрібно припинити оновлення, поки жоден клієнт не отримає доступ до даних хоста. У той же час, клієнт може попросити ексклюзивний доступ, щоб ні інші клієнти, ні хост не могли змінювати ці дані хосту.

Як я це зробив, я створив структуру:

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

У кожного клієнта буде такий член:

UpdateLock::ptr m_myLock;

Тоді хост матиме слабкий_ptr-член для ексклюзивності та список слабких_птрів для неексклюзивних блокувань:

std::weak_ptr< UpdateLock > m_exclusiveLock;
std::list< std::weak_ptr< UpdateLock > > m_locks;

Існує функція для ввімкнення блокування та ще одна функція для перевірки блокування хоста:

UpdateLock::ptr LockUpdate( bool exclusive );       
bool IsUpdateLocked( bool exclusive ) const;

Я перевіряю на наявність замків у LockUpdate, IsUpdateLocked та періодично в ході оновлення хосту. Тестування блокування настільки ж просто, як перевірити, чи закінчився термін дії слабкого коду, і видалити будь-який термін дії зі списку m_locks (я це роблю лише під час оновлення хоста), я можу перевірити, чи список порожній; в той же час, я отримую автоматичне розблокування, коли клієнт скидає shared_ptr, на який він висить, що також відбувається, коли клієнт автоматично знищується.

Ефект, що переважає все, полягає в тому, що клієнтам рідко потрібна ексклюзивність (як правило, зарезервована лише для доповнень та видалень), більшість часу запит на LockUpdate (помилковий), тобто неексклюзивний, досягає успіху до тих пір, як (! M_exclusiveLock). І LockUpdate (true), запит на ексклюзивність, є успішним лише тоді, коли і (! M_exclusiveLock), і (m_locks.empty ()).

Для зменшення між ексклюзивними та неексклюзивними блокуваннями може бути додана черга, однак у мене досі не було зіткнень, тому я маю намір почекати, поки це станеться, щоб додати рішення (здебільшого, щоб я мав умову тестування в реальному світі).

Поки це добре працює для моїх потреб; Я можу собі уявити необхідність розширити це, і деякі проблеми, які можуть виникнути при розширеному використанні, однак це було швидко реалізувати і вимагало дуже мало спеціального коду.


-4

Якщо когось цікавить атомна версія, ось реалізація. Продуктивність очікується кращою, ніж версія змінної mutex та умова.

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};
};

4
Я б очікував, що ефективність буде набагато гіршою. Цей код робить практично буквально кожну можливу помилку. Як лише найочевидніший приклад, припустимо, що waitкод повинен циклічити кілька разів. Коли він нарешті розблокується, це займе матір усіх непередбачуваних гілок, оскільки передбачення циклу процесора, безумовно, передбачить, що воно знову обернеться. Я міг би перерахувати ще багато проблем із цим кодом.
Девід Шварц

1
Ось ще один очевидний вбивця продуктивності: waitцикл споживає ресурси мікропроцесорного процесора під час його обертання. Припустимо, він знаходиться в тій же фізичній ядрі, що і нитка, яка йому належить notify- це дуже сповільнить цю нитку.
Девід Шварц

1
І ось ще одне: на процесорах x86 (найпопулярніших сьогодні процесорів) операція Compare_exchange_weak - це завжди операція запису, навіть якщо вона не працює (вона повертає те саме значення, яке вона читала, якщо порівняння не вдалося). Тож припустимо, що два ядра знаходяться в waitциклі для одного і того ж семафору. Вони обоє записують на повній швидкості до тієї ж лінії кешу, яка може сповільнити інші ядра до повзання, насичуючи міжрядкові шини.
Девід Шварц

@DavidSchwartz Радий переглянути ваші коментарі. Не впевнені, розумієте частину "... прогнозування циклу процесора ...". Домовився 2-й. Мабуть, може статися ваш 3-й випадок, але порівняно з mutex, який спричиняє перемикач режиму ядра та системний виклик режиму користувача, міжядерна синхронізація не гірша.
Jeffery

1
Немає такого поняття, як семафор, що не містить замків. Вся ідея бути незамкненою - це не писати код без використання мутексів, а писати код, де нитка взагалі ніколи не блокується. У цьому випадку сама суть семафору полягає в блокуванні ниток, які викликають функцію wait ()!
Карло Вуд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.