Почему избыточный дополнительный блок области видимости влияет на поведение std :: lock_guard?

Этот код демонстрирует, что мьютекс распределяется между двумя потоками, но что-то странное происходит с блоком обзора вокруг 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 всегда ждать, если лишний блок обзора удаляется?

3

Решение

Я протестировал ваш код (с удаленной областью блоков) в 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+, чтобы сделать его более справедливым, что заставляет меня поверить, что есть и другие нюансы в фактических деталях реализации библиотеки, планировщике ОС и эффектах подсистемы кэширования и памяти.

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

2

Другие решения

Вы можете дать ему подсказку о перепланировании, сдавая потоки (или спя) без владения мьютексом. Довольно долгий сон ниже будет наверное заставить его вывести #. #. #. #. в совершенстве. Если вы переключитесь на урожайность, вы, вероятно, получите блоки из ############ ……………, но примерно 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;
}
}
0

По вопросам рекламы [email protected]