找回密码
 立即注册
首页 业界区 安全 详解 QThreadStorage 的实现机制

详解 QThreadStorage 的实现机制

荡俊屯 7 天前
在多线程环境中,当每个线程需要独立维护某些数据时,就需要用到线程本地存储(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.png

进程有两个线程:线程 1 和线程 2,分配两个 TLS 索引: gdwTlsIndex1 和 gdwTlsIndex2。 每个线程分配两个内存块(一个用于每个索引),用于存储数据,并将指向这些内存块的指针存储在相应的 TLS 槽中。 若要访问与索引关联的数据,线程将从 TLS 槽检索指向内存块的指针,并将其存储在 lpvData 本地变量中。
使用线程本地存储的示例如下:
  1. #include <windows.h>
  2. #include <stdio.h>
  3. #define THREADCOUNT 4
  4. // TLS 索引
  5. DWORD dwTlsIndex;
  6. VOID ErrorExit (LPCSTR message);
  7. VOID CommonFunc(VOID)
  8. {
  9.    LPVOID lpvData;
  10.    // 获取线程本地存储的数据
  11.    lpvData = TlsGetValue(dwTlsIndex);
  12.    if ((lpvData == 0) && (GetLastError() != ERROR_SUCCESS))
  13.       ErrorExit("TlsGetValue error");
  14.    // 打印数据
  15.    printf("common: thread %d: lpvData=%lx\n",
  16.       GetCurrentThreadId(), lpvData);
  17.    Sleep(5000);
  18. }
  19. DWORD WINAPI ThreadFunc(VOID)
  20. {
  21.    LPVOID lpvData;
  22.    // 初始化线程本地数据
  23.    lpvData = (LPVOID) LocalAlloc(LPTR, 256);
  24.    // 存储线程本地数据
  25.    if (! TlsSetValue(dwTlsIndex, lpvData))
  26.       ErrorExit("TlsSetValue error");
  27.    printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData);
  28.    CommonFunc();
  29.    // 释放本地数据的存储空间
  30.    lpvData = TlsGetValue(dwTlsIndex);
  31.    if (lpvData != 0)
  32.       LocalFree((HLOCAL) lpvData);
  33.    return 0;
  34. }
  35. int main(VOID)
  36. {
  37.    DWORD IDThread;
  38.    HANDLE hThread[THREADCOUNT];
  39.    int i;
  40.    // 分配 TLS 索引
  41.    if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
  42.       ErrorExit("TlsAlloc failed");
  43.    // 创建线程
  44.    for (i = 0; i < THREADCOUNT; i++)
  45.    {
  46.       hThread[i] = CreateThread(NULL, 0,
  47.          (LPTHREAD_START_ROUTINE) ThreadFunc,
  48.          NULL, 0, &IDThread);
  49.       if (hThread[i] == NULL)
  50.          ErrorExit("CreateThread error\n");
  51.    }
  52.    for (i = 0; i < THREADCOUNT; i++)
  53.       WaitForSingleObject(hThread[i], INFINITE);
  54.    // 释放 TLS 索引
  55.    TlsFree(dwTlsIndex);
  56.    return 0;
  57. }
  58. VOID ErrorExit (LPCSTR message)
  59. {
  60.    fprintf(stderr, "%s\n", message)
  61.    ExitProcess(0);
  62. }
复制代码
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 键对应的值。
使用示例如下:
  1.    /* 声明 TSD 键 */
  2.    static pthread_key_t buffer_key;
  3.    /* 仅初始化一次 */
  4.    static pthread_once_t buffer_key_once = THREAD_ONCE_INIT;
  5.    /* 创建线程专用缓冲区 */
  6.    void buffer_alloc(void)
  7.    {
  8.       pthread_once(&buffer_key_once, buffer_key_alloc);
  9.       pthread_setspecific(buffer_key, malloc(100));
  10.    }
  11.    /* 返回线程专用缓冲区 */
  12.    char * get_buffer(void)
  13.    {
  14.       return (char *) pthread_getspecific(buffer_key);
  15.    }
  16.    /* 分配 TSD 键 */
  17.    static void buffer_key_alloc()
  18.    {
  19.       pthread_key_create(&buffer_key, buffer_destroy);
  20.    }
  21.    /* 释放线程专用缓冲区 */
  22.    static void buffer_destroy(void * buf)
  23.    {
  24.       free(buf);
  25.    }
复制代码
4. QThreadStorage 源码分析

源码版本为 QT6.8.3,只列出关键源码。
下面是 QThreadStorage 的声明,可以看出数据的存取操作由 QThreadStorageData 来完成。QThreadStorageData 的 get() 方法用于获取线程私有数据,set()方法用于设置线程私有数据, id 是QThreadStorage 对象的唯一标识。
  1. //\qtbase\src\corelib\thread\qthreadstorage.h
  2. ...
  3. class Q_CORE_EXPORT QThreadStorageData
  4. {
  5. public:
  6.     explicit QThreadStorageData(void (*func)(void *));
  7.     ~QThreadStorageData();
  8.     void** get() const;
  9.     void** set(void* p);
  10.     static void finish(void**);
  11.     int id;
  12. };
  13. #if !defined(QT_MOC_CPP)
  14. // MOC_SKIP_BEGIN
  15. // pointer specialization
  16. template <typename T>
  17. inline
  18. T *&qThreadStorage_localData(QThreadStorageData &d, T **)
  19. {
  20.     void **v = d.get();
  21.     if (!v) v = d.set(nullptr);
  22.     return *(reinterpret_cast<T**>(v));
  23. }
  24. template <typename T>
  25. inline
  26. T *qThreadStorage_localData_const(const QThreadStorageData &d, T **)
  27. {
  28.     void **v = d.get();
  29.     return v ? *(reinterpret_cast<T**>(v)) : 0;
  30. }
  31. template <typename T>
  32. inline
  33. void qThreadStorage_setLocalData(QThreadStorageData &d, T **t)
  34. { (void) d.set(*t); }
  35. template <typename T>
  36. inline
  37. void qThreadStorage_deleteData(void *d, T **)
  38. { delete static_cast<T *>(d); }
  39. // value-based specialization
  40. template <typename T>
  41. inline
  42. T &qThreadStorage_localData(QThreadStorageData &d, T *)
  43. {
  44.     void **v = d.get();
  45.     if (!v) v = d.set(new T());
  46.     return *(reinterpret_cast<T*>(*v));
  47. }
  48. template <typename T>
  49. inline
  50. T qThreadStorage_localData_const(const QThreadStorageData &d, T *)
  51. {
  52.     void **v = d.get();
  53.     return v ? *(reinterpret_cast<T*>(*v)) : T();
  54. }
  55. template <typename T>
  56. inline
  57. void qThreadStorage_setLocalData(QThreadStorageData &d, T *t)
  58. { (void) d.set(new T(*t)); }
  59. template <typename T>
  60. inline
  61. void qThreadStorage_deleteData(void *d, T *)
  62. { delete static_cast<T *>(d); }
  63. // MOC_SKIP_END
  64. #endif
  65. template <class T>
  66. class QThreadStorage
  67. {
  68. private:
  69.     QThreadStorageData d;
  70.     Q_DISABLE_COPY(QThreadStorage)
  71.     static inline void deleteData(void *x)
  72.     { qThreadStorage_deleteData(x, reinterpret_cast<T*>(0)); }
  73. public:
  74.     inline QThreadStorage() : d(deleteData) { }
  75.     inline ~QThreadStorage() { }
  76.     inline bool hasLocalData() const
  77.     { return d.get() != nullptr; }
  78.     inline T& localData()
  79.     { return qThreadStorage_localData(d, reinterpret_cast<T*>(0)); }
  80.     inline T localData() const
  81.     { return qThreadStorage_localData_const(d, reinterpret_cast<T*>(0)); }
  82.     inline void setLocalData(T t)
  83.     { qThreadStorage_setLocalData(d, &t); }
  84. };
复制代码
QThreadStorageData 的定义如下。从源码中可以看出具体的 TLS 数据来自 QThreadData,QThreadStorageData 的 get 或 set 方法都是利用 id 在 QThreadData 的 tls 中查找对应的值。
  1. //\qtbase\src\corelib\thread\qthreadstorage.cpp
  2. ...
  3. // 此处定义了一个全局的 QList<void (*)(void *)> destructors
  4. // 用于存储 TLS 数据的清理方法,同时也是 QThreadStorage 对象标识生成的依据
  5. typedef QList<void (*)(void *)> DestructorMap;
  6. Q_GLOBAL_STATIC(DestructorMap, destructors)
  7. QThreadStorageData::QThreadStorageData(void (*func)(void *))
  8. {
  9.     QMutexLocker locker(&destructorsMutex);
  10.     DestructorMap *destr = destructors();
  11.     if (!destr) {
  12.         ...
  13.         QThreadData *data = QThreadData::current();
  14.         id = data->tls.size();
  15.         ...
  16.         return;
  17.     }
  18.     for (id = 0; id < destr->size(); id++) {
  19.         if (destr->at(id) == nullptr)
  20.             break;
  21.     }
  22.    // destructors 遍历完成的同时生成了 QThreadStorage 的唯一标识
  23.     if (id == destr->size()) {
  24.         destr->append(func);
  25.     } else {
  26.         (*destr)[id] = func;
  27.     }
  28.    ...
  29. }
  30. QThreadStorageData::~QThreadStorageData()
  31. {
  32.     DEBUG_MSG("QThreadStorageData: Released id %d", id);
  33.     QMutexLocker locker(&destructorsMutex);
  34.     if (destructors())
  35.         (*destructors())[id] = nullptr;
  36. }
  37. void **QThreadStorageData::get() const
  38. {
  39.     QThreadData *data = QThreadData::current();
  40.     if (!data) {
  41.         qWarning("QThreadStorage::get: QThreadStorage can only be used with threads started with QThread");
  42.         return nullptr;
  43.     }
  44.     QList<void *> &tls = data->tls;
  45.     if (tls.size() <= id)
  46.         tls.resize(id + 1);
  47.     void **v = &tls[id];
  48.     ...
  49.     return *v ? v : nullptr;
  50. }
  51. void **QThreadStorageData::set(void *p)
  52. {
  53.     QThreadData *data = QThreadData::current();
  54.     if (!data) {
  55.         qWarning("QThreadStorage::set: QThreadStorage can only be used with threads started with QThread");
  56.         return nullptr;
  57.     }
  58.     QList<void *> &tls = data->tls;
  59.     if (tls.size() <= id)
  60.         tls.resize(id + 1);
  61.     void *&value = tls[id];
  62.     // delete any previous data
  63.     if (value != nullptr) {
  64.        ...
  65.         QMutexLocker locker(&destructorsMutex);
  66.         DestructorMap *destr = destructors();
  67.         void (*destructor)(void *) = destr ? destr->value(id) : nullptr;
  68.         locker.unlock();
  69.         void *q = value;
  70.         value = nullptr;
  71.         if (destructor)
  72.             destructor(q);
  73.     }
  74.     // store new data
  75.     value = p;
  76.     ...
  77.     return &value;
  78. }
复制代码
QThreadData 声明如下。它提供的静态方法 current() 是查找线程本地存储的入口。该方法会根据不同操作系统提供不同的实现。
  1. // qtbase\src\corelib\thread\qthread_p.h
  2. ...
  3. class QThreadData
  4. {
  5. public:
  6. ...
  7.    static Q_AUTOTEST_EXPORT QThreadData *current(bool createIfNecessary = true);
  8.    ...
  9. private:
  10.     QAtomicInt _ref;
  11. public:
  12.     int loopLevel;
  13.     int scopeLevel;
  14.     QStack<QEventLoop *> eventLoops;
  15.     QPostEventList postEventList;
  16.     QAtomicPointer<QThread> thread;
  17.     QAtomicPointer<void> threadId;
  18.     QAtomicPointer<QAbstractEventDispatcher> eventDispatcher;
  19.     // 数据存储在此
  20.     QList<void *> tls;
  21.     bool quitNow;
  22.     bool canWait;
  23.     bool isAdopted;
  24.     bool requiresCoreApplication;
  25. };
复制代码
windows 系统中 current() 方法的实现如下。从源码可以看出只有一个 TLS 索引,QT 中多个 QThreadStorage 对象都对应一个 TLS 索引,多个 QThreadStorage 对象存储的值在 QThreadData 的 tls 列表中,QThreadStorageData 对象的 id 标记了 QThreadStorage 在 tls 中的位置。
  1. // qtbase\src\corelib\thread\qthread_win.cpp
  2. ...
  3. // TLS 索引
  4. static DWORD qt_current_thread_data_tls_index = TLS_OUT_OF_INDEXES;
  5. // 创建 TLS 索引
  6. void qt_create_tls()
  7. {
  8.     if (qt_current_thread_data_tls_index != TLS_OUT_OF_INDEXES)
  9.         return;
  10.     Q_CONSTINIT static QBasicMutex mutex;
  11.     QMutexLocker locker(&mutex);
  12.     if (qt_current_thread_data_tls_index != TLS_OUT_OF_INDEXES)
  13.         return;
  14.     // 仅分配一次 TLS 索引
  15.     qt_current_thread_data_tls_index = TlsAlloc();
  16. }
  17. ...
  18. QThreadData *QThreadData::current(bool createIfNecessary)
  19. {
  20.     // 分配 TLS 索引
  21.     qt_create_tls();
  22.     // 该索引在线程中存储的数据类型为 QThreadData
  23.     QThreadData *threadData = reinterpret_cast<QThreadData *>(TlsGetValue(qt_current_thread_data_tls_index));
  24.     if (!threadData && createIfNecessary) {
  25.         // 在线程内新建 QThreadData 数据
  26.         threadData = new QThreadData;
  27.         // This needs to be called prior to new AdoptedThread() to
  28.         // avoid recursion.
  29.         TlsSetValue(qt_current_thread_data_tls_index, threadData);
  30.         QT_TRY {
  31.             threadData->thread.storeRelease(new QAdoptedThread(threadData));
  32.         } QT_CATCH(...) {
  33.             TlsSetValue(qt_current_thread_data_tls_index, 0);
  34.             threadData->deref();
  35.             threadData = 0;
  36.             QT_RETHROW;
  37.         }
  38.         threadData->deref();
  39.         threadData->isAdopted = true;
  40.         threadData->threadId.storeRelaxed(reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId())));
  41.         if (!QCoreApplicationPrivate::theMainThreadId) {
  42.             auto *mainThread = threadData->thread.loadRelaxed();
  43.             mainThread->setObjectName("Qt mainThread");
  44.             QCoreApplicationPrivate::theMainThread.storeRelease(mainThread);
  45.             QCoreApplicationPrivate::theMainThreadId.storeRelaxed(threadData->threadId.loadRelaxed());
  46.         } else {
  47.             HANDLE realHandle = INVALID_HANDLE_VALUE;
  48.             DuplicateHandle(GetCurrentProcess(),
  49.                     GetCurrentThread(),
  50.                     GetCurrentProcess(),
  51.                     &realHandle,
  52.                     0,
  53.                     FALSE,
  54.                     DUPLICATE_SAME_ACCESS);
  55.             qt_watch_adopted_thread(realHandle, threadData->thread.loadRelaxed());
  56.         }
  57.     }
  58.     return threadData;
  59. }
复制代码
Linux系统中 current() 方法的实现如下。与 windows 相同,只提供了一个 TSD 键,QT 中多个 QThreadStorage 对象都对应一个 TSD 键。currentThreadData 通过 thread_local 关键字保证了每个线程都有一个副本,省去了通过 pthread_getspecific() 与 pthread_setspecific() 访问数据。
  1. // qtbase\src\corelib\thread\qthread_unix.cpp
  2. ...
  3. // Always access this through the {get,set,clear}_thread_data() functions.
  4. // currentThreadData 声明为 thread_local 类型,保证每个线程一个副本
  5. Q_CONSTINIT static thread_local QThreadData *currentThreadData = nullptr;
  6. ...
  7. // Utility functions for getting, setting and clearing thread specific data.
  8. static QThreadData *get_thread_data()
  9. {
  10.     return currentThreadData;
  11. }
  12. namespace {
  13. struct PThreadTlsKey
  14. {
  15.     pthread_key_t key;
  16.     // 创建 TSD 键
  17.     PThreadTlsKey() noexcept { pthread_key_create(&key, destroy_current_thread_data); }
  18.     ~PThreadTlsKey() { pthread_key_delete(key); }
  19. };
  20. }
  21. ...
  22. // TLS 索引
  23. static PThreadTlsKey pthreadTlsKey; // intentional non-trivial init & destruction
  24. static void set_thread_data(QThreadData *data) noexcept
  25. {
  26.     if (data) {
  27.         // As noted above: one global static for the thread that called
  28.         // ::exit() (which may not be a Qt thread) and the pthread_key_t for
  29.         // all others.
  30.         static struct Cleanup {
  31.             ~Cleanup() {
  32.                 if (QThreadData *data = get_thread_data())
  33.                     destroy_current_thread_data(data);
  34.             }
  35.         } currentThreadCleanup;
  36.         pthread_setspecific(pthreadTlsKey.key, data);
  37.     }
  38.     currentThreadData = data;
  39. }
  40. ...
  41. QThreadData *QThreadData::current(bool createIfNecessary)
  42. {
  43.     QThreadData *data = get_thread_data();
  44.     if (!data && createIfNecessary) {
  45.         data = new QThreadData;
  46.         QT_TRY {
  47.             set_thread_data(data);
  48.             data->thread.storeRelease(new QAdoptedThread(data));
  49.         } QT_CATCH(...) {
  50.             clear_thread_data();
  51.             data->deref();
  52.             data = nullptr;
  53.             QT_RETHROW;
  54.         }
  55.         data->deref();
  56.         data->isAdopted = true;
  57.         data->threadId.storeRelaxed(QThread::currentThreadId());
  58.         if (!QCoreApplicationPrivate::theMainThreadId.loadAcquire()) {
  59.             auto *mainThread = data->thread.loadRelaxed();
  60.             mainThread->setObjectName("Qt mainThread");
  61.             QCoreApplicationPrivate::theMainThread.storeRelease(mainThread);
  62.             QCoreApplicationPrivate::theMainThreadId.storeRelaxed(data->threadId.loadRelaxed());
  63.         }
  64.     }
  65.     return data;
  66. }
复制代码
本内容都来自与项目 Compelling Data Designer开发过程中的思考与总结。
项目 Compelling Data Designer 用于数据的可视化设计,软件采用可扩展架构,支持扩展图形插件、数据接口。项目仍在开发中,目前已设计完成基本图形、多属性配置、动画、数据驱动等功能。
https://github.com/lsyeei/dashboard
2.png

3.gif

参考: 线程本地存储
Linux manual page

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册