15

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

“注意:可以创建的线程特定存储对象的数量有一个特定于实现的限制,这个限制可能很小。”

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

但是今天的 Linux 呢?操作系统?视窗?索拉里斯?嵌入式操作系统?对于在多种架构上运行的操作系统,它是否因架构而异?

编辑:如果您好奇为什么可能会有限制,请考虑线程本地存储的空间将被预先分配,因此您将在每个线程上为此付出成本。面对大量线程,即使是少量也可能是个问题。

4

6 回答 6

13

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

pthread_key_create然而,由和朋友分配的 TLS仅限于PTHREAD_KEYS_MAX槽(这适用于所有符合标准的 pthread 实现)。

有关 Linux 上 TLS 实现的更多信息,请参阅线程本地存储的 ELF 处理Linux 的本机 POSIX 线程库

也就是说,如果您需要可移植性,最好的办法是尽量减少 TLS 的使用——在 TLS 中放置一个指针,并将您需要的所有内容放在挂起该指针的数据结构中。

于 2009-09-22T16:11:48.713 回答
4

我只在Windows上使用过TLS,版本之间在可以使用多少方面略有不同:http: //msdn.microsoft.com/en-us/library/ms686749 (VS.85).aspx

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

于 2009-09-22T15:22:28.623 回答
1

On the Mac, I know of Task-Specific Storage in the Multiprocessing Services API:

MPAllocateTaskStorageIndex
MPDeallocateTaskStorageIndex
MPGetTaskStorageValue
MPSetTaskStorageValue

This looks very similar to Windows thread local storage.

I'm not sure if this API is currently recommended for thread local storage on the Mac. Perhaps there is something newer.

于 2010-02-05T19:42:53.310 回答
0

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

下面是我的代码的精简版。我删除了默认值逻辑以简化代码。T由于它可以存储任何数据类型,因此增量和减量运算符仅在支持它们时才可用。关键部分只需要保护查找和插入到地图中。一旦返回一个引用,使用 unprotected 是安全的,因为只有当前线程会使用这个值。

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
    }

    // ...
}

然后一个知道它的线程在其主函数中显式使用ThreadLocalcan useThreadLocal::AutoCleanup来清理变量。

或者在 DBConnection 的情况下

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

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

于 2009-09-22T16:51:41.113 回答
0

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

于 2009-09-22T14:59:42.750 回答
0

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

Windows 上有一个低级 API,但它破坏了语义,使得初始化非常尴尬:您无法判断该变量是否已经被您的线程看到,因此您需要在执行时显式初始化它创建线程。

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

于 2009-09-22T16:30:15.507 回答