Массивная загрузка ЦП с использованием std::lock (С++ 11)

Мои недавние попытки реализовать диспетчер потоков/мьютексов закончились 75%-й загрузкой ЦП (4 ядра), в то время как все четыре запущенных потока либо находились в спящем режиме, либо ожидали разблокировки мьютекса.

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

std::unique_lock<std::mutex> lock1( mutex1, std::defer_lock );
std::unique_lock<std::mutex> lock2( mutex2, std::defer_lock );
std::lock( lock1, lock2 );

Другая часть класса использует std::condition_variable с wait() и notify_one() на mutex1 для выборочного выполнения некоторого кода в одно и то же время.

Простое изменение на

std::unique_lock<std::mutex> lock1( mutex1 );
std::unique_lock<std::mutex> lock2( mutex2 );

снизил загрузку процессора до 1-2%.

Не могу поверить, что функция std::lock() настолько неэффективна. Может ли это быть ошибкой в ​​g++ 4.6.3?

редактировать: ( пример )

#include <atomic>
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <thread>

std::mutex mutex1, mutex2;
std::condition_variable cond_var;

bool cond = false;
std::atomic<bool>done{false};

using namespace std::chrono_literals;

void Take_Locks()
    {
    while( !done )
        {
        std::this_thread::sleep_for( 1s );

        std::unique_lock<std::mutex> lock1( mutex1, std::defer_lock );
        std::unique_lock<std::mutex> lock2( mutex2, std::defer_lock );
        std::lock( lock1, lock2 );

        std::this_thread::sleep_for( 1s );
        lock1.unlock();
        lock2.unlock();
        }
    }

void Conditional_Code()
    {
    std::unique_lock<std::mutex> lock1( mutex1, std::defer_lock );
    std::unique_lock<std::mutex> lock2( mutex2, std::defer_lock );

    std::lock( lock1, lock2 );
    std::cout << "t4: waiting \n";

    while( !cond )
        cond_var.wait( lock1 );

    std::cout << "t4: condition met \n";
    }

int main()
    {
    std::thread t1( Take_Locks ), t2( Take_Locks ), t3( Take_Locks );
    std::thread t4( Conditional_Code );

    std::cout << "threads started \n";
    std::this_thread::sleep_for( 10s );

    std::unique_lock<std::mutex> lock1( mutex1 );
    std::cout << "mutex1 locked \n" ;
    std::this_thread::sleep_for( 5s );

    std::cout << "setting condition/notify \n";
    cond = true;
    cond_var.notify_one();
    std::this_thread::sleep_for( 5s );

    lock1.unlock();
    std::cout << "mutex1 unlocked \n";
    std::this_thread::sleep_for( 6s );

    done = true;
    t4.join(); t3.join(); t2.join(); t1.join();
    }

person mac    schedule 02.12.2012    source источник
comment
Вы должны опубликовать код, содержащий вызовы wait() и notify_one().   -  person Ben    schedule 02.12.2012
comment
Сложные схемы блокировки для управления несколькими ресурсами, которые «полагаются» на постоянный опрос блокировок, безнадежны, так как тратят впустую ресурсы ЦП и/или пропускную способность памяти и/или приводят к задержкам, которых можно избежать. Если вы опрашиваете блокировки, вы делаете это неправильно.   -  person Martin James    schedule 10.05.2014


Ответы (4)


На моей машине следующий код выводится 10 раз в секунду и потребляет почти 0 ресурсов процессора, потому что большую часть времени поток либо спит, либо блокируется заблокированным мьютексом:

#include <chrono>
#include <thread>
#include <mutex>
#include <iostream>

using namespace std::chrono_literals;

std::mutex m1;
std::mutex m2;

void
f1()
{
    while (true)
    {
        std::unique_lock<std::mutex> l1(m1, std::defer_lock);
        std::unique_lock<std::mutex> l2(m2, std::defer_lock);
        std::lock(l1, l2);
        std::cout << "f1 has the two locks\n";
        std::this_thread::sleep_for(100ms);
    }
}

void
f2()
{
    while (true)
    {
        std::unique_lock<std::mutex> l2(m2, std::defer_lock);
        std::unique_lock<std::mutex> l1(m1, std::defer_lock);
        std::lock(l2, l1);
        std::cout << "f2 has the two locks\n";
        std::this_thread::sleep_for(100ms);
    }
}

int main()
{
    std::thread t1(f1);
    std::thread t2(f2);
    t1.join();
    t2.join();
}

Пример вывода:

f1 has the two locks
f2 has the two locks
f1 has the two locks
...

Я запускаю это в OS X, и приложение Activity Monitor говорит, что этот процесс использует процессор на 0,1%. Машина Intel Core i5 (4 ядра).

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

Обновить

Если эта программа использует чрезмерную загрузку ЦП на вашей платформе, попробуйте изменить ее на вызов ::lock() вместо этого, где это определяется с помощью:

template <class L0, class L1>
void
lock(L0& l0, L1& l1)
{
    while (true)
    {
        {
            std::unique_lock<L0> u0(l0);
            if (l1.try_lock())
            {
                u0.release();
                break;
            }
        }
        std::this_thread::yield();
        {
            std::unique_lock<L1> u1(l1);
            if (l0.try_lock())
            {
                u1.release();
                break;
            }
        }
        std::this_thread::yield();
    }
}

Мне было бы интересно узнать, если бы это имело какое-либо значение для вас, спасибо.

Обновление 2

После долгой задержки я написал первый набросок статьи на эту тему. В документе сравниваются 4 различных способа выполнения этой работы. Он содержит программное обеспечение, которое вы можете скопировать и вставить в свой собственный код и протестировать самостоятельно (и, пожалуйста, сообщите, что вы найдете!):

http://howardhinnant.github.io/dining_philosophers.html

person Howard Hinnant    schedule 25.01.2013
comment
На моей машине этот пример в основном печатает серию f1/f2 ... имеет два сообщения блокировки в произвольном порядке. (гкк 4.6.3) - person mac; 03.02.2013
comment
@mac поведение может отличаться в зависимости от операционной системы или реализации C++. Я вижу поведение, которое вы описываете с libstdc++ в Linux, но я вижу чередование f1/f2 с libc++ в OS X. - person bames53; 05.02.2013
comment
В OS X я вижу в основном чередование f1/f2, но не строго. Время от времени он ломает этот шаблон. Чаще, если сократить период сна. В любом случае, точная последовательность захвата блокировки f1/f2 здесь не имеет особого значения. Суть в том, чтобы оспорить утверждение Дэвида Шварца о том, что std::lock не может быть эффективно реализовано. т.е. Сильно оспариваю: функция не может ждать без вращения. И предложите этот эксперимент как доказательство обратного, и пригласите всех, кто опубликует код, который вращается. - person Howard Hinnant; 06.02.2013
comment
@ bames53 интересно, OS X, похоже, имеет более «продвинутые» реализации. - person mac; 16.02.2013
comment
Я заметил, что когда я запускаю эту программу под VS2012, она использует много ресурсов ЦП; 25% на 4-х ядерной машине. Если я изменю его, не используя отложенные блокировки (и приобрету блокировки в согласованном порядке), то загрузка ЦП упадет практически до нуля. Я попытался прочитать реализацию std::lock, чтобы узнать, почему, но это было слишком уродливо. @мак - person bames53; 16.02.2013
comment
Я определенно получаю код для вращения на Linux, блокируя мьютекс в main(). Но сохранив всего два потока, я сделал следующие изменения: 1) добавил короткий сон перед lock() в t1() и t2() для чередующегося порядка (см. мой первый комментарий). 2) заблокировать только один мьютекс в одном потоке и оба в другом. 3) Из-за низкой частоты обновления моего монитора ЦП я увеличил продолжительность сна после блокировки одного мьютекса до 500 мс. Теперь код дает пики до 30% загрузки процессора. - person mac; 16.02.2013
comment
@ bames53 Я использую std::thread, чтобы сохранить максимально возможную переносимость. После вашего опыта работы с VS2012 я еще больше доволен решением переписать свой код и избежать отложенных блокировок. - person mac; 16.02.2013
comment
Использование обновленной функции ::lock() снизило загрузку процессора до приемлемых значений. - person mac; 17.02.2013
comment
@mac: Спасибо за информацию, mac! - person Howard Hinnant; 17.02.2013
comment
Я знаю, в основном это реализация по ссылке, которую вы разместили в своем самом первом комментарии. Но отсутствие понимания помешало мне проверить это. - person mac; 17.02.2013
comment
Считайте, что этот код передан в общественное достояние. Отчеты об ошибках производительности могут быть нужны для реализаций, отличных от libc++. - person Howard Hinnant; 17.02.2013
comment
Как написано, этот код освобождает одну из блокировок перед возвратом. Разве все они не должны быть задержаны, когда звонок вернется? - person Nemo; 30.08.2013
comment
@Nemo На самом деле std::unique_lock::release() разрывает ассоциацию с мьютексом, не разблокируя его. (В противном случае, unique_lock вышел бы за пределы области действия с все еще связанным мьютексом, а его dtor разблокировал бы мьютекс.) - person Nicu Stiurca; 30.08.2013
comment
@HowardHinnant: Хорошо, понял. Все еще не уверен, насколько хорошо это обобщает, но для двух мьютексов это хорошая реализация. (Плохой спецификации.) - person Nemo; 30.08.2013
comment
@Nemo: Если вы хотите увидеть, как это эффективно реализовано для N мьютексов (N › 2), см. открытый исходный код libc++ по адресу (libcxx.llvm.org). Спецификация эффективно реализуема без героических усилий. Он был протестирован на производительность до N == 8 (с хорошими результатами). Если вам нужно одновременно заблокировать больше мьютексов, мне было бы интересно узнать о вашем варианте использования. - person Howard Hinnant; 17.12.2013
comment
Небольшой вопрос: почему вы снимаете первую блокировку после получения второй. Разве обедающему философу не нужны обе вилки, чтобы есть? - person StackedCrooked; 11.05.2014
comment
@StackedCrooked: функция unique_lock::release() освобождает право собственности на блокировку. Это не разблокирует мьютекс. Таким образом, локальный u0 отвечает за блокировку l0, а если l1.try_lock() выдает или возвращает false, u0 отвечает за разблокировку l0. Если l1.try_lock() завершается успешно, теперь и l0, и l1 заблокированы. Миссия выполнена. Осталось только сказать u0, что он больше не несет ответственности за l0. Вызывающий клиент теперь отвечает как за l0, так и за l1. - person Howard Hinnant; 11.05.2014
comment
@HowardHinnant О, верно. Глупая ошибка с моей стороны. Спасибо. - person StackedCrooked; 12.05.2014
comment
@HowardHinnant На моей 64-битной Ubuntu 14.04 с gcc 4.9.2 вначале у меня много f2 (только они), а затем, через несколько минут, я получаю только f1. Никакого чередования их нет. - person Patryk; 23.11.2014
comment
@Patryk: Это использование реализации lock, показанной в моем ответе, или gcc std::lock? Если второе, попробуйте первое. - person Howard Hinnant; 23.11.2014
comment
@HowardHinnant Используя версию lock из этого ответа, результат в основном такой же (какой-то другой случайный порядок, но в основном много 2 и много 1 с очень редкими изменениями) - person Patryk; 24.11.2014
comment
@Patryk: Поведение такое же, если у вас есть f1 и f2 блокировка только m1? - person Howard Hinnant; 24.11.2014
comment
@HowardHinnant Можете ли вы опубликовать код где-нибудь? Я не могу представить, как бы вы хотели, чтобы это выглядело. - person Patryk; 24.11.2014
comment
@Patryk: codepad.org/igAI2mSb Идея состоит в том, чтобы увидеть, связано ли поведение, которое вы видите, с алгоритмом это попытка заблокировать два мьютекса или исходит от планировщика потоков, который не находится под контролем этого эксперимента. - person Howard Hinnant; 24.11.2014
comment
@HowardHinnant Поведение с этим кодом такое же, как я описал выше. - person Patryk; 25.11.2014
comment
@Patryk: Спасибо за этот тест. Мне кажется, это указывает на то, что никакой алгоритм блокировки с двумя замками ничего не может сделать в вашей системе, чтобы изменить поведение, которое вы видите. - person Howard Hinnant; 25.11.2014

Как сказано в документации, [t]объекты блокируются неопределенной последовательностью вызовов lock, try_lock, unlock. Просто не может быть эффективного способа, если мьютексы удерживаются другими потоками в течение значительного периода времени. Функция не может ждать без вращения.

person David Schwartz    schedule 02.12.2012
comment
@David Schwartz: вращение может быть очень медленным и не нагружающим ЦП, если после сбоя происходит как выход, так и попытка заблокировать / заблокировать мьютекс, который ранее не выполнил try_lock. Вот эффективная реализация с открытым исходным кодом: llvm.org/svn/llvm- проект/libcxx/trunk/include/mutex . Эта реализация была протестирована в сценариях с высокой конкуренцией и признана очень эффективной. Предпосылка вашего ответа просто неверна. ОС приостановит поток A, если поток B удерживает какие-либо блокировки, необходимые потоку A, точно так же, как если бы мы говорили только об одном мьютексе. - person Howard Hinnant; 25.01.2013
comment
@HowardHinnant Ни одна известная мне ОС не будет парковать готовый к запуску поток, если у него есть доступное ядро ​​для его запуска. (Я не думаю, что вы понимаете ограничения здесь. Неопределенная серия вызовов lock, try_lock, unlock не может быть эффективной, и это то, что здесь конкретно требуется.) - person David Schwartz; 25.01.2013
comment
Все известные мне современные ОС приостанавливают поток, если он не готов к запуску, потому что он заблокирован на заблокированном мьютексе. Вот что делает достойная реализация этого алгоритма. Я дал вам ссылку на исходный код для просмотра. - person Howard Hinnant; 25.01.2013
comment
Я посмотрел на этот исходный код. Он просто повторяет lock, trylock, release, yield. Если ядер больше, чем готовых к запуску потоков, yield не работает. Эта реализация ужасна, и это не ошибка реализации, спецификация не дает ей выбора. Два потока, работающие на шаге блокировки, один из которых выполняет lock(a,b), а другой lock(b,a), могут вращаться вечно. - person David Schwartz; 25.01.2013
comment
Попробуйте настроить. Посмотрите, сможете ли вы создать live-lock. - person Howard Hinnant; 25.01.2013
comment
@HowardHinnant: я не могу заставить его работать с самим собой без обмана, но я могу написать разумный код, который, если он работает вместе с этой функцией на тех же двух блокировках, вызовет смехотворное потребление ЦП, по крайней мере вдвое больше, чем если бы вы просто делаете lock(a); lock(b);. (Чего вы, конечно, не можете. Опять же, именно спецификация препятствует эффективной реализации.) - person David Schwartz; 25.01.2013
comment
Я не знаю, как вести эту дискуссию без формата, позволяющего показывать друг другу код (который не помещается в комментариях). Предложения? - person Howard Hinnant; 25.01.2013
comment
@DavidSchwartz Мне было бы интересно увидеть этот код и объяснение. - person bames53; 28.01.2013
comment
@bames53: я решил превратить это в отдельные вопросы и ответы: stackoverflow.com/questions/18520983. Все ответы до сих пор не согласны со мной, но я согласен с Дэвидом: нет разумного способа реализовать это, как указано. (Любая форма занятого цикла неразумна в моем мире...) - person Nemo; 30.08.2013
comment
@Немо - правильно. Опрашивать замки безнадежно. Потери ЦП, потери пропускной способности памяти и задержки. - person Martin James; 10.05.2014

std::lock() Функция, не являющаяся членом, может вызвать проблемы с динамической блокировкой или снижение производительности, она гарантирует только "Никогда не блокируется".

Если вы можете определить «порядок блокировки (иерархия блокировки)» нескольких мьютексов по дизайну, предпочтительно не использовать общий std::lock(), а блокировать каждый мьютекс в заранее определенном порядке.

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

person yohjp    schedule 25.01.2013
comment
По сути, это то, с чем я столкнулся, пока не появился эффективный и детерминированный (условный) метод блокировки. - person mac; 03.02.2013

Во-первых, я хочу поблагодарить за все ответы.

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

Условная часть блокирует оба мьютекса, в то время как для функции std::condition_variable::wait() используется только один.

Но мне все равно интересно, что происходит за кулисами, что вызывает такую ​​высокую нагрузку на процессор.

person mac    schedule 03.12.2012
comment
Смотрите мой ответ. Поток постоянно блокирует один мьютекс, не может заблокировать другой, освобождает первый мьютекс, снова не может заблокировать второй и повторяется. - person David Schwartz; 04.12.2012