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