Уничтожение переменной условия случайным образом теряет уведомление

Учитывая условие_вариабель как член класса, я понимаю, что:

В свете этих ожиданий, мой вопрос: почему код примера ниже случайно не уведомляет ожидающий поток?

#include <mutex>
#include <condition_variable>
#define NOTIFY_IN_DESTRUCTOR 

struct notify_on_delete {
    std::condition_variable cv;

    ~notify_on_delete() {
#ifdef NOTIFY_IN_DESTRUCTOR
        cv.notify_all();
#endif
    }
};

int main () {
    for (int trial = 0; trial < 10000; ++trial) {
        notify_on_delete* nod = new notify_on_delete();
        std::mutex flag;
        bool kill = false;

        std::thread run([nod, &flag, &kill] () {
            std::unique_lock<std::mutex> lock(flag);
            kill = true;
            nod->cv.wait(lock);
        });

        while(true) {
            std::unique_lock<std::mutex> lock(flag);
            if (!kill) continue;
#ifdef NOTIFY_IN_DESTRUCTOR
            delete nod;
#else
            nod->cv.notify_all();
#endif
            break;
        }
        run.join();
#ifndef NOTIFY_IN_DESTRUCTOR
        delete nod;
#endif
    }
    return 0;
}

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

Я использую Apple Clang: Apple LLVM версия 9.0.0 (clang-900.0.39.2) Цель: x86_64-apple-darwin17.3.0 Модель резьбы: posix С++ 14, скомпилированный с установленными флажками DEBUG.

EDIT:

Чтобы уточнить: этот вопрос касается семантики указанного поведения экземпляров condition_variable. Второй пункт выше, как представляется, закреплен в следующей quote:

Blockquote Требуется: на этом не должно быть потока. [Примечание. То есть все потоки должны быть уведомлены; они могут впоследствии блокировать блокировку, указанную в ожидании. Это расслабляет обычные правила, которые потребовали бы, чтобы все вызовы ожидания произошли до разрушения. Только уведомление о разблокировании ожидания должно произойти до уничтожения. Пользователь должен позаботиться о том, чтобы нити не ждали * этого, как только деструктор был запущен, особенно когда ожидающие потоки вызывают функции ожидания в цикле или используют перегрузки wait, wait_for или wait_until, которые берут предикат. - конечная нота]

Основной семантический вопрос кажется тем, что означает "заблокированный". Моя нынешняя интерпретация приведенной выше цитаты состояла бы в том, что после строки

cv.notify_all(); // defined NOTIFY_IN_DESTRUCTOR

in ~ notify_on_delete() тест потока не "заблокирован на", то есть я понял, что после этого вызова произошло "уведомление о разблокировании ожидания", поэтому согласно цитате требование было выполнено для продолжения уничтожения условия_переменной экземпляра.

Может ли кто-нибудь дать разъяснение "заблокировано" или "уведомление о разблокировании" на то, что в приведенном выше коде вызов notify_all() не удовлетворяет требованиям ~ condition_variable()?

Ответы

Ответ 1

Я уверен, что реализация ваших поставщиков нарушена. Ваша программа выглядит почти нормально с точки зрения соблюдения контракта с классами cv/mutex. Я не мог проверить 100%, я за одной версией.

Понятие "блокировка" вводит в заблуждение в классе condition_variable (CV), потому что есть несколько объектов, которые нужно блокировать. Контракт требует, чтобы реализация была более сложной, чем шпон на pthread_cond * (например). Мое его чтение указывает, что для одного CV потребуется не менее 2 pthread_cond_ts.

Суть - это деструктор, имеющий определение, когда потоки ожидают CV; и его разрушение находится в гонке между CV.wait и ~ CV. Наивная реализация просто имеет ~ CV, транслирует конвеер, а затем исключает его, и CV.wait запоминает блокировку в локальной переменной, так что когда она пробуждается из среды выполнения, ее блокировка больше не должна ссылаться на объект. В этой реализации ~ CV становится механизмом "огонь и забвение".

К сожалению, гоночный CV.wait может соответствовать предварительным условиям, но еще не закончен, взаимодействуя с объектом, когда ~ CV пробирается и разрушает его. Для разрешения гонки CV.wait и ~ CV необходимо исключить друг друга, поэтому CV требует, по крайней мере, частного мьютекса для разрешения рас.

Мы закончили еще. Обычно не существует базовой поддержки [например. ядро] для операции типа "ждать на cv, контролируемой блокировкой, и отпустить эту другую блокировку, как только я заблокирован". Я думаю, что даже люди, занимавшие пост, обнаружили, что это слишком смешно. Таким образом, похоронить мьютекс в моем резюме недостаточно, я действительно требую механизма, который позволяет мне обрабатывать события внутри него; таким образом, частная condvar требуется внутри реализации CV. Обязательный David Parnas meme.

Почти все, потому что, как указывает Марек Р., вы полагаетесь на ссылку на класс после его уничтожения; а не класс cv/mutex, ваш класс notify_on_delete. Конфликт немного академичен. Я сомневаюсь, что clang будет зависеть от того, что кий остался действительным после того, как управление было перенесено на nod- > cv.wait(); но реальный клиент большинства поставщиков компиляторов - это ориентиры, а не программисты.

Как и в целом, многопоточное программирование затруднено, и теперь, достигнув максимума в модели потоковой передачи С++, лучше всего дать ему десять или два года, чтобы успокоиться. Его контракты поразительны. Когда я впервые посмотрел на вашу программу, я подумал: "Нет, вы не можете уничтожить cv, к которому можно получить доступ, потому что RAII. Глупый я.

Pthreads - еще один ужасный API для потоковой передачи. По крайней мере, он не пытается превысить охват и достаточно зрелый, что надежные тестовые комплекты поддерживают поставщиков в очереди.

Ответ 2

Когда установлен NOTIFY_IN_DESTRUCTOR:
Вызов notify_one()/notify_all() не означает, что ожидающий поток немедленно разбуждается, и текущий поток будет ждать другого потока. Это просто означает, что если ожидающий поток просыпается в какой-то момент после того, как текущий поток вызвал уведомление, он должен продолжить. Поэтому, по сути, вы можете удалить переменную условия до того, как ожидание потока просыпается (в зависимости от того, как потоки запланированы).

Объяснение причины зависания, даже если переменная условия удаляется, пока другой поток ожидает ее, лежит на том, что операции wait/notify реализованы с использованием очередей, связанных с переменными условия. Эти очереди содержат потоки, ожидающие переменные условия. Освобождение переменной условия означало бы избавиться от этих очередей потоков.