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