Хобрук: Ваш путь к мастерству в программировании

std::mutex::try_lock ложно терпит неудачу?

Возможно, я неправильно понимаю std::mutex::try_lock:

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

Это означает, что если ни один поток не заблокировал этот mutex, когда я попробую try_lock, он может вернуть false? С какой целью?

Разве функция try_lock не возвращает false, если она заблокирована ИЛИ true, если ее никто не заблокировал? Не совсем уверен, что мой неродной английский меня обманывает...

17.01.2018

  • Это по той же причине, по которой std::condition_variable::wait может ложно просыпаться, даже если условие не было установлено — это позволяет ОС больше оптимизировать некоторые распространенные случаи. 17.01.2018
  • @YSC У меня действительно нет времени, чтобы написать полноценный ответ :( но вопрос condition_variable - это вопрос, который, как я знаю, где-то есть на SO ... 17.01.2018

Ответы:


1

Это означает, что если ни один поток не заблокировал этот мьютекс, когда я попробую try_lock, он может вернуть false?

Да, это именно то, что он говорит.

Разве функция try_lock не возвращает false, если она заблокирована, ИЛИ true, если ее никто не блокирует?

Нет, функция try_lock состоит в том, чтобы попытаться заблокировать мьютекс.

Тем не менее, есть более чем один способ, которым он может выйти из строя:

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

Обычный случай на POSIX-подобных платформах, унаследованный от потоков POSIX, заключается в том, что сигнал доставляется (и обрабатывается обработчиком сигнала) в текущем потоке, прерывая попытку блокировки.

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

17.01.2018
  • Это означает, что часть моего кода случайным образом будет/не будет выполняться, даже если никакие другие потоки не используют блокировку: O 17.01.2018
  • Нет, это означает, что ваш код статически неверен и плохо написан, если он не предназначен для этой вполне нормальной ситуации. 17.01.2018
  • @markzzz вот почему вы проверяете результат попытки, а не предполагаете, что он у вас есть. 17.01.2018
  • Да, если вы просто хотите заблокировать, пока не получите блокировку, сделайте это в цикле. Очевидно, в этом случае вы могли бы просто вызвать обычный вариант без попытки - может быть, было бы лучше, если бы try_lock различал разные случаи сбоя... 17.01.2018
  • Мне нужно решение, которое ДЕЛАЕТ что-то, если ни у кого нет блокировки. В противном случае GO AHEAD (т. е. не блокировать поток, просто игнорируя оператор if). try-lock кажется неправильным. Что ты посоветуешь? 17.01.2018
  • зависит от того, кто еще борется за замок и что он защищает, но похоже, что он заслуживает отдельного вопроса 17.01.2018
  • @markzzz: Возможно, стоит отступить и вернуться к основной проблеме, которую вы пытаетесь решить, потому что это кажется слабой стратегией для ее решения. Меньше сосредотачивайтесь на одном подходе, который вы считаете правильным, и больше на том, какими могут быть другие подходы. Заставить поведение программы зависеть от таймингов и взаимодействия потоков, как это, звучит как гигантская утечка абстракции. Что должны делать потоки? 12.02.2018

  • 2

    Исходя из ваших комментариев, я бы написал (цитируя ваши слова):

    std::unique_lock<std::mutex> lock(m, std::defer_lock); // m being a mutex
    ...
    if (lock.try_lock()) {
      ... // "DO something if nobody has a lock"
    } else {
      ... // "GO AHEAD"
    }
    

    Обратите внимание, что lock.try_lock() фактически вызывает m.try_lock(), поэтому он также подвержен ложным ошибкам. Но меня бы этот вопрос особо не волновал. ИМО, на практике ложные сбои/пробуждения довольно редки (как указал Бесполезный, в Linux они могут происходить при доставке сигнала).

    Подробнее о ложных проблемах см., например: https://en.wikipedia.org/wiki/Spurious_wakeup или Почему pthread_cond_wait имеет ложные пробуждения?.

    ОБНОВЛЕНИЕ

    Если вы действительно хотите устранить ложный сбой try_lock, вы можете использовать какой-нибудь атомарный флаг, например:

    // shared by threads:
    std::mutex m;  
    std::atomic<bool> flag{false};
    
    // within threads:
    std::unique_lock<std::mutex> lock(m, std::defer_lock); // m being a mutex
    ...
    while (true) {
      lock.try_lock();
      if (lock.owns_lock()) {
        flag = true;
        ... // "DO something if nobody has a lock"    
        flag = false;
        break;
      } else if (flag == true) {
        ... // "GO AHEAD"
        break;
      }
    }
    

    Возможно, это будет переписано в лучшую форму, я не проверял. Также обратите внимание, что flag не сбрасывается автоматически через RAII, здесь может быть полезна некоторая защита области.

    ОБНОВЛЕНИЕ 2

    Если вам также не нужны блокирующие функции mutex, используйте std::atomic_flag:

    std::atomic_flag lock = ATOMIC_FLAG_INIT;
    
    // within threads:
    if (lock.test_and_set()) {
        ... // "DO something if nobody has a lock"    
        lock.clear();
    } else {
        ... // "GO AHEAD"
    }
    

    Просто, опять же, было бы лучше сбросить флаг через какой-нибудь механизм RAII.

    17.01.2018
  • Я не вижу никакого вашего кода, но вы спросили: Мне нужно решение, которое ДЕЛАЕТ что-то, если ни у кого нет блокировки. В противном случае GO AHEAD (т. е. не блокировать поток, просто игнорируя оператор if). try-lock кажется неправильным. Что вы предлагаете?. Вот что я предлагаю. Кроме того, он использует std::unique_lock, что лучше, чем использование мьютекса напрямую, поскольку он поддерживает гарантированную разблокировку с помощью технологии RAII. Но главный посыл в том, что в данном случае меня бы вообще не волновал ложный сбой. 17.01.2018
  • @markzzz Я обновил свой ответ, чтобы справиться с ложными сбоями. 17.01.2018

  • 3

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

    Блокировка мьютекса обычно требует некоторой формы операции обмена атомарным сравнением. C++11 и C11 вводят atomic_compare_exchange_strong и atomic_compare_exchange_weak. Последний допускает ложный отказ.

    Допуская ложный сбой try_lock, реализациям разрешается использовать atomic_compare_exchange_weak для максимизации производительности и минимизации размера кода.

    Например, в ARM64 атомарные операции обычно реализуются с использованием инструкций монопольной загрузки (LDXR) и монопольного сохранения (STRX). LDXR запускает аппаратное обеспечение «монитора», которое начинает отслеживать все обращения к области памяти. STRX выполняет сохранение только в том случае, если между LDXR и STRX инструкциями не было обращений к этому региону. Таким образом, вся последовательность может ложно завершиться ошибкой, если другой поток получит доступ к этой области памяти или если между ними было IRQ.

    На практике генерация кода для try_lock, реализованного с использованием слабой гарантии, не сильно отличается от кода, реализованного с использованием сильной гарантии.

    bool mutex_trylock_weak(atomic_int *mtx)
    {
        int old = 0;
        return atomic_compare_exchange_weak(mtx, &old, 1);
    }
    
    bool mutex_trylock_strong(atomic_int *mtx)
    {
        int old = 0;
        return atomic_compare_exchange_strong(mtx, &old, 1);
    }
    

    Взгляните на сгенерированную сборку для ARM64:

    mutex_trylock_weak:
      sub sp, sp, #16
      mov w1, 0
      str wzr, [sp, 12]
      ldaxr w3, [x0]      ; exclusive load (acquire)
      cmp w3, w1
      bne .L3
      mov w2, 1
      stlxr w4, w2, [x0]  ; exclusive store (release)
      cmp w4, 0           ; the only difference is here
    .L3:
      cset w0, eq
      add sp, sp, 16
      ret
    
    mutex_trylock_strong:
      sub sp, sp, #16
      mov w1, 0
      mov w2, 1
      str wzr, [sp, 12]
    .L8:
      ldaxr w3, [x0]      ; exclusive load (acquire)
      cmp w3, w1
      bne .L9
      stlxr w4, w2, [x0]  ; exclusive store (release)
      cbnz w4, .L8        ; the only difference is here
    .L9:
      cset w0, eq
      add sp, sp, 16
      ret
    

    Единственное отличие состоит в том, что "слабая" версия исключает условную обратную ветвь cbnz w4, .L8 и заменяет ее на cmp w4, 0. Ветки с обратным условием прогнозируются ЦП как «будущие выполнены» в отсутствие информации о прогнозировании ветвлений, поскольку предполагается, что они являются частью цикла - такое предположение в этом случае неверно, так как большая часть временной блокировки будет получена ( низкая конкуренция считается наиболее распространенным случаем).

    Imo это единственная разница в производительности между этими функциями. «Сильная» версия может в основном страдать от 100% коэффициента неправильного предсказания переходов для одной инструкции при некоторых рабочих нагрузках.

    Кстати, ARMv8.1 вводит атомарные инструкции, так что между ними нет никакой разницы, как и на x86_64. Код, сгенерированный с флагом -march=armv8.1-a:

      sub sp, sp, #16
      mov w1, 0
      mov w2, 1
      mov w3, w1
      str wzr, [sp, 12]
      casal w3, w2, [x0]
      cmp w3, w1
      cset w0, eq
      add sp, sp, 16
      ret
    

    Некоторые функции try_lock могут дать сбой даже при использовании atomic_compare_exchange_strong, например, try_lock_shared из shared_mutex может потребоваться увеличить счетчик считывателя и может произойти сбой, если другой считыватель ввел блокировку. «Сильный» вариант такой функции должен генерировать цикл и, следовательно, может страдать от аналогичного неправильного определения ветвления.

    Еще одна небольшая деталь: если мьютекс написан на C, некоторые компиляторы (например, Clang) могут выравнивать цикл по 16-байтовой границе, чтобы улучшить его производительность, раздувая тело функции отступами. В этом нет необходимости, если цикл почти всегда выполняется один раз.


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

    11.02.2018
  • такая операция является неблокирующей, поэтому сигналы не могут ее прервать Эмм, если только операция не является полностью атомарной, тогда, конечно, они могут. Каждая операция блокируется, это просто мера степени 12.02.2018
  • @LightnessRacesinOrbit Если все блокируется, то ничего не блокируется. Любая разумная реализация try_lock в ядре должна будет ждать только несколько спин-блокировок. Эти ожидания не прерываются сигналом. Даже posix не перечисляет EINTR для функций блокировки/временной блокировки. 12.02.2018
  • Я касался только вашего утверждения в цитируемом отрывке, а не try_lock конкретно в системах POSIX. 12.02.2018
  • @LightnessRacesinOrbit Конечно, вы можете реализовать try_lock через некоторый механизм передачи сообщений, построенный поверх сокета, который очень прерываем (и именно это на самом деле делает Wine), но я бы посчитал такую ​​реализацию неразумной. Нет причин усложнять программный интерфейс. Интерфейс должен быть максимально минимален, непродуманная реализация должна скрывать лишние детали. 12.02.2018
  • К сожалению, то, что должно быть, и то, что должно быть, не всегда совпадают. 12.02.2018
  • @LightnessRacesinOrbit Это всего лишь словесная мастурбация. 12.02.2018
  • Нет, совсем нет. Это совершенно разные понятия. 12.02.2018

  • 4

    В разделе Основы модели параллельной обработки памяти C++ 3 уже есть четкое объяснение того, почему стандарт допускает ложные отказы try_lock. Короче говоря, указано, чтобы семантика try_lock соответствовала определению расы в модели памяти C++.

    14.02.2019

    5

    Если вызов try_lock() возвращает true, вызов успешно блокирует блокировку. Если он возвращает false, если это не так. Это все. Да, функция может вернуть false, если ни у кого больше нет блокировки. False означает только, что попытка блокировки не удалась; он не говорит вам, почему это не удалось.

    17.01.2018
  • Но зачем the attempt to lock shouldn't succeed, если его никто не запирает? Звучит как ошибка при блокировке :D 17.01.2018
  • Есть серьезные причины, почему он разработан таким образом. 17.01.2018
  • @markzzz В моем ответе есть ссылки, которые объясняют их. Однако я также придумал другое и гораздо более простое решение, которое работает в случае, если вам не нужна блокирующая функциональность mutex (что, похоже, в вашем случае). Смотрите 2-е обновление моего ответа. 18.01.2018
  • Мне действительно кажется, что это большая абстракция, но так оно и есть 12.02.2018
  • Новые материалы

    Понимание СТРУКТУРЫ ДАННЫХ И АЛГОРИТМА.
    Что такое структуры данных и алгоритмы? Термин «структура данных» используется для описания того, как данные хранятся, а алгоритм используется для описания того, как данные сжимаются. И данные, и..

    Как интегрировать модель машинного обучения на ios с помощью CoreMl
    С выпуском новых функций, таких как CoreML, которые упростили преобразование модели машинного обучения в модель coreML. Доступная модель машинного обучения, которую можно преобразовать в модель..

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

    Технологии и проблемы будущей работы
    Изучение преимуществ и недостатков технологий в образовании В быстро меняющемся мире технологии являются решающим фактором в формировании будущего работы. Многие отрасли уже были..

    Игорь Минар из Google приедет на #ReactiveConf2017
    Мы рады сообщить еще одну замечательную новость: один из самых востребованных спикеров приезжает в Братиславу на ReactiveConf 2017 ! Возможно, нет двух других кланов разработчиков с более..

    Я собираюсь научить вас Python шаг за шагом
    Привет, уважаемый энтузиаст Python! 👋 Готовы погрузиться в мир Python? Сегодня я приготовил для вас кое-что интересное, что сделает ваше путешествие более приятным, чем шарик мороженого в..

    Альтернатива шаблону исходящих сообщений для архитектуры микросервисов
    Познакомьтесь с двухэтапным сообщением В этой статье предлагается альтернативный шаблон для папки Исходящие : двухэтапное сообщение. Он основан не на очереди сообщений, а на..