Действительно ли условной переменной нужна другая переменная? - PullRequest
2 голосов
/ 29 марта 2020

Примечание: я приведу примеры на C ++, но я считаю, что мой вопрос не зависит от языка c. Поправьте меня, если я ошибаюсь.

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

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

Теперь, каждый пример, который я видел, использует объект условной переменной (например, std::condition_variable), но также некоторую дополнительную переменную, чтобы пометить, если что-то произошло (например, bool dataWasLoaded). Взгляните на этот пример из https://thispointer.com//c11-multithreading-part-7-condition-variables-explained/:

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
    bool m_bDataLoaded;
public:
    Application()
    {
        m_bDataLoaded = false;
    }
    void loadData()
    {
        // Make This Thread sleep for 1 Second
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        // Lock The Data structure
        std::lock_guard<std::mutex> guard(m_mutex);
        // Set the flag to true, means data is loaded
        m_bDataLoaded = true;
        // Notify the condition variable
        m_condVar.notify_one();
    }
    bool isDataLoaded()
    {
        return m_bDataLoaded;
    }
    void mainTask()
    {
        std::cout << "Do Some Handshaking" << std::endl;
        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the Condition Variable to get signaled
        // Wait() will internally release the lock and make the thread to block
        // As soon as condition variable get signaled, resume the thread and
        // again acquire the lock. Then check if condition is met or not
        // If condition is met then continue else again go in wait.
        m_condVar.wait(mlock, std::bind(&Application::isDataLoaded, this));
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};
int main()
{
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}

Теперь, кроме std::condition_variable m_condVar, он также использует дополнительную переменную bool m_bDataLoaded. Но мне кажется, что поток, выполняющий mainTask, уже уведомлен о том, что данные были загружены с помощью std::condition_variable m_condVar. Зачем проверять bool m_bDataLoaded на ту же информацию? Сравните (тот же код без bool m_bDataLoaded):

#include <iostream>
#include <thread>
#include <functional>
#include <mutex>
#include <condition_variable>
using namespace std::placeholders;
class Application
{
    std::mutex m_mutex;
    std::condition_variable m_condVar;
public:
    Application()
    {
    }
    void loadData()
    {
        // Make This Thread sleep for 1 Second
        std::this_thread::sleep_for(std::chrono::milliseconds(1000));
        std::cout << "Loading Data from XML" << std::endl;
        // Lock The Data structure
        std::lock_guard<std::mutex> guard(m_mutex);
        // Notify the condition variable
        m_condVar.notify_one();
    }
    void mainTask()
    {
        std::cout << "Do Some Handshaking" << std::endl;
        // Acquire the lock
        std::unique_lock<std::mutex> mlock(m_mutex);
        // Start waiting for the Condition Variable to get signaled
        // Wait() will internally release the lock and make the thread to block
        // As soon as condition variable get signaled, resume the thread and
        // again acquire the lock. Then check if condition is met or not
        // If condition is met then continue else again go in wait.
        m_condVar.wait(mlock);
        std::cout << "Do Processing On loaded Data" << std::endl;
    }
};
int main()
{
    Application app;
    std::thread thread_1(&Application::mainTask, &app);
    std::thread thread_2(&Application::loadData, &app);
    thread_2.join();
    thread_1.join();
    return 0;
}
  1. Теперь я знаю о ложных пробуждениях, и только они требуют использования дополнительной переменной. Мой вопрос - они они только причина для этого? Если бы они не возникали, можно ли просто использовать условные переменные без каких-либо дополнительных переменных (и, кстати, тогда это не делает название «условная переменная» неправильным)?
  2. Другое дело - разве использование из дополнительных переменных единственная причина, почему условные переменные также требуют мьютекса? Если нет, то каковы другие причины?
  3. Если необходимы дополнительные переменные (для ложных пробуждений или по другим причинам), почему API не требует их (во 2-м коде я не использовал их для код для компиляции)? (я не знаю, то же самое в других языках, поэтому этот вопрос может быть C ++ - уточняется c.)

1 Ответ

3 голосов
/ 30 марта 2020

Это не все о ложных пробуждениях.

Когда вы звоните m_condvar.wait, как вы узнаете, что условие, которого вы ждете, еще не произошло?

Возможно, 'loadData' имеет уже был вызван в другой теме. Когда он вызвал notify_one(), ничего не произошло, потому что не было ожидающих потоков.

Теперь, если вы позвоните condvar.wait, вы будете ждать вечно, потому что ничто не будет сигнализировать вам.

Оригинальная версия делает не имеют этой проблемы, потому что:

  1. Если m_bDataLoaded имеет значение false, то он знает, что данные не загружены, и что после m_bDataLoaded установлено значение true, вызывающая сторона сообщит о состоянии;
  2. Блокировка удерживается, и мы знаем, что m_bDataLoaded нельзя изменить в другом потоке, пока он не будет освобожден;
  3. condvar.wait поместит текущий поток в очередь ожидания до снятия блокировки, поэтому мы знаем, что m_bDataLoaded будет установлен в true после мы начнем ждать, и поэтому notify_one также будет называться после мы начинаем ждать.

Чтобы ответить на другие ваши вопросы:

  • Да, согласование с дополнительными переменными является причиной, по которой переменные условия связаны с мьютексами.
  • API не требует, скажем, логического var выполнимо, потому что это не всегда то условие, которого вы ждете.

Такие вещи встречаются часто, например:

Task *getTask() {
    //anyone who uses m_taskQueue or m_shutDown must lock this mutex
    unique_lock<mutex> lock(m_mutex);

    while (m_taskQueue.isEmpty()) {
        if (m_shutdown) {
            return null;
        }
        // this is signalled after a task is enqueued
        // or m_shutdown is asserted
        m_condvar.wait(lock);
    }
    return taskQueue.pop_front();
}

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...