Как сделать блокировку многократного чтения/одной записи из более простых примитивов синхронизации?

Мы обнаружили, что в нашем коде есть несколько мест, где одновременное чтение данных, защищенных мьютексом, встречается довольно часто, а запись — редко. Наши измерения, похоже, говорят о том, что использование простого мьютекса серьезно снижает производительность кода, считывающего эти данные. Итак, нам понадобится мьютекс с множественным чтением/одной записью. Я знаю, что это можно построить на основе более простых примитивов, но прежде чем я попробую это сделать, я бы лучше попросил существующие знания:

Какой утвержденный способ создания блокировки многократного чтения/одной записи из более простых примитивов синхронизации?

У меня есть идея, как это сделать, но я бы предпочел, чтобы ответы были непредвзяты к тому, что я (возможно, ошибочно) придумал. (Примечание: я ожидаю объяснения, как это сделать, возможно, в псевдокоде, а не полноценной реализации. Я, конечно, могу написать код сам.)

Предупреждения:

  • Это должно иметь разумную производительность. (То, что я имею в виду, потребовало бы двух операций блокировки/разблокировки для каждого доступа. Теперь этого может быть недостаточно, но необходимость многих из них вместо этого кажется неразумной.)

  • Как правило, операций чтения больше, но операции записи более важны и чувствительны к производительности, чем операции чтения. Читатели не должны морить голодом писателей.

  • Мы застряли на довольно старой встраиваемой платформе (проприетарный вариант VxWorks 5.5), с довольно старым компилятором (GCC 4.1.2) и boost 1.52 — за исключением большинства частей boost, основанных на POSIX, потому что POSIX реализован не полностью. на той платформе. Доступные блокирующие примитивы в основном представляют собой несколько видов семафоров (двоичные, счетные и т. д.), поверх которых мы уже создали мьютексы, переменные условий и мониторы.

  • Это IA32, одноядерный.


person sbi    schedule 09.01.2015    source источник
comment
Это должно иметь разумную производительность - так что обычный мьютекс имеет разумную производительность? Почему нет?   -  person sehe    schedule 23.01.2015
comment
@sehe: Когда под капотом такой реализации читателю необходимо заблокировать и освободить три мьютекса для доступа к данным, то это неразумная производительность.   -  person sbi    schedule 23.01.2015
comment
Запатентованная версия VxWorks — какая платформа — IA32, IA64 или другая платформа, поддерживаемая VxWorks (PPC/ARM/и т. д.)?   -  person frasnian    schedule 25.01.2015
comment
@frasnian: это IA32.   -  person sbi    schedule 25.01.2015
comment
@sbi как часть ваших инструментов разработки. У вас есть WindView? Это очень классный графический способ точно увидеть, что происходит в вашей системе, может помочь выяснить, в первую очередь, куда идет производительность. Подумайте о ftrace (или dtrace), но на целую кучу лучше.   -  person bazza    schedule 28.01.2015
comment
@sbi Просто мысль - вам не нужно создавать свои собственные мьютексы и условные переменные; они уже реализованы в VxWorks. Есть встроенный мьютекс (см. SemMCreate()) и условные переменные POSIX (см. semPxLib). Если у вас его нет, обратитесь к руководству программиста по адресу ing.iac.es/~docs/external/vxworks.old/Programmers-Guide-5.5.pdf   -  person bazza    schedule 28.01.2015
comment
@bazza: Нет, у нас есть проприетарная IDE (на основе более старой версии Eclipse). Но мы больше не используем это ни для чего, кроме развертывания, вместо этого внедрив нашу собственную (на основе SCons) систему сборки в vanilla eclipse.   -  person sbi    schedule 28.01.2015
comment
@bazza На самом деле, semMCreate() (префикс sem означает семафор) — это именно то, во что мы обернули наш тонкий класс mutex. Переменные условия, однако, мы основывали на бинарных семафорах. Учитывая, что большая часть поддержки POSIX отсутствует, я не вижу большого смысла в использовании semPxLib, который, как вы (?) предложили в другом месте, скорее всего, является просто тонким слоем поверх двоичных семафоров, которые мы могли бы также обернуть сами.   -  person sbi    schedule 28.01.2015
comment
@sbi, ах, обертка C ++, я понимаю. Да, это был я. Жаль, что у вас нет WindView, это значительно упрощает диагностику. IDE, основанная на старом Eclipse, звучит «неофициально»; Я надеюсь, что ваш поставщик действительно продает вам лицензии на выполнение. Инструменты разработки WindRiver очень дороги (возможно, поэтому ваш поставщик предоставил что-то другое), но они того стоили в моих проектах (при отладке экономия рабочей силы была довольно высокой). Возможно, стоит попробовать идею счетного семафора — легко заменить существующий мьютекс. Если быстрее, результат! Если нет, то не так много времени потрачено впустую на попытки.   -  person bazza    schedule 28.01.2015
comment
@sbi Вам может быть полезно обновить свой вопрос с информацией о вашей платформе (процессор, ОС, если есть ...). Хотя вы запрашиваете обобщенный псевдокод, я подозреваю, что некоторые из ребят здесь могут дать вам существующее решение для вашей платформы, которого будет достаточно... Сначала я думал о блокировке в стиле RCU, но это действительно зависит от какая у вас система...   -  person nonsensickle    schedule 29.01.2015
comment
@sbi Хотя это и не псевдокод, и даже не правильное объяснение, глядя на sqlite.org/lockingv3.html может предложить альтернативу пользовательскому дизайну.   -  person nonsensickle    schedule 29.01.2015
comment
@nonsensickle: я добавил заявление о том, что здесь мы говорим об одноядерном IA32.   -  person sbi    schedule 29.01.2015
comment
@bazza: Я надеюсь, что ваш поставщик на самом деле продает вам лицензии на выполнение. Я не уверен, как это понять, но я определенно считаю, что VxWorks, которые они продают, полностью лицензированы. (Это неплохой поставщик. Мы застряли на старой версии VxWorks не потому, что не могли искать что-то лучше, а потому, что есть другие веские причины, чтобы придерживаться продуктов этой компании. Просто у них есть — хорошо! — проприетарная платформа поверх VxWorks, которую они не портировали на более новую версию VxWorks.)   -  person sbi    schedule 29.01.2015
comment
@sbi, по моему опыту, не каждый поставщик, поставляющий VxWorks, действительно имел на это лицензию от WindRiver! Сама ОС не имеет встроенных средств управления лицензированием, но инструменты разработки имеют их, и WindRiver может проверять использование лицензий. Однако авторитетные поставщики должны быть в порядке. VxWorks не продвинулся далеко, теперь он добрался только до версии 7.   -  person bazza    schedule 30.01.2015
comment
Это решение на C++11+ на основе стандартной библиотеки: linkedin.com/pulse/ . Я думаю, вам придется переписать его, используя pthreads. Но я задаюсь вопросом, насколько хорошо это будет для одного ядра. С одним ядром вы не можете на самом деле иметь двух одновременных считывателей. У вас может быть только несколько одновременно выполняемых потоков чтения, но одно ядро ​​может запускать только один поток. Так что, в лучшем случае, я думаю, вы увидите небольшое сокращение максимальной задержки чтения.   -  person WaltK    schedule 07.01.2020


Ответы (8)


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

readers - readers in the cv readerQ plus the reading reader
writers - writers in cv writerQ plus the writing writer
active_writers - the writer currently writing. can only be 1 or 0.

Так голодают читатели. Если есть несколько писателей, желающих писать, у читателей никогда не будет возможности прочитать, пока все писатели не закончат писать. Это потому, что более поздние читатели должны проверить переменную writers. В то же время переменная active_writers будет гарантировать, что одновременно может писать только один писатель.

class RWLock {
public:
    RWLock()
    : shared()
    , readerQ(), writerQ()
    , active_readers(0), waiting_writers(0), active_writers(0)
    {}

    void ReadLock() {
        std::unique_lock<std::mutex> lk(shared);
        while( waiting_writers != 0 )
            readerQ.wait(lk);
        ++active_readers;
        lk.unlock();
    }

    void ReadUnlock() {
        std::unique_lock<std::mutex> lk(shared);
        --active_readers;
        lk.unlock();
        writerQ.notify_one();
    }

    void WriteLock() {
        std::unique_lock<std::mutex> lk(shared);
        ++waiting_writers;
        while( active_readers != 0 || active_writers != 0 )
            writerQ.wait(lk);
        ++active_writers;
        lk.unlock();
    }

    void WriteUnlock() {
        std::unique_lock<std::mutex> lk(shared);
        --waiting_writers;
        --active_writers;
        if(waiting_writers > 0)
            writerQ.notify_one();
        else
            readerQ.notify_all();
        lk.unlock();
    }

private:
    std::mutex              shared;
    std::condition_variable readerQ;
    std::condition_variable writerQ;
    int                     active_readers;
    int                     waiting_writers;
    int                     active_writers;
};
person qqibrow    schedule 24.01.2015
comment
На самом деле все, что у нас есть, это семафоры, остальное построено поверх них. (Кроме того, у нас нет материалов синхронизации C++ 11/14, но я храню их под псевдокодом.) В любом случае, это очень похоже на то, что я имел в виду, только вы показываете, что для управления нужен только мьютекс. данные блокировки, и ни один для фактического доступа к данным. Спасибо за это, это определенно бонусный кандидат! - person sbi; 24.01.2015
comment
@сби. Одно улучшение, которое я мог бы придумать, это дать больше привилегий поздним авторам. Когда приходит писатель, поместите его в начало очереди мьютекса. Таким образом, вы используете deque в качестве базовой структуры данных в мьютексе. Тогда каждый раз при вызове разблокировки автор будет выходить первым, даже если он опоздает. - person qqibrow; 24.01.2015
comment
Просмотрев все ответы вместе с коллегой, я решил дать бонус этому ответу, потому что алгоритм здесь кажется достаточно надежным, был опубликован раньше, чем аналогичный алгоритм Говарда, и имеет то преимущество перед алгоритмом Говарда, что он выполняет требование предпочтения писателей. . - person sbi; 30.01.2015
comment
Однако обратите внимание, что у нас были серьезные проблемы, заставляющие нас давать бонус к ответу, что спортивный код, который не может даже решить, использует ли он пре-инкремент (потому что это лучше) или пост-инкремент (потому что это традиционно), использовать фигурные скобки или нет для однострочных тел условий явно быстро разблокирует блокировки, потому что они выходят за рамки, а также имеет другие проблемы со стилем. У меня возникает соблазн навести порядок в этом беспорядке только для того, чтобы другие не думали обо мне плохо, потому что я посвятил в рыцари код, который настолько плох. Для программиста на C++ это действительно ужасно плохой код. - person sbi; 30.01.2015
comment
@sbi Спасибо, что приняли мой ответ. Однако, несмотря на то, что я только что вошел в индустрию, я не думаю, что мой код так уж плох. в любом случае, не стесняйтесь менять код, и я увижу, что можно улучшить. - person qqibrow; 30.01.2015
comment
Я унифицировал стиль крепления, изменил все на преинкремент, теперь все члены инициализируются явно, а две переменные переименовали, чтобы сделать их значение более очевидным. (Примечание: я не спорю о том, где и когда ставятся фигурные скобки или запятые, поэтому не стесняйтесь менять это. Но я забочусь о согласованности.) - person sbi; 19.06.2015

На первый взгляд мне показалось, что я узнал этот ответ как тот же алгоритм, который представил Александр Терехов. Но изучив его, я считаю, что он ошибочен. Два записывающих устройства могут одновременно ожидать m_exclusive_cond. Когда один из этих модулей записи просыпается и получает эксклюзивную блокировку, он устанавливает exclusive_waiting_blocked = false на unlock, тем самым переводя мьютекс в несогласованное состояние. После этого мьютекс, скорее всего, будет закрыт.

N2406, который первым предложенный std::shared_mutex содержит частичную реализацию, которая повторяется ниже с обновленным синтаксисом.

class shared_mutex
{
    mutex    mut_;
    condition_variable gate1_;
    condition_variable gate2_;
    unsigned state_;

    static const unsigned write_entered_ = 1U << (sizeof(unsigned)*CHAR_BIT - 1);
    static const unsigned n_readers_ = ~write_entered_;

public:

    shared_mutex() : state_(0) {}

// Exclusive ownership

    void lock();
    bool try_lock();
    void unlock();

// Shared ownership

    void lock_shared();
    bool try_lock_shared();
    void unlock_shared();
};

// Exclusive ownership

void
shared_mutex::lock()
{
    unique_lock<mutex> lk(mut_);
    while (state_ & write_entered_)
        gate1_.wait(lk);
    state_ |= write_entered_;
    while (state_ & n_readers_)
        gate2_.wait(lk);
}

bool
shared_mutex::try_lock()
{
    unique_lock<mutex> lk(mut_, try_to_lock);
    if (lk.owns_lock() && state_ == 0)
    {
        state_ = write_entered_;
        return true;
    }
    return false;
}

void
shared_mutex::unlock()
{
    {
    lock_guard<mutex> _(mut_);
    state_ = 0;
    }
    gate1_.notify_all();
}

// Shared ownership

void
shared_mutex::lock_shared()
{
    unique_lock<mutex> lk(mut_);
    while ((state_ & write_entered_) || (state_ & n_readers_) == n_readers_)
        gate1_.wait(lk);
    unsigned num_readers = (state_ & n_readers_) + 1;
    state_ &= ~n_readers_;
    state_ |= num_readers;
}

bool
shared_mutex::try_lock_shared()
{
    unique_lock<mutex> lk(mut_, try_to_lock);
    unsigned num_readers = state_ & n_readers_;
    if (lk.owns_lock() && !(state_ & write_entered_) && num_readers != n_readers_)
    {
        ++num_readers;
        state_ &= ~n_readers_;
        state_ |= num_readers;
        return true;
    }
    return false;
}

void
shared_mutex::unlock_shared()
{
    lock_guard<mutex> _(mut_);
    unsigned num_readers = (state_ & n_readers_) - 1;
    state_ &= ~n_readers_;
    state_ |= num_readers;
    if (state_ & write_entered_)
    {
        if (num_readers == 0)
            gate2_.notify_one();
    }
    else
    {
        if (num_readers == n_readers_ - 1)
            gate1_.notify_one();
    }
}

Алгоритм взят из старой записи группы новостей Александра Терехова. Она не уморит голодом ни читателей, ни писателей.

Есть двое «ворот», gate1_ и gate2_. Читатели и писатели должны пройти gate1_ и могут быть заблокированы при попытке сделать это. Как только читатель проходит gate1_, он блокирует чтение мьютекса. Читатели могут пройти дальше gate1_, пока не будет достигнуто максимальное количество читателей с правом собственности, и пока писатель не превысит gate1_.

Только один писатель за раз может пройти дальше gate1_. И писатель может преодолеть gate1_, даже если читатели владеют им. Но после gate1_ писатель по-прежнему не имеет права собственности. Сначала он должен пройти gate2_. Писатель не может пройти дальше gate2_, пока все читатели, владеющие им, не откажутся от него. Напомним, что новые читатели не могут пройти дальше gate1_, пока писатель ждет в gate2_. И новый писатель не может пройти дальше gate1_, пока писатель ждет в gate2_.

То, что и читатели, и писатели блокируются в gate1_ с (почти) одинаковыми требованиями, предъявляемыми для его преодоления, делает этот алгоритм справедливым как для читателей, так и для писателей, не голодая ни для одного из них.

«Состояние» мьютекса намеренно сохранено в одном слове, чтобы предположить, что частичное использование атомарности (в качестве оптимизации) для определенных изменений состояния возможно (т. Е. Для неоспариваемого «быстрого пути»). Однако эта оптимизация здесь не демонстрируется. Например, если поток записи может атомарно изменить state_ с 0 на write_entered, тогда он получит блокировку без необходимости блокировать или даже блокировать/разблокировать mut_. И unlock() можно реализовать с атомарным хранилищем. И т. д. Эти оптимизации не показаны здесь, потому что их гораздо сложнее правильно реализовать, чем это кажется простым описанием.

person Howard Hinnant    schedule 25.01.2015
comment
Действительно ли недостаток, на который вы ссылаетесь, считается недостатком, учитывая, что здесь требуется разрешить только одного писателя? - person Jerry Coffin; 26.01.2015
comment
Я, по общему признанию, не потратил время на полный анализ. По крайней мере, я вижу возможность, когда этот второй ожидающий писатель будет скрыт от читателей, и поэтому читатели могут морить его голодом. Или под одним писателем вы имеете в виду только одну нить писателя? Если это требование, я пропустил это. Алгоритм в моем ответе может иметь много потоков чтения, много потоков записи, позволяет многим читателям получить блокировку или одному писателю получить блокировку, и ни один из них не голодает. - person Howard Hinnant; 26.01.2015
comment
Первоначально я думал, что это означает, что существует только один поток авторов, но после повторного прочтения вопроса я намного менее уверен в этом. - person Jerry Coffin; 26.01.2015
comment
@Jerry: я только что нашел место в коде, где много авторов/продюсеров, но только один читатель/потребитель... :( - person sbi; 27.01.2015
comment
@sbi В любом случае я не уверен, что это так же эффективно, как использование одного счетного семафора. Переменные условия реализованы в VxWorks с использованием еще пары семафоров. Это означало бы, что читатели касаются более чем одного семафора. Хотя писатель может быть более эффективным. - person bazza; 28.01.2015
comment
@sbi, предположительно, там, где много писателей, они не должны писать одновременно? Я думаю, что в этом случае это решение Говарда все еще будет работать. О, если вас беспокоят unique_lock и lock_guard, ту же функциональность можно получить от SemTake() и SemGive(). - person bazza; 28.01.2015
comment
В случае, если это непонятно читателям, случай многих авторов и только одного читателя эквивалентен монопольному доступу. С тем же успехом можно было бы использовать mutex. Предполагая абсолютно одинаковую эффективность реализации мьютекса и shared_mutex, использование shared_mutex никогда не даст выигрыша в производительности. И если shared_mutex окажется менее эффективным (потому что он сложнее), то по умолчанию побеждает мьютекс. Если shared_mutex окажется более эффективным, чем мьютекс, то это просто ошибка в реализации. - person Howard Hinnant; 28.01.2015
comment
За это проголосовали много, и я почти безоговорочно верю, что Ховард опубликует алгоритм, на который смотрели годами, но я не вижу, что это улучшение по сравнению с ответом qqibrow, который старше, и я верю, что голоса в основном исходят от другие люди также безоговорочно доверяют Ховарду. Поэтому я не буду давать бонус за этот ответ. - person sbi; 30.01.2015
comment
@sbi Преимущество этого алгоритма в том, что он не голодает по потокам записи. Если у вас есть постоянный поток читателей с другим алгоритмом, ни один писатель никогда не получит блокировку. Этот алгоритм кажется невосприимчивым к этому. - person Voo; 15.02.2016

Параллельное чтение данных, защищенных мьютексом, довольно распространено, а запись — редко.

Это звучит как идеальный сценарий для RCU пользовательского пространства:

URCU аналогичен своему аналогу ядра Linux, обеспечивая замену блокировки чтения-записи, среди прочего. Это сходство продолжается и в том, что считыватели не синхронизируются напрямую с программами обновления RCU, что делает пути кода на стороне чтения RCU чрезвычайно быстрыми, а также позволяет программам чтения RCU продвигаться вперед, даже если они работают одновременно с программами обновления RCU, и наоборот.

person Maxim Egorushkin    schedule 23.01.2015
comment
И что можно использовать на платформе 90-х, которая даже не поддерживает POSIX? - person sbi; 23.01.2015
comment
@sbi Это верно. Вы можете проверить его и посмотреть, работает ли он. - person Maxim Egorushkin; 23.01.2015
comment
Я не думаю, что добавлю в эту кодовую базу до сих пор совершенно неизвестную библиотеку, если подойдет хорошо написанная rw-lock. (Мы делаем чувствительные к ошибкам вещи.) - person sbi; 24.01.2015
comment
@sbi Идея RCU довольно проста и понятна. Читатели используют атомарную инструкцию для чтения указателя. Писатели атомарно обновляют указатель до нового значения. Единственная трудность — избавиться от старого значения, и есть несколько способов сделать это, это то, что подробно исследуется в этих официальных документах. - person Maxim Egorushkin; 24.01.2015
comment
Мы не нашли атомарных операций в API этой ОС. - person sbi; 24.01.2015
comment
@sbi: атомарные операции обычно не выполняются в API ОС - в IA32 есть машинные коды операций, и если вы не хотите компилировать/связывать какой-либо ассемблер, ищите внутренние/встроенные модули компилятора C++ или встроенный ассемблер. Google быстро выдает инструкции ЦП — например. здесь - person Tony Delroy; 27.01.2015
comment
@sbi Я понимаю, что вы осторожны, но, пожалуйста, посмотрите это видео, прежде чем делать свой разум вверх. Он очень хорошо объясняет концепцию и должен облегчить вам ее использование. - person nonsensickle; 30.01.2015
comment
Я обсудил это со своим коллегой: 1. Это потребует добавления в код новой и неизвестной нам библиотеки, чего мы делать не хотим. 2. Мы считаем, что в нашем коде есть места, где продолжение работы со старыми значениями после того, как новые доступны для некоторого значения слишком долго, может быть очень фатальным для делать. 3. Это не похоже на ответ человека, который использовал этот подход в течение многих лет, знает его все тонкости и с энтузиазмом объясняет, как его использовать, если мы задаемся вопросом о конкретном случае. Вместо этого это просто ссылка на библиотеку и цитата... - person sbi; 30.01.2015
comment
... который даже не считается хорошим стилем ответа на SO. 4. Кажется, что это требует динамического распределения, что является дополнительными затратами, которые мы не можем себе позволить во всех случаях, для которых мы хотим использовать это. Поэтому этот ответ не получит бонус. - person sbi; 30.01.2015
comment
@sbi Нет, этот механизм не требует динамического распределения. - person Maxim Egorushkin; 30.01.2015
comment
@Maxim: Однако это было наше впечатление. Хотя это может быть неправильно, ответ, конечно, не прояснил это. - person sbi; 30.01.2015
comment
@sbi Я почти чувствую твою боль :) - person Maxim Egorushkin; 30.01.2015

Есть несколько хороших трюков, которые вы можете использовать, чтобы помочь.

Во-первых, хорошая производительность. VxWorks отличается очень хорошим временем переключения контекста. Какое бы решение блокировки вы ни использовали, оно, скорее всего, будет включать семафоры. Я бы не боялся использовать для этого семафоры (множественное число), они довольно хорошо оптимизированы в VxWorks, а быстрое время переключения контекста помогает минимизировать снижение производительности из-за оценки многих состояний семафора и т. д.

Также я бы забыл об использовании семафоров POSIX, которые просто будут располагаться поверх собственных семафоров VxWork. VxWorks предоставляет собственные счетные, двоичные и мьютексные семафоры; использование того, который подходит, делает все немного быстрее. Бинарные иногда могут быть весьма полезными; отправлено много раз, никогда не превышайте значение 1.

Во-вторых, запись важнее чтения. Когда у меня были такие требования в VxWorks и я использовал семафор(ы) для управления доступом, я использовал приоритет задачи, чтобы указать, какая задача важнее и должна получить доступ к ресурсу первой. Это работает довольно хорошо; буквально все в VxWorks это задача (ну и поток) как и любая другая, включая все драйвера устройств и т.д.

VxWorks также разрешает инверсии приоритетов (такие вещи, которые ненавидит Линус Торвальдс). Таким образом, если вы реализуете блокировку с помощью семафора(ов), вы можете положиться на планировщик ОС, который будет блокировать операции чтения с более низким приоритетом, если они блокируют запись с более высоким приоритетом. Это может привести к гораздо более простому коду, и вы также получите максимальную отдачу от ОС.

Таким образом, потенциальное решение состоит в том, чтобы иметь один счетный семафор VxWorks, защищающий ресурс, инициализированный значением, равным количеству читателей. Каждый раз, когда читатель хочет прочитать, он берет семафор (уменьшая счетчик на 1. Каждый раз, когда выполняется чтение, он отправляет семафор, увеличивая счетчик на 1. Каждый раз, когда пишущий хочет записать, он берет семафор n (n = количество читателей) раз и публикует его n раз, когда закончите. Наконец, сделайте задачу записи более приоритетной, чем любой из читателей, и полагайтесь на быстрое время переключения контекста ОС и инверсию приоритета.

Помните, что вы программируете на ОС жесткого реального времени, а не на Linux. Получение/публикация собственного семафора VxWorks не требует такого же количества времени выполнения, как аналогичное действие в Linux, хотя даже Linux в наши дни довольно хорош (я использую PREEMPT_RT в настоящее время). На поведение планировщика VxWorks и всех драйверов устройств можно положиться. Вы даже можете сделать свою задачу записи наивысшим приоритетом во всей системе, если хотите, даже выше, чем у всех драйверов устройств!

Чтобы помочь делу, также подумайте, что делает каждый из ваших потоков. VxWorks позволяет указать, что задача использует или не использует FPU. Если вы используете собственные подпрограммы VxWorks TaskSpawn вместо pthread_create, у вас есть возможность указать это. Это означает, что если ваш поток/задача не выполняет математических вычислений с плавающей запятой, и вы сказали об этом в своем вызове TaskSpawn, время переключения контекста будет еще быстрее, потому что планировщик не будет заботиться о сохранении / восстановить состояние FPU.

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

В-третьих, застрял со старым GCC и старым Boost. По сути, я не могу помочь, кроме малоценных предложений о том, чтобы позвонить в WindRiver и обсудить покупку обновления. Лично говоря, когда я программировал для VxWorks, я использовал собственный API VxWork, а не POSIX. Итак, код не очень переносимый, но в итоге он стал быстрым; В любом случае POSIX — это просто слой поверх собственного API, и это всегда будет замедлять работу.

Тем не менее, семафоры подсчета и мьютекса POSIX очень похожи на собственные семафоры подсчета и мьютекса VxWork. Вероятно, это означает, что слои POSIX не очень толстые.

Общие примечания о программировании для VxWorks

Отладка Я всегда старался использовать инструменты разработки (Tornado), доступные для Solaris. Это, безусловно, лучшая многопоточная среда отладки, с которой я когда-либо сталкивался. Почему? Это позволяет запускать несколько сеансов отладки, по одному для каждого потока/задачи в системе. В итоге вы получаете окно отладки для каждого потока, и вы индивидуально и независимо отлаживаете каждый из них. Перешагнув операцию блокировки, это окно отладки будет заблокировано. Переместите фокус мыши на другое окно отладки, пройдитесь по операции, освобождающей блок, и наблюдайте, как первое окно завершает свой шаг.

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

По иронии судьбы версия Tornado для Windows не позволяла вам этого сделать; одно жалкое одиночное окно отладки на систему, как и любая другая скучная старая IDE, такая как Visual Studio и т. д. Я никогда не видел, чтобы даже современные IDE приближались к тому, чтобы быть такими же хорошими, как Tornado на Solaris для многопоточной отладки.

Жесткие диски Если ваши программы чтения и записи используют файлы на диске, учтите, что VxWorks 5.5 устарел. Такие вещи, как NCQ, не будут поддерживаться. В этом случае мое предложенное решение (изложенное выше) может быть лучше реализовано с одним семафором мьютекса, чтобы предотвратить спотыкание нескольких читателей друг о друга в их борьбе за чтение разных частей диска. Это зависит от того, что именно делают ваши читатели, но если они читают непрерывные данные из файла, это позволит избежать тряски головки чтения/записи туда и обратно по поверхности диска (очень медленно).

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

person bazza    schedule 27.01.2015
comment
Спасибо, там есть несколько очень хороших советов (например, рассмотреть возможность подсчета семафоров). Тем не менее, мы не можем переключать платформу, и это VxWorks 5.5, который, я полагаю, 1996 года. В нем реализовано очень мало POSIX-материалов, нет потоков (нет защиты памяти между задачами), и он связан с платформой, на которой мы работаем. обязан. (Вот тут и пригодится проприетарная версия.) Вызов Windriver не поможет, а производитель оборудования не будет обновляться. Тем не менее, одна только идея счетного семафора делает этот ответ очень полезным. Любая критика за это? - person sbi; 27.01.2015
comment
@sbi, не беспокойся; Да, VxWorks 5.5. примерно с 1996 года. По сути, все представляет собой нить; каждая задача может видеть глобальные символы любой другой задачи. Делает это очень быстрым при переключении контекста, потому что при переключении меньше приходится делать с MMU. Чисто ради интереса VxWorks 6 ввела защиту памяти. Кстати, что это за платформа процессора? PowerPC? - person bazza; 27.01.2015
comment
@sbi, обратите внимание, что предлагаемое мной решение не требует смены платформы. В VxWorks 5.5 есть счетные семафоры. Также, пожалуйста, не обращайте внимания на мой вопрос об использовании PowerPC. Я заметил из другого вашего поста, что вы используете IA32. - person bazza; 27.01.2015
comment
В этих коробках есть флешки. Когда нам нужно записать много данных, мы используем для этого отдельную задачу, потому что время записи, хотя в среднем хорошее, может показывать очень плохие всплески. (Мне сказали, что это унаследовано от флешек.) Данные в основном приходят и уходят через CAN, Modbus, Profibus или Profinet, все они обрабатываются в своих задачах, и мы не обнаружили здесь принципиальных проблем с нашими решениями. Я обнаружил, что лучший способ отладки сильно параллельных приложений — хорошая трассировка. Что у нас есть. - person sbi; 29.01.2015
comment
Обсудив это с коллегой, он указал, что, поскольку счетный семафор не может быть взят n раз за одну операцию, предложенный вами алгоритм с их использованием не просто станет, скажем, неэффективным, а заблокируется, как только будет использовано более одного на сцену выходит писатель. Это очень плохо. Итак, если есть только один писатель, и это можно безопасно выразить в коде, чтобы код не компилировался при появлении другого, то просто счетный семафор — прекрасное решение. В противном случае, однако, я бы предпочел не создавать этот ад для программистов сопровождения. - person sbi; 30.01.2015
comment
@sbi, не волнуйся, все в порядке. Когда я писал это, я предполагал, что есть только один писатель. Если их два или более, то да, он зайдет в тупик. Если все они пишут в один и тот же ресурс, то вам, по-видимому, все равно понадобится какая-то форма взаимного исключения между ними, если только ресурс, на который записывается, не делает это внутри. Удачи! - person bazza; 30.01.2015
comment
@sbi Конечно, если вам нужно взаимное исключение между писателями, у вас может быть для них мьютекс. Только когда модуль записи получит мьютекс, он начнет пытаться получить счетный семафор. Решает проблему взаимоблокировки. Очевидно, что если нет необходимости во взаимном исключении между писателями, то это просто излишне замедлит всех писателей. Надеюсь, все будет хорошо! - person bazza; 30.01.2015

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

person Doug Binks    schedule 23.01.2015
comment
Я кратко рассмотрел это. Кажется, это зависит от атомов, которых, я думаю, у нас нет. Кроме того, IIUC, вращение означает деловитое ожидание. В среде реального времени вы не должны этого делать, потому что это потребляет ресурсы ЦП, приводя к голоданию задач с более низким приоритетом. Итак, спасибо за предложение, но я думаю, что это не то, что нам нужно. - person sbi; 24.01.2015
comment
Спин-блокировки могут использовать меньше ЦП, когда блокировки удерживаются лишь на короткое время, поскольку переключение контекста требует затрат (состояние ЦП необходимо сохранять/восстанавливать). Возможно, ваши мьютексы уже выполняют спин-блокировку, за которой следует стандартный мьютекс отсрочки, так что вы уже можете использовать их. Однако, если производительность является вашей целью, то для проверки следует выполнить тестирование и измерение. Если вы не уверены в поддержке атомарности в вашей системе, я бы посмотрел это. - person Doug Binks; 25.01.2015
comment
Спасибо за объяснение! Мы не уверены, потому что поиск ничего не выявил. - person sbi; 25.01.2015
comment
GCC 4.1.2 должен поддерживать встроенные функции __sync_val_compare_and_swap и __sync_add_and_fetch, чтобы вы могли проверить, недостаточно ли базовой блокировки чтения-записи с семафорами. - person Doug Binks; 26.01.2015
comment
Является ли атомарность чем-то, что обеспечивает аппаратное обеспечение или она должна обеспечиваться ОС? - person sbi; 27.01.2015
comment
@sbi atomics существует с 80486. Я более чем уверен, что вы сможете использовать atomics. - person BlamKiwi; 27.01.2015
comment
@Morphing: Ах, так это аппаратная штука. Я этого не знал! - person sbi; 27.01.2015
comment
@sbi Возможно, стоит получить версию Pre C++ 11 книги C++ Concurrency in Action, если вы пойдете по этому пути. Есть много вещей, которые нужно наверстать в отношении программирования без блокировки. - person BlamKiwi; 27.01.2015
comment
Я не советую использовать спин-блокировки в VxWorks. Не уверен, что он даже реализует их. Учитывая возраст платформы, я не удивлюсь, если @sbi использует одноядерное оборудование. - person bazza; 27.01.2015
comment
@bazza: Это определенно одноядерный процессор. :-/ Итак, вы думаете, что мое подозрение относительно спин-блокировок было верным (хотя, может быть, только случайно)? - person sbi; 27.01.2015
comment
@Morphing: Спасибо за предложение! Разве современная версия книги (которая уже где-то валяется) не объясняет этих вещей? - person sbi; 27.01.2015
comment
@sbi это было бы, но в примерах будут использоваться новые функции C ++, недоступные вам. - person BlamKiwi; 27.01.2015
comment
@sbi, если ваше оборудование является одноядерным, то вам, конечно же, не следует использовать спин-блокировку, так как вам нужен текущий поток для переключения контекста, чтобы позволить держателю блокировки завершиться. - person Doug Binks; 28.01.2015
comment
@sbi Спин-блокировки на одном ядре действительно означают, что больше ничего не работает, пока спин-блокировка не будет вытеснена по какой-либо другой причине. Никуда тебя не приведет. Весь смысл быстрого планировщика VxWork заключается в том, что вы можете использовать «правильные» вещи, такие как семафоры, с почти такой же производительностью, как что-то вроде спин-блокировки, но при этом иметь возможность выполнять многопоточные операции. - person bazza; 28.01.2015
comment
Ну, я с самого начала с подозрением относился к спин-блокировкам. У нас есть проекты, в которых нам трудно выполнить все наши коммуникации за несколько миллисекунд, отведенных нам на цикл, и где мы используем менее очевидные алгоритмические подходы, потому что мы обнаружили, что они снижают использование ЦП с 85% до 75%, поэтому Использование ЦП для нас является нарушителем условий сделки. Таким образом, особенно в свете бесспорного комментария Баззы о том, что спин-блокировки хуже на одноядерном оборудовании, это не получило бонуса. Спасибо за ответ в любом случае! - person sbi; 30.01.2015

Один из алгоритмов для этого, основанный на семафорах и мьютексах, описан в Concurrent Control with Читатели и писатели; П.Дж. Куртуа, Ф. Хейманс и Д.Л. Парнас; Исследовательская лаборатория MBLE; Брюссель, Бельгия.

person wilx    schedule 23.01.2015
comment
Это очень интересная статья, и я узнал, что статьи по CS, выпущенные до 80-х годов, практически невозможно доказать ошибочность. Тем не менее, читательский вариант этой статьи использует довольно много ресурсов, и все ответы, которые я собрал здесь до сих пор, далеко не такие дорогие. (Имейте в виду, это все еще может означать, что те старейшины были правы, а все остальные ошибались. Я просто не вижу, где они.) - person sbi; 27.01.2015
comment
Ну через недельку вроде там _are_simpler подходит. Те, что публикуют qqibrow, Howard и QuestionC, очень похожи и используют меньше ресурсов. Так что я не выбираю этот ответ. - person sbi; 30.01.2015
comment
Это может быть хорошим решением (не читал его), но это ответ только по ссылке. - person mjs; 10.04.2015

Это упрощенный ответ, основанный на моих заголовках Boost (я бы назвал Boost одобренным способом). Для этого требуются только переменные условия и мьютексы. Я переписал его, используя примитивы Windows, потому что считаю их описательными и очень простыми, но рассматриваю это как псевдокод.

Это очень простое решение, которое не поддерживает такие вещи, как обновление мьютекса или операции try_lock(). Я могу добавить их, если хотите. Я также убрал некоторые излишества, такие как отключение прерываний, которые не являются строго необходимыми.

Кроме того, стоит проверить boost\thread\pthread\shared_mutex.hpp (это основано на этом). Это удобочитаемо для человека.

class SharedMutex {
  CRITICAL_SECTION m_state_mutex;
  CONDITION_VARIABLE m_shared_cond;
  CONDITION_VARIABLE m_exclusive_cond;

  size_t shared_count;
  bool exclusive;

  // This causes write blocks to prevent further read blocks
  bool exclusive_wait_blocked;

  SharedMutex() : shared_count(0), exclusive(false)
  {
    InitializeConditionVariable (m_shared_cond);
    InitializeConditionVariable (m_exclusive_cond);
    InitializeCriticalSection (m_state_mutex);
  }

  ~SharedMutex() 
  {
    DeleteCriticalSection (&m_state_mutex);
    DeleteConditionVariable (&m_exclusive_cond);
    DeleteConditionVariable (&m_shared_cond);
  }

  // Write lock
  void lock(void)
  {
    EnterCriticalSection (&m_state_mutex);
    while (shared_count > 0 || exclusive)
    {
      exclusive_waiting_blocked = true;
      SleepConditionVariableCS (&m_exclusive_cond, &m_state_mutex, INFINITE)
    }
    // This thread now 'owns' the mutex
    exclusive = true;

    LeaveCriticalSection (&m_state_mutex);
  }

  void unlock(void)
  {
    EnterCriticalSection (&m_state_mutex);
    exclusive = false;
    exclusive_waiting_blocked = false;
    LeaveCriticalSection (&m_state_mutex);
    WakeConditionVariable (&m_exclusive_cond);
    WakeAllConditionVariable (&m_shared_cond);
  }

  // Read lock
  void lock_shared(void)
  {
    EnterCriticalSection (&m_state_mutex);
    while (exclusive || exclusive_waiting_blocked)
    {
      SleepConditionVariableCS (&m_shared_cond, m_state_mutex, INFINITE);
    }
    ++shared_count;
    LeaveCriticalSection (&m_state_mutex);
  }

  void unlock_shared(void)
  {
    EnterCriticalSection (&m_state_mutex);
    --shared_count;

    if (shared_count == 0)
    {
      exclusive_waiting_blocked = false;
      LeaveCriticalSection (&m_state_mutex);
      WakeConditionVariable (&m_exclusive_cond);
      WakeAllConditionVariable (&m_shared_cond);
    }
    else
    {
      LeaveCriticalSection (&m_state_mutex);
    }
  }
};

Поведение

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

Во время блокировки записи: блоки чтения и записи блокируются.

В конце блокировки записи — потоки чтения и один поток записи будут соревноваться, чтобы определить, какой из них начнется.

Во время блокировки чтения — модули записи заблокированы. Читатели также блокируются, если и только если блокируется писатель.

После снятия окончательной блокировки чтения — потоки чтения и один поток записи будут соревноваться, чтобы определить, какой из них начнется.

Это может привести к тому, что читатели будут голодать при записи, если контекст процессора часто переключается на m_shared_cond поток перед m_exclusive_cond во время уведомления, но я подозреваю, что эта проблема является теоретической, а не практической, поскольку это алгоритм Boost.

person QuestionC    schedule 23.01.2015
comment
Кажется, это заблокирует писателей, пока я не закончу все читатели, верно? - person sbi; 24.01.2015
comment
да. Но читатели не могут морить писателей голодом, потому что флаг exclusive_waiting_blocked предотвращает запуск других считывателей. - person QuestionC; 24.01.2015
comment
Он выполняет EnterCriticalSection при каждой операции. CRITICAL_SECTION Windows — это внутрипроцессный мьютекс с необязательным начальным ожиданием занятости. Ваше решение, по сути, представляет собой блокировку чтения-записи на основе мьютекса. - person Maxim Egorushkin; 24.01.2015
comment
Отсюда это выглядит так, как будто unlock_shared() при очистке флага exclusive_waiting_blocked освобождает и читателей, и писателей, нет? - person sbi; 24.01.2015
comment
О, подожди. В первую очередь это сигнал для писателей. Я не видел этого изначально. Что ж, теперь это кажется серьезным бонусным кандидатом! (Это означает: критика приветствуется!) - person sbi; 24.01.2015
comment
Похоже, это тот же алгоритм, который описан здесь: open-std.org/jtc1/sc22/wg21/docs/papers/2007/ Это то, из чего была получена реализация boost. И это было взято из старой публикации группы новостей Александра Терехова. Его главная особенность в том, что он не отдает приоритет ни читателям, ни писателям. Когда ни у кого нет мьютекса, а читатель и писатель одновременно попали в мьютекс, ОС решает, какой из них попадет первым. Когда входит один писатель, он ждет, пока все существующие читатели выйдут, и новые читатели не могут войти. Никто не голодает. - person Howard Hinnant; 24.01.2015
comment
Да, это алгоритм повышения. - person QuestionC; 24.01.2015
comment
@Howard: И пока он есть у одного читателя, новые читатели обходят ожидающего писателя, добавляя дополнительную задержку к времени ожидания. По крайней мере, это то, что я прочитал из алгоритма. - person sbi; 25.01.2015
comment
В open-std.org/jtc1 /sc22/wg21/docs/papers/2007/ когда новый читатель и новый писатель одновременно блокируют мьютекс, когда один или несколько читателей уже имеют блокировку, существует вероятность 50/50 того, какой поток попадет в: ОС решает. Если в дело вступает новый читатель, поток записи также входит, но затем ждет, пока все читатели (включая нового) выйдут. Если писатель входит, новый читатель блокируется до тех пор, пока писатель не выйдет. В любом случае новые читатели не смогут войти, пока писатель ждет. Я не подтвердил, что этот ответ является двухпоходным алгоритмом Терехова. - person Howard Hinnant; 25.01.2015
comment
Изучил это. Я не верю, что это двухпоходный алгоритм Терехова. Добавление ответа... - person Howard Hinnant; 25.01.2015
comment
@Howard: Если бы это был ответ мне: то, как я прочитал эту реализацию (пожалуйста, поправьте меня, если я здесь ошибаюсь), читатели всегда будут продолжать, если другой читатель уже имеет блокировку, тем самым минуя любых ожидающих писателей. Это моя проблема с этим решением. (Думаю, я знаю, как это исправить, но мой опыт параллелизма WRT ограничен, и я пришел сюда за одобренным решением, а не за результатом моей собственной переделки. - person sbi; 27.01.2015
comment
@HowardHinnant Недостатка, который вы указали в моем ответе, на самом деле нет. Инвариант exclusive || exclusive_waiting_blocked. В сценарии с двумя писателями ответственный за exclusive_waiting_blocked = false безопасно установит exclusive = true, а при разблокировке проснувшийся писатель сбросит exclusive_waiting_blocked = true, если он проиграл гонку другому пробуждающемуся потоку. Нет нарушения состояния с несколькими писателями, значение exclusive_waiting_blocked просто нюансировано. - person QuestionC; 27.01.2015
comment
@sbi Если писатель блокируется, то exclusive || exclusive_waiting_blocked равно true, что приводит к блокировке чтения. Я добавил описание того, как ведет себя этот алгоритм, надеюсь, что ответит на любые вопросы. - person QuestionC; 28.01.2015
comment
@QuestionC: при разблокировке поток записи устанавливает для exclusive и exclusive_waiting_blocked значение false и отправляет сигналы. Однако нет никакой гарантии, что я увижу, что следующий разбуженный поток не является потоком чтения, который просыпается, видит как exclusive, так и exclusive_waiting_blocked false и принимает на себя владение, даже если другой поток мог ранее установите exclusive_waiting_blocked в true и ждите m_exclusive_cond. - person Howard Hinnant; 28.01.2015
comment
Теперь мьютекс находится в состоянии владения потоком чтения и имеет поток записи, ожидающий m_exclusive_cond, и все же exclusive_wait_blocked == false. На данный момент моя уверенность терпит неудачу. - person Howard Hinnant; 28.01.2015
comment
@HowardHinnant Хватит зацикливаться на exclusive_wait_blocked, контроль основан на exclusive || exclusive_waiting_blocked. Когда m_exclusive_cond уведомлен, есть только два возможных пути управления (выйти из цикла или остаться в нем), и каждый путь устанавливает одну из этих переменных. Я не знаю, как это объяснить, кроме как прибегнуть к прямоугольникам и стрелкам. И действительно, вы более уверены в собственном анализе, чем Boost? - person QuestionC; 28.01.2015
comment
@QuestionC: Я почти уверен, что другие люди, работающие над повышением, очень внимательно слушают каждый раз, когда Ховард критикует их общий мьютекс, и у них есть веские причины для этого. - person sbi; 29.01.2015
comment
Хотя я также считаю, что этот алгоритм работает, и @Howard здесь ошибается, мы с коллегой, которые изучали это, также считаем, что тот факт, что exclusive_wait_blocked является логическим, а не целым числом без знака, является недостатком - если ни по какой другой причине, потому что это затрудняет понимание кода. Вот почему этот ответ не получит бонус. - person sbi; 30.01.2015

Теперь, когда Microsoft открыла исходный код .NET, вы можете посмотреть их ReaderWRiterLockSlim.

Я не уверен, что более простые примитивы, которые они используют, доступны вам, некоторые из них также являются частью библиотеки .NET, и их код также доступен.

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

person zmbq    schedule 30.01.2015
comment
Я очень кратко посмотрел на это. Кажется, что здесь используется дюжина или две переменные-члены, и, следовательно, это совсем не легковесно. Я не стал смотреть дальше. - person sbi; 30.01.2015