在多线程环境中,当每个线程需要独立维护某些数据时,就需要用到线程本地存储(TLS, Thread-Local Storage)。不同的操作系统 TLS 的实现方式也不相同,但都提供了相应的接口。QT 作为一个可跨平台的编程工具,封装了不同操作系统对 TLS 的操作,通过 QThreadStorage 类为每个线程提供独立数据存储。
1. QThreadStorage 官方说明
QThreadStorage 是一个为线程提供独立数据存储的模板类,通过hasLocalData()、localData()、setLocalData() 访问和存储线程数据。如果 QThreadStorage 存储指针类型的数据,数据必须通过关键字 new 在堆上创建,QThreadStorage 拥有该数据的管理权,并在线程退出(正常结束或终止)时删除该数据。
注意:
- QThreadStorage 的析构函数不会删除每个线程的数据,只有线程退出或多次调用 setLocalData() 时才会删除线程数据。
- QThreadStorage 可用于存储主线程的数据。当 QApplication 销毁时(不管主线程是否结束),QThreadStorage 会删除其存储的主线程数据。
2. window 中 TLS 的操作方法
windows 平台中 TLS 索引通常由 TlsAlloc 函数在进程或 DLL 初始化期间分配。分配 TLS 索引后每个线程都有自己的索引槽。在分配 TLS 索引时,线程存储槽初始化为 NULL。进程的每个线程使用 TLS 索引来访问自己的 TLS 存储槽。线程存储数据时,指定 TLS 索引,调用 TlsSetValue 方法在其槽中存储值。线程检索存储数据时,指定相同的索引,调用用 TlsGetValue 检索存储的值。
下图说明了 TLS 的工作原理。
进程有两个线程:线程 1 和线程 2,分配两个 TLS 索引: gdwTlsIndex1 和 gdwTlsIndex2。 每个线程分配两个内存块(一个用于每个索引),用于存储数据,并将指向这些内存块的指针存储在相应的 TLS 槽中。 若要访问与索引关联的数据,线程将从 TLS 槽检索指向内存块的指针,并将其存储在 lpvData 本地变量中。
使用线程本地存储的示例如下:- #include <windows.h>
- #include <stdio.h>
- #define THREADCOUNT 4
- // TLS 索引
- DWORD dwTlsIndex;
- VOID ErrorExit (LPCSTR message);
- VOID CommonFunc(VOID)
- {
- LPVOID lpvData;
- // 获取线程本地存储的数据
- lpvData = TlsGetValue(dwTlsIndex);
- if ((lpvData == 0) && (GetLastError() != ERROR_SUCCESS))
- ErrorExit("TlsGetValue error");
- // 打印数据
- printf("common: thread %d: lpvData=%lx\n",
- GetCurrentThreadId(), lpvData);
- Sleep(5000);
- }
- DWORD WINAPI ThreadFunc(VOID)
- {
- LPVOID lpvData;
- // 初始化线程本地数据
- lpvData = (LPVOID) LocalAlloc(LPTR, 256);
- // 存储线程本地数据
- if (! TlsSetValue(dwTlsIndex, lpvData))
- ErrorExit("TlsSetValue error");
- printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData);
- CommonFunc();
- // 释放本地数据的存储空间
- lpvData = TlsGetValue(dwTlsIndex);
- if (lpvData != 0)
- LocalFree((HLOCAL) lpvData);
- return 0;
- }
- int main(VOID)
- {
- DWORD IDThread;
- HANDLE hThread[THREADCOUNT];
- int i;
- // 分配 TLS 索引
- if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
- ErrorExit("TlsAlloc failed");
- // 创建线程
- for (i = 0; i < THREADCOUNT; i++)
- {
- hThread[i] = CreateThread(NULL, 0,
- (LPTHREAD_START_ROUTINE) ThreadFunc,
- NULL, 0, &IDThread);
- if (hThread[i] == NULL)
- ErrorExit("CreateThread error\n");
- }
- for (i = 0; i < THREADCOUNT; i++)
- WaitForSingleObject(hThread[i], INFINITE);
- // 释放 TLS 索引
- TlsFree(dwTlsIndex);
- return 0;
- }
- VOID ErrorExit (LPCSTR message)
- {
- fprintf(stderr, "%s\n", message)
- ExitProcess(0);
- }
复制代码 3. linux 中 TLS 的操作方法
Linux 中线程本地存储成为 TSD(Thread specific data),它是 POSIX 标准的一部分。每个线程都拥有一个私有内存块,即线程专用的数据区,简称 TSD 区。该区域由 TSD 键索引。TSD 键在所有线程中都是通用的,每个 TSD 键在每个线程中都指向自己的 TSD 区域。TSD 区域存储 void * 类型值。TSD 区域可以看作存储 void * 类型的数组,TSD 键作为整数索引,TSD 键对应数组元素的值的就是线程里存储的值。线程创建时,TSD 键对应的区域都初始化为 NULL。
函数 pthread_key_create()用于新的 TSD 键。在给定时间分配的密钥数量存在一个上限 PTHREAD_KEYS_MAX。 在所有已运行的线程中,新建的 TSD 键所关联的值都为 NULL。
函数 pthread_key_delete() 用于收回给 TSD 键分配的空间。
函数 pthread_setspecific() 用于设置线程中 TSD 键对应的值。
函数 pthread_getspecific() 用于获取线程中 TSD 键对应的值。
使用示例如下:- /* 声明 TSD 键 */
- static pthread_key_t buffer_key;
- /* 仅初始化一次 */
- static pthread_once_t buffer_key_once = THREAD_ONCE_INIT;
- /* 创建线程专用缓冲区 */
- void buffer_alloc(void)
- {
- pthread_once(&buffer_key_once, buffer_key_alloc);
- pthread_setspecific(buffer_key, malloc(100));
- }
- /* 返回线程专用缓冲区 */
- char * get_buffer(void)
- {
- return (char *) pthread_getspecific(buffer_key);
- }
- /* 分配 TSD 键 */
- static void buffer_key_alloc()
- {
- pthread_key_create(&buffer_key, buffer_destroy);
- }
- /* 释放线程专用缓冲区 */
- static void buffer_destroy(void * buf)
- {
- free(buf);
- }
复制代码 4. QThreadStorage 源码分析
源码版本为 QT6.8.3,只列出关键源码。
下面是 QThreadStorage 的声明,可以看出数据的存取操作由 QThreadStorageData 来完成。QThreadStorageData 的 get() 方法用于获取线程私有数据,set()方法用于设置线程私有数据, id 是QThreadStorage 对象的唯一标识。- //\qtbase\src\corelib\thread\qthreadstorage.h
- ...
- class Q_CORE_EXPORT QThreadStorageData
- {
- public:
- explicit QThreadStorageData(void (*func)(void *));
- ~QThreadStorageData();
- void** get() const;
- void** set(void* p);
- static void finish(void**);
- int id;
- };
- #if !defined(QT_MOC_CPP)
- // MOC_SKIP_BEGIN
- // pointer specialization
- template <typename T>
- inline
- T *&qThreadStorage_localData(QThreadStorageData &d, T **)
- {
- void **v = d.get();
- if (!v) v = d.set(nullptr);
- return *(reinterpret_cast<T**>(v));
- }
- template <typename T>
- inline
- T *qThreadStorage_localData_const(const QThreadStorageData &d, T **)
- {
- void **v = d.get();
- return v ? *(reinterpret_cast<T**>(v)) : 0;
- }
- template <typename T>
- inline
- void qThreadStorage_setLocalData(QThreadStorageData &d, T **t)
- { (void) d.set(*t); }
- template <typename T>
- inline
- void qThreadStorage_deleteData(void *d, T **)
- { delete static_cast<T *>(d); }
- // value-based specialization
- template <typename T>
- inline
- T &qThreadStorage_localData(QThreadStorageData &d, T *)
- {
- void **v = d.get();
- if (!v) v = d.set(new T());
- return *(reinterpret_cast<T*>(*v));
- }
- template <typename T>
- inline
- T qThreadStorage_localData_const(const QThreadStorageData &d, T *)
- {
- void **v = d.get();
- return v ? *(reinterpret_cast<T*>(*v)) : T();
- }
- template <typename T>
- inline
- void qThreadStorage_setLocalData(QThreadStorageData &d, T *t)
- { (void) d.set(new T(*t)); }
- template <typename T>
- inline
- void qThreadStorage_deleteData(void *d, T *)
- { delete static_cast<T *>(d); }
- // MOC_SKIP_END
- #endif
- template <class T>
- class QThreadStorage
- {
- private:
- QThreadStorageData d;
- Q_DISABLE_COPY(QThreadStorage)
- static inline void deleteData(void *x)
- { qThreadStorage_deleteData(x, reinterpret_cast<T*>(0)); }
- public:
- inline QThreadStorage() : d(deleteData) { }
- inline ~QThreadStorage() { }
- inline bool hasLocalData() const
- { return d.get() != nullptr; }
- inline T& localData()
- { return qThreadStorage_localData(d, reinterpret_cast<T*>(0)); }
- inline T localData() const
- { return qThreadStorage_localData_const(d, reinterpret_cast<T*>(0)); }
- inline void setLocalData(T t)
- { qThreadStorage_setLocalData(d, &t); }
- };
复制代码 QThreadStorageData 的定义如下。从源码中可以看出具体的 TLS 数据来自 QThreadData,QThreadStorageData 的 get 或 set 方法都是利用 id 在 QThreadData 的 tls 中查找对应的值。- //\qtbase\src\corelib\thread\qthreadstorage.cpp
- ...
- // 此处定义了一个全局的 QList<void (*)(void *)> destructors
- // 用于存储 TLS 数据的清理方法,同时也是 QThreadStorage 对象标识生成的依据
- typedef QList<void (*)(void *)> DestructorMap;
- Q_GLOBAL_STATIC(DestructorMap, destructors)
- QThreadStorageData::QThreadStorageData(void (*func)(void *))
- {
- QMutexLocker locker(&destructorsMutex);
- DestructorMap *destr = destructors();
- if (!destr) {
- ...
- QThreadData *data = QThreadData::current();
- id = data->tls.size();
- ...
- return;
- }
- for (id = 0; id < destr->size(); id++) {
- if (destr->at(id) == nullptr)
- break;
- }
- // destructors 遍历完成的同时生成了 QThreadStorage 的唯一标识
- if (id == destr->size()) {
- destr->append(func);
- } else {
- (*destr)[id] = func;
- }
- ...
- }
- QThreadStorageData::~QThreadStorageData()
- {
- DEBUG_MSG("QThreadStorageData: Released id %d", id);
- QMutexLocker locker(&destructorsMutex);
- if (destructors())
- (*destructors())[id] = nullptr;
- }
- void **QThreadStorageData::get() const
- {
- QThreadData *data = QThreadData::current();
- if (!data) {
- qWarning("QThreadStorage::get: QThreadStorage can only be used with threads started with QThread");
- return nullptr;
- }
- QList<void *> &tls = data->tls;
- if (tls.size() <= id)
- tls.resize(id + 1);
- void **v = &tls[id];
- ...
- return *v ? v : nullptr;
- }
- void **QThreadStorageData::set(void *p)
- {
- QThreadData *data = QThreadData::current();
- if (!data) {
- qWarning("QThreadStorage::set: QThreadStorage can only be used with threads started with QThread");
- return nullptr;
- }
- QList<void *> &tls = data->tls;
- if (tls.size() <= id)
- tls.resize(id + 1);
- void *&value = tls[id];
- // delete any previous data
- if (value != nullptr) {
- ...
- QMutexLocker locker(&destructorsMutex);
- DestructorMap *destr = destructors();
- void (*destructor)(void *) = destr ? destr->value(id) : nullptr;
- locker.unlock();
- void *q = value;
- value = nullptr;
- if (destructor)
- destructor(q);
- }
- // store new data
- value = p;
- ...
- return &value;
- }
复制代码 QThreadData 声明如下。它提供的静态方法 current() 是查找线程本地存储的入口。该方法会根据不同操作系统提供不同的实现。- // qtbase\src\corelib\thread\qthread_p.h
- ...
- class QThreadData
- {
- public:
- ...
- static Q_AUTOTEST_EXPORT QThreadData *current(bool createIfNecessary = true);
- ...
- private:
- QAtomicInt _ref;
- public:
- int loopLevel;
- int scopeLevel;
- QStack<QEventLoop *> eventLoops;
- QPostEventList postEventList;
- QAtomicPointer<QThread> thread;
- QAtomicPointer<void> threadId;
- QAtomicPointer<QAbstractEventDispatcher> eventDispatcher;
- // 数据存储在此
- QList<void *> tls;
- bool quitNow;
- bool canWait;
- bool isAdopted;
- bool requiresCoreApplication;
- };
复制代码 windows 系统中 current() 方法的实现如下。从源码可以看出只有一个 TLS 索引,QT 中多个 QThreadStorage 对象都对应一个 TLS 索引,多个 QThreadStorage 对象存储的值在 QThreadData 的 tls 列表中,QThreadStorageData 对象的 id 标记了 QThreadStorage 在 tls 中的位置。- // qtbase\src\corelib\thread\qthread_win.cpp
- ...
- // TLS 索引
- static DWORD qt_current_thread_data_tls_index = TLS_OUT_OF_INDEXES;
- // 创建 TLS 索引
- void qt_create_tls()
- {
- if (qt_current_thread_data_tls_index != TLS_OUT_OF_INDEXES)
- return;
- Q_CONSTINIT static QBasicMutex mutex;
- QMutexLocker locker(&mutex);
- if (qt_current_thread_data_tls_index != TLS_OUT_OF_INDEXES)
- return;
- // 仅分配一次 TLS 索引
- qt_current_thread_data_tls_index = TlsAlloc();
- }
- ...
- QThreadData *QThreadData::current(bool createIfNecessary)
- {
- // 分配 TLS 索引
- qt_create_tls();
- // 该索引在线程中存储的数据类型为 QThreadData
- QThreadData *threadData = reinterpret_cast<QThreadData *>(TlsGetValue(qt_current_thread_data_tls_index));
- if (!threadData && createIfNecessary) {
- // 在线程内新建 QThreadData 数据
- threadData = new QThreadData;
- // This needs to be called prior to new AdoptedThread() to
- // avoid recursion.
- TlsSetValue(qt_current_thread_data_tls_index, threadData);
- QT_TRY {
- threadData->thread.storeRelease(new QAdoptedThread(threadData));
- } QT_CATCH(...) {
- TlsSetValue(qt_current_thread_data_tls_index, 0);
- threadData->deref();
- threadData = 0;
- QT_RETHROW;
- }
- threadData->deref();
- threadData->isAdopted = true;
- threadData->threadId.storeRelaxed(reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId())));
- if (!QCoreApplicationPrivate::theMainThreadId) {
- auto *mainThread = threadData->thread.loadRelaxed();
- mainThread->setObjectName("Qt mainThread");
- QCoreApplicationPrivate::theMainThread.storeRelease(mainThread);
- QCoreApplicationPrivate::theMainThreadId.storeRelaxed(threadData->threadId.loadRelaxed());
- } else {
- HANDLE realHandle = INVALID_HANDLE_VALUE;
- DuplicateHandle(GetCurrentProcess(),
- GetCurrentThread(),
- GetCurrentProcess(),
- &realHandle,
- 0,
- FALSE,
- DUPLICATE_SAME_ACCESS);
- qt_watch_adopted_thread(realHandle, threadData->thread.loadRelaxed());
- }
- }
- return threadData;
- }
复制代码 Linux系统中 current() 方法的实现如下。与 windows 相同,只提供了一个 TSD 键,QT 中多个 QThreadStorage 对象都对应一个 TSD 键。currentThreadData 通过 thread_local 关键字保证了每个线程都有一个副本,省去了通过 pthread_getspecific() 与 pthread_setspecific() 访问数据。- // qtbase\src\corelib\thread\qthread_unix.cpp
- ...
- // Always access this through the {get,set,clear}_thread_data() functions.
- // currentThreadData 声明为 thread_local 类型,保证每个线程一个副本
- Q_CONSTINIT static thread_local QThreadData *currentThreadData = nullptr;
- ...
- // Utility functions for getting, setting and clearing thread specific data.
- static QThreadData *get_thread_data()
- {
- return currentThreadData;
- }
- namespace {
- struct PThreadTlsKey
- {
- pthread_key_t key;
- // 创建 TSD 键
- PThreadTlsKey() noexcept { pthread_key_create(&key, destroy_current_thread_data); }
- ~PThreadTlsKey() { pthread_key_delete(key); }
- };
- }
- ...
- // TLS 索引
- static PThreadTlsKey pthreadTlsKey; // intentional non-trivial init & destruction
- static void set_thread_data(QThreadData *data) noexcept
- {
- if (data) {
- // As noted above: one global static for the thread that called
- // ::exit() (which may not be a Qt thread) and the pthread_key_t for
- // all others.
- static struct Cleanup {
- ~Cleanup() {
- if (QThreadData *data = get_thread_data())
- destroy_current_thread_data(data);
- }
- } currentThreadCleanup;
- pthread_setspecific(pthreadTlsKey.key, data);
- }
- currentThreadData = data;
- }
- ...
- QThreadData *QThreadData::current(bool createIfNecessary)
- {
- QThreadData *data = get_thread_data();
- if (!data && createIfNecessary) {
- data = new QThreadData;
- QT_TRY {
- set_thread_data(data);
- data->thread.storeRelease(new QAdoptedThread(data));
- } QT_CATCH(...) {
- clear_thread_data();
- data->deref();
- data = nullptr;
- QT_RETHROW;
- }
- data->deref();
- data->isAdopted = true;
- data->threadId.storeRelaxed(QThread::currentThreadId());
- if (!QCoreApplicationPrivate::theMainThreadId.loadAcquire()) {
- auto *mainThread = data->thread.loadRelaxed();
- mainThread->setObjectName("Qt mainThread");
- QCoreApplicationPrivate::theMainThread.storeRelease(mainThread);
- QCoreApplicationPrivate::theMainThreadId.storeRelaxed(data->threadId.loadRelaxed());
- }
- }
- return data;
- }
复制代码 本内容都来自与项目 Compelling Data Designer开发过程中的思考与总结。
项目 Compelling Data Designer 用于数据的可视化设计,软件采用可扩展架构,支持扩展图形插件、数据接口。项目仍在开发中,目前已设计完成基本图形、多属性配置、动画、数据驱动等功能。
https://github.com/lsyeei/dashboard
参考: 线程本地存储
Linux manual page
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |