синхронизации - потоки c++



Использование очереди Boost.Lockfree выполняется медленнее, чем использование мьютексов (1)

До сих пор я использовал std::queue в моем проекте. Я измерил среднее время, которое требуется конкретной операции в этой очереди.

Время измерялось на двух машинах: моя локальная Ubuntu VM и удаленный сервер. Используя std::queue , среднее значение было почти одинаковым на обеих машинах: ~ 750 микросекунд.

Затем я «модернизировал» std::queue для boost::lockfree::spsc_queue , поэтому я мог избавиться от мьютексов, защищающих очередь. На моей локальной виртуальной машине я видел огромный прирост производительности, а средний показатель составляет 200 микросекунд. Однако на удаленной машине среднее значение достигало 800 микросекунд, что было медленнее, чем раньше.

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

На странице Boost.Lockfree:

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

Чтобы узнать, поддерживаются ли эти инструкции, boost::lockfree::queue имеет метод bool is_lock_free(void) const; , Однако boost::lockfree::spsc_queue не имеет такой функции, которая для меня подразумевает, что она не полагается на аппаратное обеспечение, а на любой машине всегда без блокировки.

Что может быть причиной потери производительности?

Скопировать код (Производитель / Потребитель)

// c++11 compiler and boost library required

#include <iostream>
#include <cstdlib>
#include <chrono>
#include <async>
#include <thread>
/* Using blocking queue:
 * #include <mutex>
 * #include <queue>
 */
#include <boost/lockfree/spsc_queue.hpp>


boost::lockfree::spsc_queue<int, boost::lockfree::capacity<1024>> queue;

/* Using blocking queue:
 * std::queue<int> queue;
 * std::mutex mutex;
 */

int main()
{
    auto producer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Producing data in a random interval
        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            // Push random int (0-9999)
            queue.push(std::rand() % 10000);

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for random duration (0-999 microseconds)
            std::this_thread::sleep_for(std::chrono::microseconds(rand() % 1000));
        }
    }

    auto consumer = std::async(std::launch::async, [queue /*,mutex*/]() 
    {
        // Example operation on the queue.
        // Checks if 1234 was generated by the producer, returns if found.

        while(true)
        {
            /* Using the blocking queue, the mutex must be locked here.
             * mutex.lock();
             */

            int value;
            while(queue.pop(value)
            {
                if(value == 1234)
                    return;
            }

            /* Using the blocking queue, the mutex must be unlocked here.
             * mutex.unlock();
             */

            // Sleep for 100 microseconds
            std::this_thread::sleep_for(std::chrono::microseconds(100));
        }
    }

    consumer.get();
    std::cout << "1234 was generated!" << std::endl;
    return 0;
}

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

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

Позвольте мне дать вам очень экстремальную гипотезу. Представьте, что четыре потока работают на типичном, современном двухъядерном процессоре. Нити A1 и A2 управляют коллекцией A. Нити B1 и B2 управляют коллекцией B.

Во-первых, давайте представим, что в коллекции используются блокировки. Это будет означать, что если потоки A1 и A2 (или B1 и B2) будут работать одновременно, один из них будет заблокирован блокировкой. Итак, очень быстро, один поток A и один поток B будут работать. Эти потоки будут работать очень быстро и не будут бороться. Каждый раз, когда нить пытается бороться, конфликтный поток будет отменен. Ура.

Теперь представьте, что в коллекции нет блокировок. Теперь потоки A1 и A2 могут запускаться одновременно. Это вызовет постоянную конкуренцию. Кэш-линии для коллекции будут пинг-понг между двумя ядрами. Межъядерные шины могут насыщаться. Производительность будет ужасной.

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

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

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





lock-free