Синхронизация потоков
Хотя потоки предназначены для выполнения кода параллельно, иногда потоки должны приостанавливаться и ждать других потоков. Например, если два потока пытаются одновременно записать в одну и ту же переменную, результат будет неопределённым. Принцип принуждения потоков к ожиданию друг друга называется взаимным исключением. Это распространённая техника для защиты общих ресурсов, таких как данные.
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/archives/qt-5.6/threads-synchronizing.html