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