Перезаписываемость и потокобезопасность
В документации термины перезаписываемый и потокобезопасный используются для маркировки классов и функций, чтобы указать, как их можно использовать в многопоточных приложениях:
- Функция, потокобезопасная, может быть вызвана одновременно из нескольких потоков, даже при использовании общих данных, потому что все ссылки на общие данные сериализуются.
- Функция, перезаписываемая, также может быть вызвана одновременно из нескольких потоков, но только если каждый вызов использует свои собственные данные.
Следовательно, потокобезопасная функция всегда перезаписываемая, но перезаписываемая функция не всегда потокобезопасная.
Расширяя это, класс считается перезаписываемым, если его методы-члены могут безопасно вызываться из нескольких потоков, при условии, что каждый поток использует разный экземпляр класса. Класс считается потокобезопасным, если его методы-члены могут безопасно вызываться из нескольких потоков, даже если все потоки используют один и тот же экземпляр класса.
Примечание: Классы 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-5.15/threads-reentrancy.html