Edit: это не дубликат любого вопроса, который разрешает блокировку мьютексов в post (). Пожалуйста, прочтите внимательно, мне нужен lockfree post ()! Не отмечайте этот дубликат, если у вас нет настоящего ответа.

Семафор (как в Linux) - полезный строительный блок, которого нет ни в стандарте C ++, ни в boost (в настоящее время). В основном я говорю о семафорах между потоками одного процесса над вытесняющим планировщиком.

Меня особенно интересует, чтобы они были неблокирующими (то есть без блокировки), если это действительно не нужно блокировать. То есть post () и try_wait () всегда должны быть свободными от блокировок. И вызовы wait () должны быть без блокировки, если их вызовы происходят строго после того, как было возвращено достаточное количество post (). Также планировщик должен блокировать блокирующую функцию wait (), а не блокировку спина. Что, если мне также нужен wait_for с таймаутом - насколько это еще больше усложняет реализацию, при этом избегая голодания?

Есть ли причины, по которым семафоры не входят в стандарт?

Edit3: Итак, я не знал, что есть предложение к стандарту P0514R4, которое точно занимается этими проблемами и имеет решения для всех проблем, поднятых здесь, помимо специального добавления std :: semaphore. http: //www.open-std. org / jtc1 / sc22 / wg21 / docs / paper / 2018 / p0514r4.pdf

Также у boost их нет. В частности, те, которые находятся в межпроцессном режиме, заблокированы по спину.

Какие библиотеки поддерживают что-то подобное?

Можно ли реализовать это через windows api и другие распространенные системы?

Edit: Невозможно реализовать это lockfree с помощью atomics + mutex + condition_variable - вам нужно либо заблокировать в сообщении, либо отложить в ожидании. Если вам нужен lockfree post (), вы не можете заблокировать мьютекс в post (). Я хочу запустить, возможно, вытесняющий планировщик, и я не хочу, чтобы post () блокировался другими потоками, которые приняли мьютекс и были вытеснены. Итак, это не дубликат таких вопросов, как C ++ 0x не имеет семафоров? Как синхронизировать потоки?

Edit2: Следуя примеру реализации, чтобы продемонстрировать лучшее, что можно сделать с помощью atomics + mutex + condvar, AFAIK. post () и wait () выполняют один безблокирующий compare_exchange и только в случае необходимости блокируют мьютекс.

Однако post () не блокируется. И что еще хуже, он может быть заблокирован wait (), который заблокировал мьютекс и был вытеснен.

Для простоты я реализовал только post_one () и wait_one_for (Duration) вместо post (int) и wait_for (int, Duration). Также я предполагаю отсутствие ложного пробуждения, что не предусмотрено стандартом.

class semaphore //provides acquire release memory ordering for the user
{
private:
    using mutex_t = std::mutex;
    using unique_lock_t = std::unique_lock<mutex_t>;
    using condvar_t = std::condition_variable;
    using counter_t = int;

    std::atomic<counter_t> atomic_count_; 
    mutex_t mutex_;
    condvar_t condvar_;
    counter_t posts_notified_pending_;
    counter_t posts_unnotified_pending_;
    counter_t waiters_running_;
    counter_t waiters_aborted_pending_;

public:
    void post_one()
    {
        counter_t start_count = atomic_count_.fetch_add(+1, mo_acq_rel);
        if (start_count < 0) {
            unique_lock_t lock(mutex_);
            if (0 < waiters_running_) {
                ++posts_notified_pending_;
                condvar_.notify_one();
            }
            else {
                if (0 == waiters_aborted_pending_) {
                    ++posts_unnotified_pending_;
                }
                else {
                    --waiters_aborted_pending_;
                }
            }
        }
    }

    template< typename Duration >
    bool wait_one_for(Duration timeout)
    {
        counter_t start_count = atomic_count_.fetch_add(-1, mo_acq_rel);
        if (start_count <= 0) {
            unique_lock_t a_lock(mutex_);

            ++waiters_running_;
            BOOST_SCOPE_EXIT(&waiters_running_) {
                --waiters_running_;
            } BOOST_SCOPE_EXIT_END

            if( ( 0 == posts_notified_pending_ ) && ( 0 < posts_unnotified_pending_ ) ) {
                --posts_unnotified_pending_;
                return true;
            }
            else {

                auto wait_result = condvar_.wait_for( a_lock, timeout);
                switch (wait_result) {
                case std::cv_status::no_timeout: {
                    --posts_notified_pending_;
                    return true;
                } break;
                case std::cv_status::timeout: {

                    counter_t abort_count = atomic_count_.fetch_add(+1, mo_acq_rel);
                    if (abort_count >= 0) {
                        /*too many post() already increased a negative atomic_count_ and will try to notify, let them know we aborted. */
                        ++waiters_aborted_pending_;
                    }

                    return false;
                } break;
                default: assert(false); return false;
                }
            }
        }
        return true;
    }


    bool try_wait_one()
    {
        counter_t count = atomic_count_.load( mo_acquire );
        while (true) {
            if (count <= 0) {
                return false;
            }
            else if (atomic_count_.compare_exchange_weak(count, count-1, mo_acq_rel, mo_relaxed )) {
                return true;
            }
        }
    }
};
1
itaj 8 Ноя 2018 в 03:11

1 ответ

Лучший ответ

Да, вы можете сделать это, если ваша операционная система предлагает подходящий механизм «парковки» и «снятия с парковки», который не требует блокировки для снятия с парковки. Park означает переход потока в спящий режим (блокировка ОС), а unpark означает пробуждение этого потока.

Вы уже близки со своим атомным счетчиком и подходом condvar. Проблема в том, что condvar a mutex требуется как часть семантики. Так что вам придется отказаться от кондваров и перейти на более низкий уровень. Во-первых, вы должны упаковать все состояние, такое как текущее значение семафора, есть ли какие-либо ожидающие (и, возможно, сколько wwaiters), в одно атомарное значение и управлять этим атомарно с помощью сравнения и обмена. Это предотвращает скачки, которые могли бы произойти, если бы у вас были отдельные значения.

Затем вы можете нарисовать диаграмму состояний, показывающую все возможные состояния семафора, с краями для всех возможных состояний перехода (например, состояние «нет официантов» перейдет в состояние «да, официанты», когда появится официант). Вы реализуете все переходы с помощью сравнения и обмена, и всякий раз, когда это не удается, вам нужно повторно вычислить переход, поскольку он мог измениться!

Тогда вам нужно только реализовать блокировку. В Windows вы должны использовать События - либо автоматически, либо ручной сброс. У обоих есть свои преимущества и особенности, и есть несколько способов снять шкуру с этой кошки. Например, вы, вероятно, можете заставить его работать с одним общим событием и событиями с автоматическим сбросом.

Однако вот набросок одного механизма, который использует объект-официант для каждого потока в очереди без блокировок. Семафор состоит из управляющего слова, управляемого атомарно, и списка без блокировок с типом элемента waiter_node или стеком или любой другой готовой параллельной вещью, подобной списку, которую вы хотите использовать.

Предположим, что каждый поток владеет объектом waiter_node, который содержит только один объект события ручного сброса. Это может быть создано один раз и сохранено в TLS (вероятно, наиболее эффективно) или выделено по запросу каждый раз, когда должно произойти ожидание, и отменено, когда ожидание выполнено.

Вот основная схема:

Подождите

  • Если семафор доступен (положительный), CAS уменьшает его и продолжает.
  • Если семафор недоступен (ноль), поток вызывает ResetEvent для своего waiter_node, а затем помещает событие в список официантов, проверяет, что значение sem все еще равно нулю, а затем вызывает WaitForObject на waiter_node. Когда это вернется, начните процедуру ожидания сверху.

Почта

  • Увеличьте контрольное слово. Вставьте waiter_node, если есть, и вызовите SetEvent по нему.

Здесь бывают всевозможные "скачки", такие как waiter_node, вызываемый операцией post еще до того, как ожидающий поток даже засыпает на нем, но они должны быть мягкими.

Есть много вариантов даже для этой конструкции, основанной на очереди официантов. Например, вы можете объединить «заголовок» списка и контрольное слово, чтобы они были одним и тем же. Тогда wait не нужно дважды проверять счетчик семафоров, поскольку операция push одновременно проверяет состояние семафора. Вы также можете реализовать «прямую передачу обслуживания», при которой поток post ing вообще не увеличивает управляющее слово, если есть официанты, а просто выдает одно и будит его с информацией о том, что он успешно получил семафор.

В Linux нужно заменить Event на futex. Там проще реализовать решение "одного фьютекса", поскольку futex позволяет выполнять атомарную операцию проверки и блокировки внутри ядра, что позволяет избежать множества гонок, присущих решению Event. Таким образом, базовый эскиз - это одно управляющее слово, и вы выполняете свои переходы атомарно с помощью CAS, а затем используете futex() с FUTEX_WAIT, чтобы выполнить вторую проверку управляющего слова и атомарно блокировать (эта атомарная проверка -and-sleep - это сила futex).

3
BeeOnRope 9 Ноя 2018 в 16:10