Создание блокировки, которая сохраняет порядок попыток блокировки в С++ 11

Есть ли способ убедиться, что заблокированные потоки проснулись в том же порядке, в каком они были заблокированы? Я где-то читал, что это будет называться "сильным замком", но я не нашел на этом никаких ресурсов.

В Mac OS X можно создать очередь FIFO, в которой хранятся все идентификаторы потоков заблокированных потоков, а затем использовать функцию nifty pthread_cond_signal_thread_np(), чтобы разбудить один конкретный поток, который явно нестандартен и не переносится.

Один из способов, который я могу представить, - использовать аналогичную очередь, а в точке unlock() отправить a broadcast() ко всем потокам и проверить, какая из них следующая в строке.
Но это вызвало бы много накладных расходов.

Путь к проблеме будет заключаться в том, чтобы выдать packaged_task в очередь и обработать их по порядку. Но это больше похоже на обходное решение для меня, чем решение.

Edit:
Как отмечалось в комментариях, этот вопрос может казаться несущественным, поскольку в принципе нет гарантированного упорядочения попыток блокировки.
В качестве пояснения:

У меня есть что-то, что я называю ConditionLockQueue, который очень похож на класс NSConditionLock в библиотеке Cocoa, но он поддерживает очередь FIFO заблокированных потоков вместо более или менее случайного пула.

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

Это обеспечивает очень гибкий способ синхронизации, и я нашел его очень полезным в моей программе.
Теперь мне действительно нужен способ пробудить конкретный поток с определенным идентификатором.
Но эти проблемы почти одинаковы.

Ответ 1

Основываясь на ответе @Chris Dodd, используя очередь переменных условия вместо билетов

#include <deque>
#include <mutex>
#include <condition_variable>

class ordered_lock {
    std::mutex q_lock;
    std::deque<std::condition_variable*> q;
    std::condition_variable q_empty;
public:
    void lock() {
        std::unique_lock<std::mutex> acquire(q_lock);
        std::condition_variable cv;
        q.push_back(&cv);
        q_empty.notify_one();
        cv.wait(acquire);
    }

    void unlock() {
        std::unique_lock<std::mutex> acquire(q_lock);
        while (q.size() == 0)
            q_empty.wait(acquire);

        std::condition_variable *cv  = q.front();
        q.pop_front();
        cv->notify_one();
    }

    int size() {
        std::unique_lock<std::mutex> acquire(q_lock);
        return q.size();
    }
};

Это позволяет потоку ждать отдельной переменной условия. Поскольку существует только один поток, ожидающий любой переменной условия, легко выбрать конкретный поток для пробуждения.

Хотя, я не знаю, если это масштабируется и если есть ограничение на максимальное количество переменных условия в одной программе.


Чтобы решить возникшие проблемы, я написал небольшую тестовую программу

#include <thread>

ordered_lock g;

void t()
{
    g.lock();
}

int main(int argc, char **argv)
{
    std::thread t1(t);
    std::thread t2(t);
    std::thread t3(t);

    while (g.size() < 3) {
        // busy waiting for the threads to lock in any order
    }

    while (g.size() > 0) {
        // unlock threads in lock order
        g.unlock();
    }

    t1.join();
    t2.join();
    t3.join();

    return 0;
}

С несколькими отладочными операциями печати в lock(), каждый может видеть, что это не работает, хотя и не по указанным причинам.

Это не работает из-за политик планирования ОС. Когда мы предполагаем, что порядок блокировки t1, t2, t3, он может (и делает), что основной поток разблокирует t1 и t2, затем t2 переносится,...

main: g.unlock() // t1
main: g.unlock() // t2
t2: wakeup
main: g.unlock() // t3
t1: wakeup
t3: wakeup

или в любом другом порядке, даже желаемый

main: g.unlock() // t1
t1: wakeup
main: g.unlock() // t2
t2: wakeup
main: g.unlock() // t3
t3: wakeup

К счастью, мы можем исправить ordered_lock и принудительно установить желаемый порядок, перемещая q.pop_front() в метод lock()

void lock() {
    std::unique_lock<std::mutex> acquire(q_lock);
    std::condition_variable cv;
    q.push_back(&cv);
    q_empty.notify_one();
    cv.wait(acquire);
    q.pop_front();
}

void unlock() {
    std::unique_lock<std::mutex> acquire(q_lock);
    while (q.size() == 0)
        q_empty.wait(acquire);

    std::condition_variable *cv  = q.front();
    cv->notify_one();
}

Удаление переменной условия из очереди внутри lock() гарантирует, что ни один другой поток не может разбудить перед запуском переднего потока.

Но это, конечно, имеет и другие последствия.

  • В зависимости от программы может быть гораздо больше unlock, чем предыдущие lock s.
  • Если работа не выполняется внутри lock (или до q.pop_front()), может произойти перепланирование на другой поток, что делает "исправление" спорным.

Вы также можете позаботиться об этих проблемах, но для "простого" ответа это слишком долго.

Ответ 2

Его довольно легко построить объект блокировки, который использует пронумерованные билеты, чтобы гарантировать, что его вполне справедливый (блокировка предоставляется в потоках заказа, сначала попыталась ее приобрести):

#include <mutex>
#include <condition_variable>

class ordered_lock {
    std::condition_variable  cvar;
    std::mutex               cvar_lock;
    unsigned int             next_ticket, counter;
public:
    ordered_lock() : next_ticket(0), counter(0) {}
    void lock() {
        std::unique_lock<std::mutex> acquire(cvar_lock);
        unsigned int ticket = next_ticket++;
        while (ticket != counter)
            cvar.wait(acquire);
    }
    void unlock() {
        std::unique_lock<std::mutex> acquire(cvar_lock);
        counter++;
        cvar.notify_all();
    }
};

изменить

Чтобы исправить предложение Олафа:

#include <mutex>
#include <condition_variable>
#include <queue>

class ordered_lock {
    std::queue<std::condition_variable>  cvar;
    std::mutex                           cvar_lock;
    bool                                 locked;
public:
    ordered_lock() : locked(false) {};
    void lock() {
        std::unique_lock<std::mutex> acquire(cvar_lock);
        if (locked) {
            cvar.emplace();
            cvar.back().wait(acquire);
        } else {
            locked = true;
        }
    }
    void unlock() {
        std::unique_lock<std::mutex> acquire(cvar_lock);
        if (cvar.empty()) {
            locked = false;
        } else {
            cvar.front().notify_one();
            cvar.pop();
        }
    }
};

Ответ 3

Мы задаем правильные вопросы по этой теме??? И если да: правильно ли они отвечают???

Или иначе:

Я совершенно неправильно понял здесь?

Изменить абзац: Кажется, StatementOnOrder (см. ниже) является ложным. См. link1 (потоки С++ и т.д. Под Linux основаны на pthreads) и link2 (упоминает текущую политику планирования как определяющий фактор) - благодаря Cubbi из cppreference ( ref), См. Также ссылка, ссылка, ссылка, . Если утверждение ложно, то, вероятно, предпочтительнее использовать метод вытягивания бита атома (!), Как показано в приведенном ниже коде.

Здесь идет...

StatementOnOrder: "Несколько потоков, которые запускаются в заблокированный мьютекс и, таким образом," идут спать "в определенном порядке, впоследствии получат право владения мьютексом и продолжают работать в том же порядке".

Вопрос: StatementOnOrder true или false

void myfunction() {
    std::lock_guard<std::mutex> lock(mut);

    // do something
    // ...
    // mutex automatically unlocked when leaving funtion.
}

Я спрашиваю об этом, потому что все примеры кода на этой странице на сегодняшний день выглядят как:

a) отходы (если StatementOnOrder истинно)

или

b) серьезно неверно (если StatementOnOrder).

Так почему же говорят, что они могут "серьезно ошибаться", если StatementOnOrder - false?
Причина в том, что все примеры кода считают, что они супер-умны, используя std::condition_variable, но на самом деле используют блокировки до этого, что будет (если StatementOnOrder ложно) испортить заказ!!!
Просто найдите эту страницу для std::unique_lock<std::mutex>, чтобы увидеть иронию.

Итак, если StatementOnOrder действительно ошибочно, вы не можете запускать блокировку, а затем обрабатывать билеты и материалы condition_variables после этого. Вместо этого вам нужно будет сделать что-то вроде этого: вытащите атомный билет перед запуском любой блокировки!!!
Зачем тянуть билет, прежде чем наткнуться на замок? Потому что здесь мы принимаем StatementOnOrder как ложное, поэтому любое упорядочение должно выполняться до блокировки "зла".

#include <mutex>
#include <thread>
#include <limits>
#include <atomic>
#include <cassert>
#include <condition_variable>
#include <map>

std::mutex mut;
std::atomic<unsigned> num_atomic{std::numeric_limits<decltype(num_atomic.load())>::max()};
unsigned num_next{0};
std::map<unsigned, std::condition_variable> mapp;

void function() {
    unsigned next = ++num_atomic; // pull an atomic ticket

    decltype(mapp)::iterator it;

    std::unique_lock<std::mutex> lock(mut);
    if (next != num_next) {
        auto it = mapp.emplace(std::piecewise_construct,
                               std::forward_as_tuple(next),
                               std::forward_as_tuple()).first;
        it->second.wait(lock);
        mapp.erase(it);
    }



    // THE FUNCTION INTENDED WORK IS NOW DONE
    // ...
    // ...
    // THE FUNCTION INDENDED WORK IS NOW FINISHED

    ++num_next;

    it = mapp.find(num_next); // this is not necessarily mapp.begin(), since wrap_around occurs on the unsigned                                                                          
    if (it != mapp.end()) {
        lock.unlock();
        it->second.notify_one();
    }
}

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

Но главный вопрос: Является StatementOnOrder true или false??? (Если это так, то мой пример кода выше также является отходами, и мы можем просто использовать мьютекс и делать с ним.)
Я хочу, чтобы кто-то вроде Энтони Уильямс просмотрел эту страницу...;)