Що є більш ефективним, базовим блокуванням мьютексу чи атомним цілим числом?


76

Для чогось простого, як лічильник, якщо кількість потоків буде збільшувати кількість. Я читав, що блокування mutex може знизити ефективність, оскільки потоки повинні чекати. Отже, для мене атомний лічильник був би найефективнішим, але я читав, що зсередини це в основному замок? Тож, мабуть, мене бентежить, як будь-який може бути ефективнішим за інший.


Чи слід відповідати на всі платформи та мови програмування, що підтримують pthreads чи деякі підмножини? Я не повністю розумію взаємозв'язок між pthreads, операційними системами та мовами програмування, але, схоже, ці взаємозв'язки можуть бути актуальними.
snow_abstraction

Відповіді:


53

Атомні операції використовують підтримку процесора (порівнюйте та міняйте інструкції) і взагалі не використовуйте блокування, тоді як блокування більш залежать від ОС і працюють по-різному, наприклад, на Win та Linux.

Блокування фактично призупиняють виконання потоку, звільняючи ресурси процесора для інших завдань, але виникаючи в очевидних накладних перемиканнях контексту при зупинці / перезапуску потоку. Навпаки, потоки, що намагаються проводити атомні операції, не чекають і продовжують намагатися до успіху (так зване зайняте очікування), тому вони не загрожують накладними перемиканнями контексту, але не звільняють ресурси процесора.

Підводячи підсумок, загалом атомні операції швидші, якщо суперечка між потоками є досить низькою. Вам обов’язково слід проводити бенчмаркінг, оскільки немає жодного іншого надійного методу, який би знав, які найменші накладні витрати між перемиканням контексту та зайнятим очікуванням.


46

Якщо у вас є лічильник, для якого підтримуються атомні операції, він буде ефективнішим, ніж мьютекс.

Технічно атомар блокує шину пам'яті на більшості платформ. Однак є дві деталі покращення:

  • Неможливо призупинити потік під час блокування шини пам'яті, але можна призупинити потік під час блокування mutex. Це те, що дозволяє отримати гарантію без блокування (яка нічого не говорить про те, щоб не блокувати - це просто гарантує прогрес хоча б одного потоку).
  • Зрештою, мьютекси реалізуються за допомогою атомів. Оскільки вам потрібна принаймні одна атомна операція, щоб заблокувати мьютекс, і одна атомна операція, щоб розблокувати мьютекс, це робить принаймні вдвічі більше часу, щоб зробити блокування мьютексу, навіть у найкращих випадках.

Важливо розуміти, що це залежить від того, наскільки добре компілятор або інтерпретатор підтримує платформу, щоб генерувати найкращі машинні інструкції (в даному випадку інструкції без блокування) для платформи. Я думаю, що це те, що @Cort Ammon мав на увазі під словом "підтримується". Також деякі мьютекси можуть давати гарантії щодо прогресу вперед або справедливості для деяких або всіх потоків, які не зроблені простими атомними інструкціями.
snow_abstraction

18

Мінімальна (сумісна зі стандартами) введення мутексу вимагає 2 основних інгредієнтів:

  • Спосіб атомарного передавання зміни стану між потоками (`` заблокований '' стан)
  • бар’єри пам’яті для забезпечення операцій пам’яті, захищених мутексом, щоб залишатися всередині захищеної зони.

Ви не можете зробити це простіше, ніж це, через взаємозв'язок "синхронізувати з", який вимагає стандарт С ++.

Мінімальна (правильна) реалізація може виглядати так:

class mutex {
    std::atomic<bool> flag{false};

public:
    void lock()
    {
        while (flag.exchange(true, std::memory_order_relaxed));
        std::atomic_thread_fence(std::memory_order_acquire);
    }

    void unlock()
    {
        std::atomic_thread_fence(std::memory_order_release);
        flag.store(false, std::memory_order_relaxed);
    }
};

Через свою простоту (він не може призупинити потік виконання), ймовірно, що за низьких суперечок ця реалізація перевершує a std::mutex. Але навіть тоді легко помітити, що кожне прирощення цілого числа, захищене цим мьютексом, вимагає наступних операцій:

  • an atomicмагазин , щоб звільнити семафор
  • an atomicпорівняння і заміни (читання-модифікація-запис) , щоб отримати взаємне блокування (можливо , кілька разів)
  • цілочисельний приріст

Якщо порівняти це з автономним, std::atomic<int>який збільшується за допомогою одного (безумовного) читання-модифікації-запису (наприклад, fetch_add), розумно очікувати, що атомна операція (з використанням тієї ж моделі упорядкування) перевершить випадок, коли мутекс використовується.


8

атомарне ціле число - це об'єкт користувацького режиму, оскільки він набагато ефективніший, ніж мьютекс, який працює в режимі ядра . Сфера дії атомарного цілого числа - це одна програма, тоді як сфера дії mutex призначена для всього запущеного програмного забезпечення на машині.


1
Це майже правда. Сучасні реалізації mutex, такі як Futex від Linux, зазвичай використовують атомні операції, щоб уникнути переходу в режим ядра на швидкому шляху. Такі мьютекси повинні перейти в режим ядра, лише якщо атомна операція не виконала бажане завдання (наприклад, у випадку, коли потоку потрібно заблокувати).
Cort Ammon

Я думаю, що область дії атомарного цілого числа - це єдиний процес , який є значним, оскільки додатки можуть складатися з безлічі процесів (наприклад, багатопроцесорна обробка Python для паралелізму).
weberc2


2

Більшість процесорів підтримують атомне читання або запис, і часто атомний cmp & swap. Це означає, що процесор сам пише або читає останнє значення за одну операцію, і може бути втрачено кілька циклів у порівнянні зі звичайним цілочисельним доступом, тим більше, що компілятор не може оптимізувати навколо атомних операцій майже так само добре, як звичайно.

З іншого боку, мьютекс - це ряд рядків коду для входу та виходу, і під час цього виконання інші процесори, які отримують доступ до того самого місця, повністю заблоковані, так що очевидно великі накладні витрати на них. У неоптимізованому високорівневому коді вхід / вихід mutex та атомар будуть викликами функцій, але для mutex будь-який конкуруючий процесор буде заблокований, поки ваша функція введення mutex повернеться, і поки ваша функція виходу буде запущена. Для атомного блокується лише тривалість фактичної операції. Оптимізація повинна зменшити цю вартість, але не всю.

Якщо ви намагаєтесь збільшити, тоді ваш сучасний процесор, ймовірно, підтримує атомне збільшення / зменшення, що буде чудово.

Якщо цього не відбувається, то він або реалізується за допомогою процесора atomic cmp & swap, або за допомогою mutex.

Мютекс:

get the lock
read
increment
write
release the lock

Atomic cmp & swap:

atomic read the value
calc the increment
do{
   atomic cmpswap value, increment
   recalc the increment
}while the cmp&swap did not see the expected value

Отже, ця друга версія має цикл [якщо інший процесор збільшує значення між нашими атомними операціями, тому значення більше не збігається, і приріст буде неправильним], який може затягнутися [якщо є багато конкурентів], але, як правило, все одно повинен бути швидшим, ніж версія mutex, але версія mutex може дозволити цьому процесору перемикатися на завдання.


1

Mutex є семантикою рівня ядра, яка забезпечує взаємне виключення навіть на Process level . Зауважте, що це може бути корисним для поширення взаємного виключення за межі процесу, а не лише в межах процесу (для потоків). Це дорожче.

Атомний лічильник, AtomicIntegerнаприклад, базується на CAS, і, як правило, намагаються виконати операцію до успіху. В основному, в цьому випадку потоки гоняються або змагаються за атомне збільшення / зменшення значення. Тут ви можете побачити хороші цикли процесора, які використовуються потоком, який намагається оперувати поточним значенням.

Оскільки ви хочете підтримувати лічильник, AtomicInteger \ AtomicLong буде найкращим для вашого випадку використання.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.