Почему избыточный дополнительный блок области видимости влияет на поведение std :: lock_guard? - PullRequest
0 голосов
/ 30 октября 2018

Этот код демонстрирует, что мьютекс распределяется между двумя потоками, но с областью видимости вокруг thread_mutex.

происходит нечто странное.

(у меня есть вариант этого кода в другой вопрос , но это кажется второй загадкой.)

#include <thread>
#include <mutex>
#include <iostream>

#include <unistd.h>

    int main ()
    {
        std::mutex m;

        std::thread t ([&] ()
        {
            while (true)
            {
                {
                    std::lock_guard <std::mutex> thread_lock (m);

                    usleep (10*1000); // or whatever
                }

                std::cerr << "#";
                std::cerr.flush ();
            }
        });

        while (true)
        {
            std::lock_guard <std::mutex> main_lock (m);
            std::cerr << ".";
            std::cerr.flush ();
        }
    }

Это в принципе работает, как есть, но теоретический блок вокруг thread_lock теоретически не должен быть необходим. Однако, если вы закомментируете это ...

#include <thread>
#include <mutex>
#include <iostream>

#include <unistd.h>

int main ()
{
    std::mutex m;

    std::thread t ([&] ()
    {
        while (true)
        {
//          {
                std::lock_guard <std::mutex> thread_lock (m);

                usleep (10*1000); // or whatever
//          }

            std::cerr << "#";
            std::cerr.flush ();
        }
    });

    while (true)
    {
        std::lock_guard <std::mutex> main_lock (m);
        std::cerr << ".";
        std::cerr.flush ();
    }
}

Вывод выглядит так:

........########################################################################################################################################################################################################################################################################################################################################################################################################################################################################################

т.е., похоже, что thread_lock НИКОГДА не уступает main_lock.

Почему thread_lock всегда получает блокировку и main_lock всегда ожидает, если удаленный блок области видимости удален?

Ответы [ 2 ]

0 голосов
/ 31 октября 2018

Я протестировал ваш код (с удаленной областью блока) в Linux с GCC (7.3.0), используя pthreads, и получил те же результаты, что и вы. Основной поток истощен, хотя, если бы я подождал достаточно долго, я бы иногда видел, как основной поток выполняет какую-то работу.

Тем не менее, я запустил тот же код в Windows с MSVC (19.15), и поток не был истощен.

Похоже, вы используете posix, так что я полагаю, что ваша стандартная библиотека использует pthreads на серверной части? (Я должен связать pthreads даже с C ++ 11.) Мьютексы Pthreads не гарантируют справедливости. Но это только половина истории. Похоже, ваш вывод связан с вызовом usleep.

Если я достану usleep, я увижу справедливость (Linux):

    // fair again
    while (true)
    {
        std::lock_guard <std::mutex> thread_lock (m);
        std::cerr << "#";
        std::cerr.flush ();
    }

Я предполагаю, что из-за того, что во время удержания мьютекса он так долго спит, практически гарантируется, что основной поток будет , поскольку заблокирован, поскольку заблокирован может быть . Представьте, что сначала основной поток может попытаться раскрутиться в надежде, что мьютекс скоро станет доступен. Через некоторое время он может попасть в список ожидания.

Во вспомогательном потоке объект lock_guard уничтожается в конце цикла, поэтому мьютекс освобождается. Он разбудит основной поток, но он немедленно создаст новый lock_guard, который снова блокирует мьютекс. Маловероятно, что основной поток захватит мьютекс, потому что он был запланирован. Поэтому, если в этом небольшом окне не произойдет переключение контекста, вспомогательный поток, вероятно, снова получит мьютекс.

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

Как сказал @Ted Lyngmo в своем ответе, если вы добавите сон перед созданием lock_guard, это значительно снизит вероятность голодания.

    while (true)
    {
        usleep (1);
        std::lock_guard <std::mutex> thread_lock (m);
        usleep (10*1000);
        std::cerr << "#";
        std::cerr.flush ();
    }

Я также попробовал это с yield, но мне нужно было 5+, чтобы сделать его более справедливым, что наводит меня на мысль, что существуют другие нюансы в фактических деталях реализации библиотеки, планировщике ОС и эффектах подсистемы кэширования и памяти.

Кстати, спасибо за отличный вопрос. Это было действительно легко проверить и поиграть.

0 голосов
/ 30 октября 2018

Вы можете дать подсказку для переназначения, сдавая потоки (или спя), не владея мьютексом. Довольно длинный сон ниже вероятно заставит его вывести #. #. #. #. в совершенстве. Если вы переключитесь на урожайность, вы, вероятно, получите блоки ############ ..............., но в конечном итоге примерно 50/50.

#include <thread>
#include <mutex>
#include <iostream>

#include <unistd.h>

int main ()
{
    std::mutex m;

    std::thread t ([&] ()
    {
        while (true)
        {
            usleep (10000);
            //std::this_thread::yield();
            std::lock_guard <std::mutex> thread_lock (m);

            std::cerr << "#" << std::flush;
        }
    });

    while (true)
    {
        usleep (10000);
        //std::this_thread::yield();
        std::lock_guard <std::mutex> main_lock (m);
        std::cerr << "." << std::flush;
    }
}
...