線程本地數(shù)據(jù)(TLS)
?專欄內(nèi)容:
參天引擎內(nèi)核架構(gòu)
本專欄一起來聊聊參天引擎內(nèi)核架構(gòu),以及如何實現(xiàn)多機的數(shù)據(jù)庫節(jié)點的多讀多寫,與傳統(tǒng)主備,MPP的區(qū)別,技術(shù)難點的分析,數(shù)據(jù)元數(shù)據(jù)同步,多主節(jié)點的情況下對故障容災(zāi)的支持。手寫數(shù)據(jù)庫toadb
本專欄主要介紹如何從零開發(fā),開發(fā)的步驟,以及開發(fā)過程中的涉及的原理,遇到的問題等,讓大家能跟上并且可以一起開發(fā),讓每個需要的人成為參與者。
本專欄會定期更新,對應(yīng)的代碼也會定期更新,每個階段的代碼會打上tag,方便階段學(xué)習(xí)。
?開源貢獻(xiàn):
- toadb開源庫
個人主頁:我的主頁
管理社區(qū):開源數(shù)據(jù)庫
座右銘:天行健,君子以自強不息;地勢坤,君子以厚德載物.
前言
現(xiàn)代的CPU都是多core處理器,而且在intel處理器中每個core又可以多個processor,形成了多任務(wù)并行處理的硬件架構(gòu),在服務(wù)器端的處理器上架構(gòu)又有一些不同,傳統(tǒng)的采用SMP,也就是對稱的多任務(wù)處理架構(gòu),每個任務(wù)都可以對等的訪問所有內(nèi)存,外設(shè)等,而如今在ARM系列CPU上,多采用NUMA架構(gòu),它將CPU核分了幾個組,給每個組的CPU core分配了對應(yīng)的內(nèi)存和外設(shè),CPU訪問對應(yīng)的內(nèi)存和外設(shè)時速度最優(yōu),跨組訪問時性能會降底一些。
隨著硬件技術(shù)的持續(xù)發(fā)展,它們對一般應(yīng)用的性能優(yōu)化能力越來越強,同時對于服務(wù)器軟件的開發(fā),提出更高要求,要想達(dá)到極高的并發(fā)和性能,就需要充分利用當(dāng)前硬件架構(gòu)的特點,對它們進(jìn)行壓榨。那么,我們的應(yīng)用至少也是要采用多任務(wù)架構(gòu),不管是多線程還是多進(jìn)程的多任務(wù)架構(gòu),才可以充分利用硬件的資源,達(dá)到高效的處理能力。
當(dāng)然多任務(wù)框架的采用,不僅僅是多線程的執(zhí)行,需要對多任務(wù)下帶來的問題進(jìn)行處理,如任務(wù)執(zhí)行返回值獲取,任務(wù)間數(shù)據(jù)的傳遞,任務(wù)執(zhí)行次序的協(xié)調(diào);當(dāng)然也不是任務(wù)越多處理越快,要避免線程過多導(dǎo)致操作系統(tǒng)夯住,也要防止任務(wù)空轉(zhuǎn)過快導(dǎo)致CPU使用率飆高。
本專欄主要介紹使用多線程與多進(jìn)程模型,如何搭建多任務(wù)的應(yīng)用框架,同時對多任務(wù)下的數(shù)據(jù)通信,數(shù)據(jù)同步,任務(wù)控制,以及CPU core與任務(wù)綁定等相關(guān)知識的分享,讓大家在實際開發(fā)中輕松構(gòu)建自已的多任務(wù)程序。
概述
linux 系統(tǒng)中 線程是一種經(jīng)量級的任務(wù),同一進(jìn)程的多個線程是共享進(jìn)程內(nèi)存的;當(dāng)我們定義一個全局變量時,它可以被當(dāng)前進(jìn)程下的所有線程訪問,如何來定義一個線程本地的變量呢?
TLS方式
在linux 系統(tǒng)下一般有兩種方式來定義線程本地變量,這一技術(shù)叫做Thread Local Storage, TLS。
- GCC的__thread關(guān)鍵字
- 鍵值對API
TLS生命周期
線程本地變量的生命周期與線程的生命周期一樣,當(dāng)線程結(jié)束時,線程本地變量的內(nèi)存就會被回收。
當(dāng)然這里需要特別注意,當(dāng)線程本地變量為指針類型時,動態(tài)分配的內(nèi)存空間,系統(tǒng)并不會自動回收,只是將指針變量置為NULL,為了避免內(nèi)存泄漏,需要在線程退出時主動進(jìn)行清理動作,這將在后面的博文中介紹。
線程pthread結(jié)構(gòu)內(nèi)存
在介紹線程本地變量存儲時,就不得不介紹一下pthread結(jié)構(gòu)的內(nèi)存,它定義了線程的重要數(shù)據(jù)結(jié)構(gòu),描述了用戶狀態(tài)線程的完整信息。
pthread 結(jié)構(gòu)非常復(fù)雜,通過 specific_1stblock 數(shù)組和特定的輔助數(shù)組與 TLS 相關(guān)。
#define PTHREAD_KEY_2NDLEVEL_SIZE 32
#define PTHREAD_KEY_1STLEVEL_SIZE \
((PTHREAD_KEYS_MAX + PTHREAD_KEY_2NDLEVEL_SIZE - 1) \
/ PTHREAD_KEY_2NDLEVEL_SIZE)
struct pthread
{
union
{
#if !TLS_DTV_AT_TP
/* This overlaps the TCB as used for TLS without threads (see tls.h). */
tcbhead_t header;
#else
struct
{
int multiple_threads;
int gscope_flag;
} header;
#endif
void *__padding[24];
};
list_t list;
pid_t tid;
...
struct pthread_key_data
{
/* Sequence number. We use uintptr_t to not require padding on
32- and 64-bit machines. On 64-bit machines it helps to avoid
wrapping, too. */
uintptr_t seq;
/* Data pointer. */
void *data;
} specific_1stblock[PTHREAD_KEY_2NDLEVEL_SIZE];
/* Two-level array for the thread-specific data. */
struct pthread_key_data *specific[PTHREAD_KEY_1STLEVEL_SIZE];
/* Flag which is set when specific data is set. */
bool specific_used;
...
}
__thread 關(guān)鍵字
該關(guān)鍵字可用于在 GCC/Clang 編譯環(huán)境中聲明 TLS 變量, 該關(guān)鍵字不是 C 標(biāo)準(zhǔn),并且因編譯器不同而有差異;
原理介紹
使用 __thread關(guān)鍵字聲明的變量存儲在線程的pthred 結(jié)構(gòu)與堆??臻g之間,也就是說,在內(nèi)存布局方面,從高地址到底層地址的內(nèi)存分布為:pthred結(jié)構(gòu)、可變區(qū)和堆棧區(qū)(堆棧的底部和可變區(qū)的頂部是連續(xù)的);
在這種方式下的線程本地變量,變量的類型不能是復(fù)雜的類型,如C++的class類型,而且動態(tài)申請的變量空間,需要主動釋放,線程結(jié)束時,只是對變量空間回收,而對應(yīng)的動態(tài)內(nèi)存則會泄漏。
代碼舉例
/*
* created by senllang 2024/1/1
* mail : study@senllang.onaliyun.com
* Copyright (C) 2023-2024, senllang
*/
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define THREAD_NAME_LEN 32
__thread char threadName[THREAD_NAME_LEN];
__thread int delay = 0;
typedef struct ThreadData
{
char name[THREAD_NAME_LEN];
int delay;
}ThreadData;
void *threadEntry(void *arg)
{
int ret = 0;
int i = 0;
ThreadData * data = (ThreadData *)arg;
printf("[%lu] thread entered \n", pthread_self());
strncpy(threadName, data->name, THREAD_NAME_LEN);
delay = data->delay;
for(i = 0; i < delay; i++)
{
usleep(10);
}
printf("[%lu] %s exiting after delay %d.\n", pthread_self(), threadName, delay);
pthread_exit(&ret);
}
int main(int argc, char *argv[])
{
pthread_t thid1,thid2,thid3;
void *ret;
ThreadData args1 = {"thread 1", 50000}, args2 = {"thread 2", 25000}, args3 = {"thread 3", 12500};
strncpy(threadName, "Main Thread", THREAD_NAME_LEN);
if (pthread_create(&thid1, NULL, threadEntry, &args1) != 0)
{
perror("pthread_create() error");
exit(1);
}
if (pthread_create(&thid2, NULL, threadEntry, &args2) != 0)
{
perror("pthread_create() error");
exit(1);
}
if (pthread_create(&thid3, NULL, threadEntry, &args3) != 0)
{
perror("pthread_create() error");
exit(1);
}
if (pthread_join(thid1, &ret) != 0)
{
perror("pthread_create() error");
exit(3);
}
if (pthread_join(thid2, &ret) != 0)
{
perror("pthread_create() error");
exit(3);
}
if (pthread_join(thid3, &ret) != 0)
{
perror("pthread_create() error");
exit(3);
}
printf("[%s]all thread exited delay:%d .\n", threadName, delay);
}
每個線程定義了兩個線程本地變量 threadName, delay,在線程處理函數(shù)中,對它們賦值后,再延遲一段時間,然后輸出這兩個變量值,結(jié)果可以看到每個線程的本地變量值都不一樣,可以獨立使用。
運行結(jié)果:
[senllang@hatch example_04]$ gcc -lpthread threadLocalStorage_gcc.c
[senllang@hatch example_04]$ ./a.out
[139945977145088] thread entered
[139945960359680] thread entered
[139945968752384] thread entered
[139945960359680] thread 3 exiting after delay 12500.
[139945968752384] thread 2 exiting after delay 25000.
[139945977145088] thread 1 exiting after delay 50000.
[Main Thread]all thread exited delay:0 .
線程API方式
另一種使用線程本地變量的方式,是使用線程key相關(guān)的API,它分為兩類,一是創(chuàng)建和銷毀接口,另一類是變量的設(shè)置與獲取接口。
這種方式下,線程的本地數(shù)據(jù)存儲在 pthread結(jié)構(gòu)中,其中specific_1stblock,specific兩個數(shù)組按key值索引,并存儲對應(yīng)的線程本地數(shù)據(jù);
線程本地數(shù)據(jù)的數(shù)量,在這種方式下是有限的。
創(chuàng)建與銷毀接口
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void(*destructor)(void*));
int pthread_key_delete(pthread_key_t key);
創(chuàng)建接口,獲取一個 pthread_key_t變量的值,其實就是內(nèi)存獲取一個鍵值來存儲數(shù)據(jù),第二個參數(shù)destructor傳遞一個銷毀數(shù)據(jù)的方法,當(dāng)本地數(shù)據(jù)為復(fù)雜數(shù)據(jù)類型,或者動態(tài)申請內(nèi)存時,在線程退出時進(jìn)行清理調(diào)用。
在線程使用完后,需要釋放對應(yīng)的key。
設(shè)置本地變量值接口
#include <pthread.h>
int pthread_setspecific(pthread_key_t key, const void * value);
void * pthread_getspecific(pthread_key_t key);
這里設(shè)置線程的本地變量值,和獲取線程本地變量值;
在不同線程中設(shè)置時,就會只設(shè)置當(dāng)前線程的本地變量,不影響其它線程。
代碼示例
/*
* created by senllang 2024/1/1
* mail : study@senllang.onaliyun.com
* Copyright (C) 2023-2024, senllang
*/
#include <stdio.h>
#include <pthread.h>
// 定義一個 TLS 鍵
pthread_key_t tls_key;
void ShowThreadLocalData(char *prompt, pthread_t thid)
{
// 獲取 TLS 存儲的值
int *value = (int *) pthread_getspecific(tls_key);
if (value == NULL)
{
printf("[%s]Thread: %ld, Value: NULL\n", prompt, thid);
} else
{
printf("[%s]Thread: %ld, Value: %d\n", prompt, thid, *value);
}
}
// 線程函數(shù)
void *thread_func(void *arg)
{
ShowThreadLocalData("pre", pthread_self());
pthread_setspecific(tls_key, (void *) arg);
ShowThreadLocalData("after", pthread_self());
return NULL;
}
int main()
{
// 創(chuàng)建 2 個線程
pthread_t thread1, thread2;
int args1 = 100, args2=200;
pthread_key_create(&tls_key, NULL);
// 設(shè)置 TLS 值
pthread_setspecific(tls_key, (void *) 500);
pthread_create(&thread1, NULL, thread_func, &args1);
pthread_create(&thread2, NULL, thread_func, &args2);
// 等待線程結(jié)束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_key_delete(tls_key);
return 0;
}
在主線程和兩個子線程中都設(shè)置了本地變量值,運行后,可以看到每個線程中的值都不一樣。
[senllang@hatch example_04]$ gcc -lpthread threadLocalStorage_key.c
[senllang@hatch example_04]$ ./a.out
[pre]Thread: 140252914022144, Value: NULL
[after]Thread: 140252914022144, Value: 100
[pre]Thread: 140252905629440, Value: NULL
[after]Thread: 140252905629440, Value: 200
在線程開始時,獲取本地變量值,都沒有獲取到主線程設(shè)置的值。
兩種方式比較
-
不同的存儲區(qū)域/尋址方法
API 方式定義的數(shù)據(jù)由 specific_1stblock 數(shù)組和結(jié)構(gòu)的特定輔助數(shù)組尋址,而__thread存儲類型變量由??臻g地址偏移量尋址。 -
性能/效率差異
由于__thread由棧地址偏移量解決,因此性能高于 API方式。 -
可以存儲不同的數(shù)據(jù)
__thread只能修改常規(guī)的POD類型變量,對于指針類型數(shù)據(jù),動態(tài)申請的內(nèi)存,需要主動銷毀;而 API方式 支持傳入銷毀方法并支持所有數(shù)據(jù)類型。 -
支持的數(shù)據(jù)數(shù)量不同
理論上,只要堆棧不滿,__thread類型的變量就可以無限期定義;而API 方式只能創(chuàng)建PTHREAD_KEYS_MAX個鍵,但可以使用一個鍵通過結(jié)構(gòu)體等方式存儲多個值。
總結(jié)
本文所涉及的代碼已經(jīng)上傳到工程hatchCode, 在multipleThreads/example_04目錄下;
線程本地變量的使用,使得線程并發(fā)時,與進(jìn)程并發(fā)更加相似,都有自己的私有全局?jǐn)?shù)據(jù),當(dāng)然線程的特別之處在于,線程的本地變量的空間取決于線程棧的大小,當(dāng)然也可以是結(jié)構(gòu)指針,再動態(tài)申請空間,那么空間也就不存在問題了。
結(jié)尾
非常感謝大家的支持,在瀏覽的同時別忘了留下您寶貴的評論,如果覺得值得鼓勵,請點贊,收藏,我會更加努力!文章來源:http://www.zghlxwxcb.cn/news/detail-778461.html
作者郵箱:study@senllang.onaliyun.com
如有錯誤或者疏漏歡迎指出,互相學(xué)習(xí)。文章來源地址http://www.zghlxwxcb.cn/news/detail-778461.html
到了這里,關(guān)于【linux 多線程并發(fā)】線程本地數(shù)據(jù)存儲的兩種方式,每個線程可以有同名全局私有數(shù)據(jù),以及兩種方式的性能分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!