Синхронизация потоков
Хотя потоки предназначены для параллельного выполнения кода, бывают случаи, когда потоки должны приостанавливаться и ожидать других потоков. Например, если два потока пытаются одновременно записать в одну и ту же переменную, результат будет неопределенным. Принцип принудительного ожидания одних потоков другими называется взаимным исключением. Это распространенный метод защиты общих ресурсов, таких как данные.
Qt предоставляет как низкоуровневые примитивы, так и высокоуровневые механизмы для синхронизации потоков.
Низкоуровневые примитивы синхронизации
QMutex — это базовый класс для обеспечения взаимного исключения. Поток блокирует мьютекс, чтобы получить доступ к общему ресурсу. Если второй поток пытается заблокировать мьютекс, когда он уже заблокирован, второй поток будет приостановлен, пока первый поток не завершит свою задачу и не разблокирует мьютекс.
QReadWriteLock похож на QMutex, за исключением того, что он различает «чтение» и «запись». Когда к данным не происходит запись, несколько потоков могут одновременно читать их. QMutex заставляет несколько читателей поочередно читать общие данные, но QReadWriteLock позволяет одновременное чтение, что повышает параллелизм.
QSemaphore — это обобщение QMutex, которое защищает определенное количество идентичных ресурсов. В отличие от QMutex, который защищает ровно один ресурс. Пример Семафоров демонстрирует типичное применение семафоров: синхронизацию доступа к кольцевому буферу между производителем и потребителем.
QWaitCondition синхронизирует потоки не путем обеспечения взаимного исключения, а путем предоставления условной переменной. В то время как другие примитивы заставляют потоки ждать, пока ресурс не будет разблокирован, QWaitCondition заставляет потоки ждать, пока не будет выполнено определенное условие. Чтобы позволить ожидающим потокам продолжить работу, вызовите wakeOne() для разбуждения случайным образом выбранного потока или wakeAll() для одновременного разбуждения всех.
Пример Условных переменных демонстрирует, как решить проблему производителя-потребителя с помощью QWaitCondition вместо QSemaphore.
Примечание: Классы синхронизации Qt полагаются на использование правильно выровненных указателей. Например, вы не можете использовать упакованные классы с MSVC.
Эти классы синхронизации могут быть использованы для обеспечения безопасности метода в отношении потоков. Однако это влечёт за собой потери производительности, поэтому большинство методов Qt не делаются безопасными для многопоточного использования.
Риски
Если поток заблокировал ресурс, но не разблокировал его, приложение может зависнуть, так как ресурс станет недоступным для других потоков. Это может произойти, например, если возникнет исключение и заставит текущую функцию вернуть результат без освобождения блокировки.
Другая аналогичная ситуация — это тупик. Например, предположим, что поток А ожидает, пока поток В разблокирует ресурс. Если поток В также ожидает, пока поток А разблокирует другой ресурс, оба потока будут бесконечно ждать, и приложение зависнет.
Классы-удобства
QMutexLocker, QReadLocker и QWriteLocker — это классы-удобства, которые упрощают использование QMutex и QReadWriteLock. Они блокируют ресурс при создании и автоматически разблокируют его при уничтожении. Они предназначены для упрощения кода, использующего QMutex и QReadWriteLock, тем самым уменьшая вероятность случайной постоянной блокировки ресурса.
Высокоуровневые очереди событий
Система событий Qt очень полезна для межпотоковой коммуникации. Каждый поток может иметь свою собственную очередь событий. Чтобы вызвать слот (или любой вызываемый метод) в другом потоке, поместите этот вызов в очередь событий целевого потока. Это позволит целевому потоку завершить текущую задачу перед запуском слота, в то время как исходный поток будет продолжать работу параллельно.
Чтобы поместить вызов в очередь событий, создайте связанную сигнал-слот очередь. Всякий раз, когда сигнал излучается, его аргументы будут записаны системой событий. Поток, в котором находится получатель сигнала находится, затем выполнит слот. В качестве альтернативы, вызовите QMetaObject::invokeMethod(), чтобы достичь того же результата без сигналов. В обоих случаях необходимо использовать очередное соединение, потому что прямое соединение обходит систему событий и выполняет метод сразу в текущем потоке.
В отличие от использования низкоуровневых примитивов, при использовании системы событий для синхронизации потоков риск тупиковых ситуаций отсутствует. Однако система событий не обеспечивает взаимного исключения. Если вызываемые методы обращаются к общим данным, они все равно должны быть защищены низкоуровневыми примитивами.
Тем не менее, система событий Qt вместе с неявно разделяемыми структурами данных предлагает альтернативу традиционному блокированию потоков. Если сигналы и слоты используются исключительно и никакие переменные не общие между потоками, многопоточное приложение может обойтись без низкоуровневых примитивов.
См. также QThread::exec() и Потоки и QObjects.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.9/threads-synchronizing.html