线程本地存储在哪些平台上有限,有多少可用?

时间:2009-09-22 14:37:55

标签: c++ multithreading cross-platform boost-thread thread-local

我最近意识到线程本地存储在某些平台上是有限的。例如,C ++库boost :: thread read:

的文档

“注意:可以创建的特定于线程的存储对象的数量存在特定于实现的限制,并且此限制可能很小。”

我一直在寻找尝试找出不同平台的限制,但我找不到权威表。如果您正在编写使用TLS的跨平台应用程序,这是一个重要问题。 Linux是我找到信息的唯一平台,以Ingo Monar补丁的形式于2002年发送到内核列表添加TLS支持,他提到“TLS区域的数量是无限的,没有 与TLS支持相关的额外分配开销。“如果在2009年仍然如此(是吗?)非常漂亮。

但是今天Linux怎么样? OS X?视窗?的Solaris?嵌入式操作系统?对于在多种体系结构上运行的操作系统,它是否因架构而异?

编辑:如果您对可能存在限制的原因感到好奇,请考虑预先分配线程本地存储的空间,因此您将为每个线程上的支付费用 。面对很多线程,即使是少量线程也是一个问题。

6 个答案:

答案 0 :(得分:11)

在Linux上,如果您使用__thread TLS数据,唯一的限制是由可用的地址空间设置的,因为这些数据只是被分配为gs引用的常规RAM(在x86上)或fs(在x86-64上)段描述符。请注意,在某些情况下,动态加载的库使用的TLS数据的分配可以在不使用该TLS数据的线程中省略。

pthread_key_create和朋友分配的TLS仅限于PTHREAD_KEYS_MAX个插槽(这适用于所有符合标准的pthreads实现)。

有关Linux上TLS实施的更多信息,请参阅ELF Handling For Thread-Local StorageThe Native POSIX Thread Library for Linux

也就是说,如果你需要可移植性,最好的办法是尽量减少TLS的使用 - 在TLS中放一个指针,然后将你需要的所有东西都挂在指针上。

答案 1 :(得分:3)

我在Windows上只使用过TLS,版本之间存在细微差别,可以使用多少: http://msdn.microsoft.com/en-us/library/ms686749(VS.85).aspx

我认为您的代码仅针对支持线程的操作系统 - 过去我曾使用过不支持线程的嵌入式和桌面操作系统,所以不支持TLS。

答案 2 :(得分:1)

在Mac上,我知道Task-Specific Storage API中的Multiprocessing Services

MPAllocateTaskStorageIndex
MPDeallocateTaskStorageIndex
MPGetTaskStorageValue
MPSetTaskStorageValue

这与Windows线程本地存储非常相似。

我不确定此API是否目前推荐用于Mac上的线程本地存储。也许有更新的东西。

答案 3 :(得分:0)

可能是提升文档只是谈论一般的可配置限制,而不一定是平台的一些硬限制。在Linux上,ulimit命令限制了资源进程可以拥有的数量(线程数,堆栈大小,内存和一堆其他东西)。这将间接影响您的线程本地存储。在我的系统上,似乎没有特定于线程本地存储的ulimit条目。其他平台可能有一种方法来指定它自己。此外,我认为在许多多处理器系统中,线程本地存储将位于专用于该CPU的内存中,因此您可能会在系统整体耗尽内存之前很久就遇到物理内存限制。我会假设在那种情况下有一些回退行为来定位主内存中的数据,但我不知道。你可以说,我猜想了很多。希望它仍然能引导你走向正确的方向......

答案 4 :(得分:0)

Windows上的线程本地存储declspec限制您仅将其用于静态变量,这意味着如果您想以更具创造性的方式使用它,则运气不佳。

Windows上有一个低级别的API,但它已经破坏了语义,这使得初始化非常难以理解:你无法判断你的线程是否已经看到该变量,因此你需要明确初始化当你创建线程时。

另一方面,用于线程本地存储的pthread API经过深思熟虑且灵活。

答案 5 :(得分:0)

我使用一个简单的模板类来提供线程本地存储。这只是包含std::map和一个关键部分。这样就不会遇到任何特定于平台的线程本地问题,唯一的平台要求是将当前线程id作为整数获取。它可能比本机线程本地存储慢一点,但它可以存储任何数据类型。

以下是我的代码的缩减版本。我已经删除了默认值逻辑来简化代码。因为它可以存储任何数据类型,所以增量和减量运算符仅在T支持它们时可用。关键部分仅用于保护查找和插入地图。一旦返回引用,就可以安全地使用不受保护的,因为只有当前线程才会使用该值。

template <class T>
class ThreadLocal
{
public:
    operator T()
    {
        return value();
    }

    T & operator++()
    {
        return ++value();
    }

    T operator++(int)
    {
        return value()++;
    }

    T & operator--()
    {
        return --value();
    }

    T operator--(int)
    {
        return value()--;
    }

    T & operator=(const T& v)
    {
        return (value() = v);
    }

private:
    T & value()
    {
        LockGuard<CriticalSection> lock(m_cs);
        return m_threadMap[Thread::getThreadID()];
    }

    CriticalSection     m_cs;
    std::map<int, T>    m_threadMap;
};

要使用此类,我通常会在类中声明一个静态成员,例如

class DBConnection {
    DBConnection() {
        ++m_connectionCount;
    }

    ~DBConnection() {
        --m_connectionCount;
    }

    // ...
    static ThreadLocal<unsigned int> m_connectionCount;
};

ThreadLocal<unsigned int> DBConnection::m_connectionCount

它可能并不适用于所有情况,但它涵盖了我的需求,我可以轻松添加它在我发现它时所缺少的任何功能。

bdonlan 是正确的,此示例在线程退出后不会清理。但是,这很容易添加手动清理。

template <class T>
class ThreadLocal
{
public:
    static void cleanup(ThreadLocal<T> & tl)
    {
        LockGuard<CriticalSection> lock(m_cs);
        tl.m_threadMap.erase(Thread::getThreadID());
    }

    class AutoCleanup {
    public:
        AutoCleanup(ThreadLocal<T> & tl) : m_tl(tl) {}
        ~AutoCleanup() {
            cleanup(m_tl);
        }

    private:
        ThreadLocal<T> m_tl
    }

    // ...
}

然后一个知道它的线程明确使用ThreadLocal可以在其主函数中使用ThreadLocal::AutoCleanup来清理变量。

或者在DBConnection的情况下

~DBConnection() {
    if (--m_connectionCount == 0)
        ThreadLocal<int>::cleanup(m_connectionCount);
}

cleanup()方法是静态的,以免干扰operator T()。可以使用全局函数来调用它来推断模板参数。