Синхронизация потоков
Хотя назначение потоков заключается в возможности выполнения кода параллельно, иногда потокам необходимо останавливаться и ожидать других потоков. Например, если два потока пытаются одновременно записать в одну переменную, результат будет неопределённым. Принцип принуждения потоков ожидать друг друга называется взаимным исключением. Это распространённый метод защиты общих ресурсов, таких как данные.
Qt предоставляет как низкоуровневые примитивы, так и высокоуровневые механизмы для синхронизации потоков.
Низкоуровневые примитивы синхронизации
QMutex — базовый класс для обеспечения взаимного исключения. Поток блокирует мьютекс, чтобы получить доступ к общему ресурсу. Если второй поток пытается заблокировать мьютекс, когда он уже заблокирован, второй поток будет помещён в состояние ожидания, пока первый поток не завершит свою задачу и не разблокирует мьютекс.
QReadWriteLock похож на QMutex, за исключением того, что он различает чтение и запись. Когда к данным не происходит запись, несколько потоков могут одновременно читать их безопасно. QMutex заставляет несколько читателей по очереди читать общие данные, а QReadWriteLock допускает одновременное чтение, тем самым улучшая параллелизм.
QSemaphore — обобщение QMutex, защищающее определённое количество идентичных ресурсов. В отличие от QMutex, который защищает ровно один ресурс. Пример с Семафорами демонстрирует типичное применение семафоров: синхронизацию доступа к кольцевому буферу между производителем и потребителем.
QWaitCondition синхронизирует потоки не путём обеспечения взаимного исключения, а путём предоставления условной переменной. В то время как другие примитивы заставляют потоки ждать, пока ресурс не разблокируется, QWaitCondition заставляет потоки ждать, пока не будет выполнено определённое условие. Чтобы позволить ожидающим потокам продолжить выполнение, вызовите wakeOne() для разбуждения одного случайно выбранного потока или wakeAll() для одновременного разбуждения всех.
Пример с Условными переменными показывает, как решить проблему производителя-потребителя с помощью QWaitCondition вместо QSemaphore.
Примечание: Классы синхронизации Qt полагаются на использование правильно выровненных указателей. Например, вы не можете использовать упакованные классы с MSVC.
Эти классы синхронизации могут использоваться для создания потокобезопасных методов. Однако это влечёт за собой снижение производительности, поэтому большинство методов Qt не делают потокобезопасными.
Риски
Если поток блокирует ресурс, но не разблокирует его, приложение может зависнуть, потому что ресурс станет навсегда недоступным для других потоков. Это может произойти, например, если возникает исключение и заставляет текущую функцию вернуться, не освобождая блокировку.
Ещё один похожий сценарий — это тупик. Например, предположим, что поток A ждёт, пока поток B не разблокирует ресурс. Если поток B также ждёт, пока поток A не разблокирует другой ресурс, оба потока будут ждать вечно, поэтому приложение зависнет.
Удобные классы
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-6.1/threads-synchronizing.html