LINUX.ORG.RU

Instruction reordering

 , ,


2

4
void some_internal_function()
{
  std::unique_lock<std::mutex> l(mutex);
  if (task_status == deferred)
  {
    task_status=in_progress;
    l.unlock();
    execute_task();
    l.lock();
    task_status=ready;
    l.unlock();
    cv.notify_all();
  }
}

void execute_task()
{
  //perform blocking I/O or other user-provided task
}

Гарантируется ли вызов execute_task() _не_ под блокировкой? Ведь ничто не мешает компилятору перенести этот вызов на строку выше или ниже. Реальный компилятор врядли так сделает, а вот стандарт такие преобразования не запрещает(???)

Deleted

Не запрещает, но между присвоением task_status in_progress и ready точно будет разблокировка.

vzzo ★★★
()
Ответ на: комментарий от vzzo

точно будет разблокировка

Можно подробнее, или ссылку, если не затруднит. Разве он не дропнет что-то вида x.unlock(); x.lock(); как не имеющее эффекта?

Deleted
()
Ответ на: комментарий от Deleted

Хм, оказывается, вопрос достаточно спорный. Я отвечал исходя из моего опыта с clang: он оптимизирует две записи, но не оптимизирует блокировку/разблокировку.

С натяжкой можно приплести следующее:

1.10.25:
An implementation should ensure that the last value (in modification order) assigned by an atomic or synchronization operation will become visible to all other threads in a finite period of time.

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

В случае mutex, видимо, неизвестной функцией является pthread_mutex_lock/pthread_mutex_unlock.

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

vzzo ★★★
()
Ответ на: комментарий от vzzo

не переносить атомарные операции между функциями, про которые мы ничего не знаем

Да, в этом случае вопросов нет, когда компилятор не знает содержимого execute_task(), он обломится что-то менять. Сомнения вызывает именно случай, когда компилятор «насквозь» видит все вызовы из execute_task(). Как крайний случай - отсутствие side-эффектов и долгое время выполнения. Корректность программы не страдает, а вот производительность упадёт. worker-у не нужен результат, он только deferred/in_progress смотрит. А при reordering-е он на локе вхолостую стоять будет. Вручную барьеров поставить - слишком костыльно получается)

Deleted
()
Ответ на: комментарий от Deleted

Тогда, видимо, гарантий нет, но можно попытаться рассчитывать на «здравый смысл» разработчиков компилятора :-).

vzzo ★★★
()

Для пущей уверенности можно использовать std::atomic, с соответсвующим std::memory_order (в крайнем случае с std::memory_order_seq_cst)

KennyMinigun ★★★★★
()
Ответ на: комментарий от KennyMinigun

Например сделай std::atomic на task_status:


enum Status { deferred, in_progress, ready };

std::atomic<Status> task_status;

{
  Status prev_status;
  if (task_status.compare_exchange_strong(prev_status, 
                                          in_progress,
                                          std::memory_order_release, 
                                          std::memory_order_relaxed))
  {
    execute_task();
    task_status.store(std::memory_order_release);
    cv.notify_all();
  }
}
KennyMinigun ★★★★★
()
Ответ на: комментарий от Deleted

Да, только походу task_status.store() надо тогда делать с std::memory_order_acq_rel. Тогда имеем уверенность, что execute_task() (грубо говоря) не можно перенести после .store() а cv.notify_all() — не можно перенести перед .store().

KennyMinigun ★★★★★
()
Последнее исправление: KennyMinigun (всего исправлений: 1)

В примитивах синхронизации тем или иным образом создают барьер памяти на методах lock/unlock. Поэтому reordering через них запрещён.

quiet_readonly ★★★★
()
Ответ на: комментарий от Deleted

В смысле... Блокировка/разблокировка мьютексв это же полный memory fenece/seq барьер а через него вроде ничего не reorderится

lberserq
()
Ответ на: комментарий от KennyMinigun

C memory_order неточность, но идею я понял. Я вот о чём:

template<class P>
void std::condition_variable::wait(std::unique_lock<std::mutex>& lock, P p)
                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Сюда нужно лок отдать, и не абы какой, а тот же самый, под которым выставляется, в моём случае, task_status

Deleted
()
Ответ на: комментарий от quiet_readonly

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

Deleted
()
Ответ на: комментарий от Deleted

Сюда нужно лок отдать, и не абы какой, а тот же самый, под которым выставляется, в моём случае, task_status

В std::atomic вообще может и не быть лока (lock-free), так что надо либо лочить отдельным локом, либо синхронизироваться на task_status, в стиле():

while (task_status.load(std::memory_order_acquire) != ready) 
{
  std::this_thread::sleep_for(INTERVAL);
}

UPD Хотя, on the second thought, может atomic тут и не подходит (если надо ждать).

KennyMinigun ★★★★★
()
Последнее исправление: KennyMinigun (всего исправлений: 2)
Ответ на: комментарий от KennyMinigun

Вариант с busy wait очевиден, но ожидание предполагается долгим, следовательно не прокатит. Отельный лок - как только он появляется, возвращаются проблемы из ОП)

Deleted
()
Ответ на: комментарий от Deleted

Отельный лок - как только он появляется, возвращаются проблемы из ОП)

Если с атомиками то лок можно не использовать для синхронизации на статусе, а только чтоб не делать busy wait.

http://www.youtube.com/watch?v=c1gO9aB9nbs?t=39m29s

KennyMinigun ★★★★★
()
Ответ на: комментарий от KennyMinigun

Без лока ждать не получиться

Deleted
()
Ответ на: комментарий от Deleted

Что неа? C++11 1.10.5

«The library defines a number of atomic operations (Clause 29) and operations on mutexes (Clause 30) that are specially identified as synchronization operations. These operations play a special role in making assignments in one thread visible to another. A synchronization operation on one or more memory locations is either a consume operation, an acquire operation, a release operation, or both an acquire and release operation. A synchronization operation without an associated memory location is a fence and can be either an acquire fence, a release fence, or both an acquire and release fence.»

lberserq
()
Последнее исправление: lberserq (всего исправлений: 2)
Ответ на: комментарий от lberserq

Ну и где там

полный memory fenece/seq барьер

? На случай, если решишь, что это acquire_release_fence - вспомни, какие именно ограничения он вносит

Deleted
()
Ответ на: комментарий от KennyMinigun

Для пущей уверенности можно использовать std::atomic

lock()/unlock() уже содержам атомарные операции...

anonymous
()
Ответ на: комментарий от anonymous

lock()/unlock() уже содержам атомарные операции...

Ну, (может) да. Но в данном случае нам важнее memory barriers. Которые тоже там есть.

KennyMinigun ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.