介绍TLS 线程本地存储(Thread Local Storage, TLS) 是一种机制,允许每个线程拥有自己独立的一份数据。这些数据对于每个线程是唯一的,不会与其他线程共享。TLS 的主要目的是解决多线程编程中共享数据的竞争条件问题。 操作系统中的 TLS 操作系统为每个线程提供独立的 TLS 存储空间,这些空间在每个线程的上下文中是独立的。具体实现方式可能因操作系统不同而有所差异,但基本原理是相似的。
- TLS 在操作系统中的实现 Windows:Windows 提供了 TlsAlloc、TlsFree、TlsSetValue 和 TlsGetValue 这些 API,用于分配、释放和访问 TLS 槽。每个线程有一个 TLS 存储区,由操作系统管理。 POSIX(例如 Linux):POSIX 标准提供了 pthread_key_create、pthread_key_delete、pthread_setspecific 和 pthread_getspecific 这些 API。每个线程有一个与之关联的键值存储区,用于存储 TLS 数据。
- 线程上下文 每个线程在操作系统中都有自己的上下文,包括寄存器、堆栈、TLS 存储区等。TLS 存储区是线程上下文的一部分,并且是独立的。
- 关键函数 tlsAlloc:分配线程本地存储 (TLS) 索引。 进程的任何线程随后都可以使用此索引来存储和检索线程本地的值,因为每个线程都会收到自己的索引槽。 该函数一般仅被调用一次,返回的key值为一个多线程可访问的索引。但每个线程通过这个索引访问其独立的 TLS 存储区。
问题 : 如何确保每个线程通过相同的 key 访问不同的 TLS 存储区? 答案:操作系统为每个线程分配独立的 TLS 存储区,并通过上下文切换确保每个线程访问其独立的 TLS 数据。key 作为索引,指向每个线程的 TLS 存储区。操作系统的 TLS 机制确保同一个键在不同线程中指向不同的存储区,从而实现数据隔离。
Chromium TLS 通过 ThreadLocalStorage::Slot 管理槽位
- 第一个访问的线程,通过AllocTLS分配一次key,保存在 g_native_tls_key 中,其他线程直接使用已分配过的key。(操作系统实现:每个线程访问同一个key时,将会被指向自己的线程存储区)
- 开辟TlsVectorEntry[256]个向量,也即256个槽位,SetTlsVectorValue 存入线程存储区。
- 从 256个TlsVectorEntry 中寻找一个可用的槽位,将值存入 使用g_tls_metadata 管理 TlsMetadata 槽位数据 g_tls_metadata 维护了所有256个槽位的当前状态 struct TlsMetadata { TlsStatus status; base::ThreadLocalStorage::TLSDestructorFunc destructor; // Incremented every time a slot is reused. Used to detect reuse of slots. uint32_t version; }; 槽位分配和初始化 在 ThreadLocalStorage::Slot::Initialize 函数中,槽位被分配并初始化。函数会遍历 g_tls_metadata 数组,寻找一个空闲的槽位,并将其状态设置为 IN_USE,同时记录槽位的版本号。 void ThreadLocalStorage::Slot::Initialize(TLSDestructorFunc destructor) { PlatformThreadLocalStorage::TLSKey key = g_native_tls_key.load(std::memory_order_relaxed); if (key == PlatformThreadLocalStorage::TLS_KEY_OUT_OF_INDEXES || GetTlsVectorStateAndValue(key) == TlsVectorState::kUninitialized) { ConstructTlsVector(); }
// Grab a new slot. { base::AutoLock auto_lock(*GetTLSMetadataLock()); for (int i = 0; i < kThreadLocalStorageSize; ++i) { size_t slot_candidate = (g_last_assigned_slot + 1 + i) % kThreadLocalStorageSize; if (g_tls_metadata[slot_candidate].status == TlsStatus::FREE) { g_tls_metadata[slot_candidate].status = TlsStatus::IN_USE; g_tls_metadata[slot_candidate].destructor = destructor; g_last_assigned_slot = slot_candidate; slot_ = slot_candidate; version_ = g_tls_metadata[slot_candidate].version; break; } } } CHECK_NE(slot_, kInvalidSlotValue); CHECK_LT(slot_, kThreadLocalStorageSize); } 槽位释放和版本管理 在 ThreadLocalStorage::Slot::Free 函数中,槽位被释放。函数会将槽位状态设置为 FREE,清除析构函数,并递增槽位的版本号,以确保旧数据不会干扰新的数据。 void ThreadLocalStorage::Slot::Free() { DCHECK_NE(slot_, kInvalidSlotValue); DCHECK_LT(slot_, kThreadLocalStorageSize); { base::AutoLock auto_lock(*GetTLSMetadataLock()); g_tls_metadata[slot_].status = TlsStatus::FREE; g_tls_metadata[slot_].destructor = nullptr; ++(g_tls_metadata[slot_].version); } slot_ = kInvalidSlotValue; } 数据一致性和线程安全 版本号的作用 版本号在槽位分配和释放过程中起到关键作用。每次槽位被释放时,版本号递增,确保即使相同的槽位再次被分配,旧的数据不会干扰新的数据。 线程安全 使用 base::AutoLock 确保在多线程环境下对槽位元数据的操作是安全的,避免数据竞争。 总结 Chromium 的 TLS 设计通过以下机制确保高效且安全的线程本地存储:
- 使用 ThreadLocalStorage::Slot 类管理 TLS 槽位。
- 通过 g_tls_metadata 数组存储槽位的元数据,包括状态、析构函数和版本号。
- 在槽位分配和释放时,更新槽位的状态和版本号,确保数据的唯一性和一致性。
- 使用锁机制确保多线程环境下的操作安全。
链接 msvc 编译的 Chrome TLS 在 Windows 下的实现 https://github.com/guopengwei-github/ChromeTLS