Возвращаемость и потокобезопасность
Всюду в документации термины возвращаемый и потокобезопасный используются для обозначения классов и функций, чтобы указать, как они могут быть использованы в многопоточных приложениях:
- Функция, потокобезопасная, может вызываться одновременно из нескольких потоков, даже когда вызовы используют общие данные, потому что все ссылки на общие данные сериализуются.
- Функция, возвращаемая, также может вызываться одновременно из нескольких потоков, но только в том случае, если каждый вызов использует свои собственные данные.
Таким образом, функция потокобезопасная всегда возвращаемая, но функция возвращаемая не всегда потокобезопасная.
По аналогии, класс считается возвращаемым, если его члены-функции могут безопасно вызываться из нескольких потоков, при условии, что каждый поток использует разный экземпляр класса. Класс считается потокобезопасным, если его члены-функции могут безопасно вызываться из нескольких потоков, даже если все потоки используют один и тот же экземпляр класса.
Примечание: Классы Qt документируются как потокобезопасные только в том случае, если они предназначены для использования несколькими потоками. Если функция не помечена как потокобезопасная или возвращаемая, она не должна использоваться из разных потоков. Если класс не помечен как потокобезопасный или возвращаемый, то к конкретному экземпляру этого класса не следует обращаться из разных потоков.
Возвращаемость
Классы C++ часто являются возвращаемыми, просто потому что они обращаются только к своим собственным данным-членам. Любой поток может вызвать член-функцию экземпляра возвращаемого класса, при условии, что ни один другой поток не может вызвать член-функцию у того же экземпляра класса в одно и то же время. Например, класс Counter ниже является возвращаемым:
class Counter
{
public:
Counter() { n = 0; }
void increment() { ++n; }
void decrement() { --n; }
int value() const { return n; }
private:
int n;
}; Класс не является потокобезопасным, потому что, если несколько потоков пытаются изменить данные-члена n, результат неопределён. Это происходит потому, что операторы ++ и -- не всегда являются атомными. Действительно, они обычно расширяются до трёх машинных инструкций:
- Загрузка значения переменной в регистр.
- Инкремент или декремент значения регистра.
- Возврат значения регистра в основную память.
Если поток A и поток B одновременно загружают старое значение переменной, инкрементируют своё значение в регистре и сохраняют его обратно, они перезаписывают друг друга, и переменная инкрементируется только один раз!
Потокобезопасность
Очевидно, доступ должен быть сериализован: поток A должен выполнить шаги 1, 2, 3 без прерывания (атомарно), прежде чем поток B сможет выполнить те же шаги; или наоборот. Простой способ сделать класс потокобезопасным — защитить весь доступ к данным-членам с помощью QMutex:
class Counter
{
public:
Counter() { n = 0; }
void increment() { QMutexLocker locker(&mutex); ++n; }
void decrement() { QMutexLocker locker(&mutex); --n; }
int value() const { QMutexLocker locker(&mutex); return n; }
private:
mutable QMutex mutex;
int n;
}; Класс QMutexLocker автоматически блокирует мьютекс в своём конструкторе и разблокирует его при вызове деструктора в конце функции. Блокировка мьютекса гарантирует, что доступ из разных потоков будет сериализован. Данные-член mutex объявлен с квалификатором mutable, потому что нам нужно заблокировать и разблокировать мьютекс в value(), что является константной функцией.
Примечания по классам Qt
Многие классы Qt являются возвращаемыми, но они не являются потокобезопасными, потому что сделать их потокобезопасными потребовало бы дополнительных накладных расходов на многократное блокирование и разблокирование QMutex. Например, QString является возвращаемым, но не потокобезопасным. Вы можете безопасно обращаться к разным экземплярам QString из нескольких потоков одновременно, но вы не можете безопасно обращаться к одному и тому же экземпляру QString из нескольких потоков одновременно (если вы сами не защищаете доступ с помощью QMutex).
Некоторые классы и функции Qt являются потокобезопасными. В основном это классы, связанные с потоками (например, QMutex), и фундаментальные функции (например, QCoreApplication::postEvent()).
Примечание: Терминология в области многопоточности не полностью стандартизована. POSIX использует определения возвращаемых и потокобезопасных, которые несколько отличаются для своих C-API. При использовании других библиотек классов C++ с Qt убедитесь, что определения поняты.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-6.2/threads-reentrancy.html