详解 QThreadStorage 的实现机制

在多线程环境中,当每个线程需要独立维护某些数据时,就需要用到线程本地存储(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 的工作原理。
image
进程有两个线程:线程 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
image

img

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

posted @ 2026-01-26 10:54  永不停转  阅读(4)  评论(0)    收藏  举报