На каких платформах ограничено локальное хранилище потоков и сколько доступно?

Недавно мне стало известно, что локальное хранилище потоков ограничено на некоторых платформах. Например, документы для библиотеки С++ boost:: thread read:

"Примечание. Ограничение реализации зависит от количества объектов хранения конкретных потоков, которые могут быть созданы, и этот предел может быть небольшим."

Я искал попытку выяснить пределы для разных платформ, но мне не удалось найти авторитетную таблицу. Это важный вопрос, если вы пишете кроссплатформенное приложение, использующее TLS. Linux была единственной платформой, на которой я нашел информацию, в виде патча Ingo Monar, отправленного в 2002 году в список ядра, добавляющий поддержку TLS, где он упоминает: "Количество TLS-областей неограничено, и нет дополнительные расходы на распределение, связанные с поддержкой TLS ". Что, если все еще верно в 2009 году (это так?), довольно изящно.

Но как насчет Linux сегодня? OS X? Окна? Solaris? Встроенные ОС? Для ОС, работающей на нескольких архитектурах, зависит ли она от разных архитектур?

Изменить: Если вам интересно, почему может быть предел, подумайте, что пространство для локального хранилища потоков будет предварительно распределено, поэтому вы будете платить за него в каждом отдельном потоке. Даже небольшая сумма перед лицом множества потоков может быть проблемой.

Ответы

Ответ 1

В Linux, если вы используете __thread TLS-данные, единственное ограничение устанавливается вашим доступным адресным пространством, так как эти данные просто распределяются как обычная оперативная память, на которую ссылаются gs (на x86) или fs (на x86-64) дескрипторы сегментов. Обратите внимание, что в некоторых случаях распределение данных TLS, используемых динамически загружаемыми библиотеками, может быть отменено в потоках, которые не используют данные TLS.

TLS, назначенные pthread_key_create, а друзья, однако, ограничены слотами PTHREAD_KEYS_MAX (это относится ко всем соответствующим реализациям pthreads).

Для получения дополнительной информации о реализации TLS в Linux см. Обработка ELF для хранилища на основе темы и Собственная библиотека потоков POSIX для Linux.

Тем не менее, если вам нужна переносимость, лучше всего свести к минимуму использование TLS - поместите один указатель в TLS и поместите все, что вам нужно, в структуру данных, отвисшую от этого указателя.

Ответ 2

Я использовал TLS только в Windows, и есть небольшие различия между версиями в том, сколько можно использовать: http://msdn.microsoft.com/en-us/library/ms686749(VS.85).aspx

Я предполагаю, что ваш код ориентирован только на операционные системы, поддерживающие потоки - в прошлом я работал со встроенными и настольными ОС, которые не поддерживают потоки, поэтому не поддерживают TLS.

Ответ 3

На Mac я знаю Task-Specific Storage в Многопроцессорные сервисы API:

MPAllocateTaskStorageIndex
MPDeallocateTaskStorageIndex
MPGetTaskStorageValue
MPSetTaskStorageValue

Это похоже на локальное хранилище Windows.

Я не уверен, что этот API в настоящее время рекомендуется для локального хранилища потоков на Mac. Возможно, что-то новое.

Ответ 4

Возможно, документация по ускорению просто говорит об общем настраиваемом лимите, не обязательно о каком-то жестком пределе платформы. В Linux ограничение ulimit ограничений ресурсов может иметь (количество потоков, размер стека, память и множество других вещей), Это косвенно повлияет на локальное хранилище ваших потоков. В моей системе, похоже, нет записи в ulimit, специфичной для локального хранилища потоков. У других платформ есть возможность самостоятельно указать это. Кроме того, я думаю, что во многих многопроцессорных системах локальное хранилище потоков будет в памяти, предназначенном для этого процессора, поэтому вы можете столкнуться с ограничениями физической памяти задолго до того, как система в целом исчерпает свою память. Я бы предположил, что в этой ситуации есть какое-то резервное поведение, чтобы найти данные в основной памяти, но я не знаю. Как вы можете сказать, я многого хочу. Надеюсь, это все равно приведет вас в правильном направлении...

Ответ 5

Локальное хранилище данных declspec в Windows ограничивает его использование только для статических переменных, что означает, что вам не повезло, если вы хотите использовать его более творчески.

В Windows существует низкоуровневый API, но он имеет сломанную семантику, которая очень неудобно инициализировать: вы не можете определить, была ли переменная уже замечена вашим потоком, поэтому вам нужно явно инициализировать это когда вы создаете поток.

С другой стороны, API-интерфейс pthread для локального хранилища потоков хорошо продуман и гибкий.

Ответ 6

Я использую простой класс шаблонов для обеспечения локального хранилища потоков. Это просто обертывает std::map и критический раздел. Тогда это не страдает от каких-либо локальных проблем, связанных с конкретной конкретной платформой, единственное требование к платформе - получить текущий идентификатор потока, как в целочисленном. Это может быть немного медленнее, чем локальное хранилище собственного потока, но оно может хранить любой тип данных.

Ниже приведена сокращенная версия моего кода. Я удалил логику значений по умолчанию, чтобы упростить код. Поскольку он может хранить любой тип данных, операторы приращения и уменьшения доступны только в том случае, если 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(). Для вызова этой функции можно использовать глобальную функцию, которая выводит параметры шаблона.