Glibc 線程資源---__thread & pthread_key_t


前言

前面寫了一篇文章《Glibc 線程資源分配與釋放-----線程棧》适室,其中主要講解了 glibc 在 x86_64 平臺(tái) Linux 系統(tǒng)上的線程棧區(qū)管理捣辆。但是這并不是全部的線程資源,本文中我們將介紹另外兩類資源的旧巾,以 __thread 定義的變量以及 pthread_key_create 創(chuàng)建的鍵值對(duì)資源整袁。我們?nèi)匀灰?x86_64 Linux 平臺(tái)為例坐昙,分析的源碼是 glibc-2.25炸客。

__thread 變量

__thread 標(biāo)識(shí)符修飾的全局或靜態(tài)變量是線程獨(dú)立的戈钢,線程對(duì)該變量的操作對(duì)其它線程來說是不可見的殉了。然而線程之間共享內(nèi)存空間的薪铜,因此要達(dá)到如些效果就需要針對(duì)該變量為每個(gè)線程分配變量的存儲(chǔ)位置。在 Glibc 中谓娃, 所有的 __thread 變量是與 pthread 關(guān)聯(lián)存儲(chǔ)的蜒滩,通過相對(duì)于 pthread 變量地址的偏移實(shí)現(xiàn)對(duì)變量的尋址滨达。即是說,pthread 變量的地址是基址俯艰。

《Glibc 線程資源分配與釋放-----線程椉癖椋》中提到,pthread 是存儲(chǔ)在線程棧內(nèi)存塊中的竹握,在線程棧布局圖中画株,我們省略了為 __thread 變量預(yù)留的內(nèi)存空間。下圖說明了__thread 變量在線程棧內(nèi)存空間中的存儲(chǔ)。

線程棧布局

下面這段代碼可以驗(yàn)證這個(gè)結(jié)論污秆。

#include <pthread.h>
#include <stdio.h>
#include <asm/prctl.h>
#include <sys/prctl.h>
#include <errno.h>

static int __thread var_1 = 0;
static short  __thread var_2 = 0;

void* func(void *f) {
    
    long pthread_addr = 0;
    arch_prctl(ARCH_GET_FS, &pthread_addr);
    printf("&pthread = %p, &var_1 = %p, off_var_1 = %d,  &var_2 = %p, off_var_2 = %d\n",
            pthread_addr, &var_1, (long)&var_1 - pthread_addr, 
            &var_2, (long)&var_2 - pthread_addr );
    return NULL;
}

int main() {
    pthread_t chs[3];
    int i = 0;

    for (i = 0; i < 3; ++i) {
        pthread_create(&chs[i], NULL, func, NULL);
    }
    
    for (i = 0; i < 3; ++i) {
        pthread_join(chs[i], NULL);
    }
    return 0;
}

上面這段代碼中,比較難以理解的在于為什么 arch_prctl 函數(shù)取得的是 pthread 的地址良拼。在解答這個(gè)問題之間战得,我們需要先看一下創(chuàng)建線程的 clone 函數(shù) 以及調(diào)用時(shí)傳入的 flags 參數(shù) CLONE_SETTLS。

int clone(int (*fn)(void *), void *child_stack,
                 int flags, void *arg, ...
                 /* pid_t *ptid, void *newtls, pid_t *ctid */ );

CLONE_SETTLS
(since Linux 2.5.32) The TLS (Thread Local Storage) descriptor is set to newtls. The interpretation of newtls and the resulting effect is architecture dependent. On x86, newtls is interpreted as a **struct user_desc ** (See set_thread_area(2)). On x86_64 it is the new value to be set for the %fs base register (See the ARCH_SET_FS argument to arch_prctl(2)). On architectures with a dedicated TLS register, it is the new value of that register.

這段描述說明庸推, 在 x86_64 位系統(tǒng)上常侦,clone 函數(shù)傳入的 newtls 參數(shù)會(huì)作為 fs 寄存器的基地址,并且該地址值能通過 arch_prctl 函數(shù)獲得贬媒。在《Glibc 線程資源分配與釋放-----線程椓觯》中我們也看到了,newtls 傳入的實(shí)參就是 pthread 變量的地址 (pd)际乘。

if (__glibc_unlikely (ARCH_CLONE (&start_thread, STACK_VARIABLES_ARGS,
                    clone_flags, pd, &pd->tid, tp, &pd->tid)
            == -1))

因此坡倔,可以說明 arch_prctl 函數(shù)獲得的就是線程 pthread 變量的地址值。執(zhí)行一次上面程序的結(jié)果(每次執(zhí)行結(jié)果可能不同):

&pthread = 0x7f8325948700, &var_1 = 0x7f83259486f8, off_var_1 = -8,  &var_2 = 0x7f83259486fc, off_var_2 = -4
&pthread = 0x7f8324f47700, &var_1 = 0x7f8324f476f8, off_var_1 = -8,  &var_2 = 0x7f8324f476fc, off_var_2 = -4
&pthread = 0x7f8324546700, &var_1 = 0x7f83245466f8, off_var_1 = -8,  &var_2 = 0x7f83245466fc, off_var_2 = -4

可以看出脖含, var_1 與 var_2 確實(shí)存入在線程 pthread 地址的下端罪塔,不同線程訪問的變量的地址是不相同的,但是變量相對(duì)于 pthread 地址的偏移是相同的养葵,在本例中分別是 -8 與 -4征堪。

鍵值對(duì)資源

另外一種創(chuàng)建線程特定數(shù)據(jù)(Tthread-specific data)的方式是通過 pthread_key_create 創(chuàng)建鍵值映射。每個(gè)線程通過鍵訪問線程特定的數(shù)據(jù)关拒。glibc 中鍵集中分配管理佃蚜,值分開存儲(chǔ)的方式提供 TSD 數(shù)據(jù)。

鍵的分配

pthread_key_create 創(chuàng)建的鍵事實(shí)上是一個(gè)無符號(hào)的整型數(shù)(sysdeps/x86/bits/pthreadtypes.h):

/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;

glibc 定義了一個(gè)全局?jǐn)?shù)組用于管理鍵是否已被創(chuàng)建着绊,這個(gè)全局?jǐn)?shù)組定義在 nptl/vars.c 中(如下)谐算。每個(gè)鍵都會(huì)對(duì)應(yīng)于數(shù)組中一個(gè) pthread_struct_t 結(jié)構(gòu)體,該結(jié)構(gòu)體描述了鍵是否已正被使用畔柔。由數(shù)組定義可以看出氯夷, 一個(gè)進(jìn)程中最多個(gè)通過 pthread_key_create 創(chuàng)建 PTHREAD_KEYS_MAX (1024)個(gè)鍵。

/* Table of the key information.  */
struct pthread_key_struct __pthread_keys[PTHREAD_KEYS_MAX]

如下 (sysdeps/nptl/internaltypes.h)靶擦, pthread_key_struct 中定義一個(gè)序號(hào)值(seq)及一個(gè)用于釋放數(shù)據(jù)的“析構(gòu)函數(shù)” (destr)腮考。

/* Thread-local data handling.  */
struct pthread_key_struct
{
  /* Sequence numbers.  Even numbers indicated vacant entries.  Note
     that zero is even.  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;

  /* Destructor for the data.  */
  void (*destr) (void *);
};

seq 用于判斷對(duì)應(yīng)的鍵是否被創(chuàng)建,若 seq 是奇數(shù)則正被使用玄捕,若為偶數(shù)則未被使用踩蔚。 例如,若 __pthread_keys[3].seq &1 == 0 為 True 則說明該鍵 3 沒有被創(chuàng)建枚粘,否則已被創(chuàng)建馅闽。 destr 允許應(yīng)用創(chuàng)建鍵時(shí)定義一個(gè)釋放資源的函數(shù)。

鍵值的映射

鍵值的映射信息是存儲(chǔ)在各線程的 pthread 結(jié)構(gòu)體中的。最直接的方法是在每個(gè) pthread 結(jié)構(gòu)體中也定義一個(gè)類似于 __pthread_keys 的數(shù)組福也, 該數(shù)組中存儲(chǔ) key-value 的映射關(guān)系局骤。不過為了節(jié)約內(nèi)存空間(大部分情況下應(yīng)用只會(huì)使用很少的 key), pthread 并不是直接創(chuàng)建一個(gè)長(zhǎng)度為 1024 的數(shù)組暴凑,而是使用了兩級(jí)數(shù)組的方式來存儲(chǔ)這種映射關(guān)系峦甩。先來看一下 pthread 結(jié)構(gòu)體中存儲(chǔ)映射關(guān)系的變量:

  /* We allocate one block of references here.  This should be enough
     to avoid allocating any memory dynamically for most applications.  */
  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];

在 pthread 中定義了一個(gè)結(jié)構(gòu)體 pthread_key_data 存儲(chǔ)指向數(shù)據(jù)的指針(data)。同樣的现喳,其中 seq 標(biāo)識(shí)對(duì)應(yīng)的鍵是否被創(chuàng)建凯傲。pthread 中定義了一個(gè) pthread_key_data 的數(shù)組 specific_1stblock 以及指針數(shù)組 specific。 當(dāng)鍵較少時(shí)嗦篱,映射關(guān)系直接存儲(chǔ)到 sepcific_1stblock 中冰单,隨著鍵的增加,再分配空間存儲(chǔ)到 specific 中灸促。為了說明這個(gè)過程诫欠,我們來看一下 pthread_setspecific 函數(shù)(nptl/pthread_setspecific.c):

int
__pthread_setspecific (pthread_key_t key, const void *value)
{
  struct pthread *self;
  unsigned int idx1st;
  unsigned int idx2nd;
  struct pthread_key_data *level2;
  unsigned int seq;

  self = THREAD_SELF;

  /* Special case access to the first 2nd-level block.  This is the
     usual case.  */
  if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
    {
      /* Verify the key is sane.  */
      if (KEY_UNUSED ((seq = __pthread_keys[key].seq)))
    /* Not valid.  */
    return EINVAL;

      level2 = &self->specific_1stblock[key];

      /* Remember that we stored at least one set of data.  */
      if (value != NULL)
    THREAD_SETMEM (self, specific_used, true);
    }
  else
    {
      if (key >= PTHREAD_KEYS_MAX
      || KEY_UNUSED ((seq = __pthread_keys[key].seq)))
    /* Not valid.  */
    return EINVAL;

      idx1st = key / PTHREAD_KEY_2NDLEVEL_SIZE;
      idx2nd = key % PTHREAD_KEY_2NDLEVEL_SIZE;

      /* This is the second level array.  Allocate it if necessary.  */
      level2 = THREAD_GETMEM_NC (self, specific, idx1st);
      if (level2 == NULL)
    {
      if (value == NULL)
        /* We don't have to do anything.  The value would in any case
           be NULL.  We can save the memory allocation.  */
        return 0;

      level2
        = (struct pthread_key_data *) calloc (PTHREAD_KEY_2NDLEVEL_SIZE,
                          sizeof (*level2));
      if (level2 == NULL)
        return ENOMEM;

      THREAD_SETMEM_NC (self, specific, idx1st, level2);
    }

      /* Pointer to the right array element.  */
      level2 = &level2[idx2nd];

      /* Remember that we stored at least one set of data.  */
      THREAD_SETMEM (self, specific_used, true);
    }

  /* Store the data and the sequence number so that we can recognize
     stale data.  */
  level2->seq = seq;
  level2->data = (void *) value;

  return 0;
}

從函數(shù)中可以看出,如果 key 小于第 specific_1stblock 數(shù)組大型仍住(PTHREA_KEY_2NDLEVEL_SIZE)呕诉,則直接將 value 的地址直接存儲(chǔ)于 specific_1stblock[key] 處缘厢;如果 key 大于或等 PTHREA_KEY_2NDLEVEL_SIZE吃度, 則會(huì)為指針數(shù)組 sepcific 分配內(nèi)存空間(如果未曾分配),并將 value 的地址位于 specific[idx1st][idx2nd] 處贴硫。其中 idx1st椿每、idx2nd 分別是 key 除 PTHREA_KEY_2NDLEVEL_SIZE 的商與余數(shù)。由于大部分應(yīng)用使用的 key 的數(shù)量很小英遭,所以 specific 數(shù)組大部分指針都為 NULL间护。

可以看出,通過 key 訪問線程特定數(shù)據(jù)的步驟比 __thread 變量更為復(fù)雜一些挖诸。在 x86_64 架構(gòu)上汁尺, fs 寄存器已經(jīng)存儲(chǔ)了線程 pthread 的地址值,因此訪問 __thread 變量直接通過 fs 相對(duì)尋址即可多律,只需要一條指令痴突。而 pthread_getspecific 訪問線程特定數(shù)據(jù)時(shí),需要通過 specific_1stblock 數(shù)組來完成狼荞,其中還包括了諸多的有效性檢驗(yàn)辽装。效率上 __thread 變量的訪問應(yīng)該會(huì)更高一些。但是兩者的差距有多大相味,需要真實(shí)實(shí)驗(yàn)去測(cè)試一下拾积。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拓巧,更是在濱河造成了極大的恐慌斯碌,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肛度,死亡現(xiàn)場(chǎng)離奇詭異输拇,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)贤斜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門策吠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人瘩绒,你說我怎么就攤上這事猴抹。” “怎么了锁荔?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵蟀给,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我阳堕,道長(zhǎng)跋理,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任恬总,我火速辦了婚禮前普,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘壹堰。我一直安慰自己拭卿,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布贱纠。 她就那樣靜靜地躺著峻厚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谆焊。 梳的紋絲不亂的頭發(fā)上惠桃,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音辖试,去河邊找鬼辜王。 笑死,一個(gè)胖子當(dāng)著我的面吹牛剃执,可吹牛的內(nèi)容都是我干的誓禁。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼肾档,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼摹恰!你這毒婦竟也來了辫继?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤俗慈,失蹤者是張志新(化名)和其女友劉穎姑宽,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體闺阱,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡炮车,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了酣溃。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瘦穆。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赊豌,靈堂內(nèi)的尸體忽然破棺而出扛或,到底是詐尸還是另有隱情,我是刑警寧澤碘饼,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布熙兔,位于F島的核電站,受9級(jí)特大地震影響艾恼,放射性物質(zhì)發(fā)生泄漏住涉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一钠绍、第九天 我趴在偏房一處隱蔽的房頂上張望舆声。 院中可真熱鬧,春花似錦五慈、人聲如沸纳寂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忽媒,卻和暖如春争拐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背晦雨。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工架曹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人闹瞧。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓绑雄,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親奥邮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子万牺,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 轉(zhuǎn)自:Youtherhttps://www.cnblogs.com/youtherhome/archive/201...
    njukay閱讀 1,607評(píng)論 0 52
  • 簡(jiǎn)介 線程創(chuàng)建 線程屬性設(shè)置 線程參數(shù)傳遞 線程優(yōu)先級(jí) 線程的數(shù)據(jù)處理 線程的分離狀態(tài) 互斥鎖 信號(hào)量 一 線程創(chuàng)...
    第八區(qū)閱讀 8,553評(píng)論 1 6
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理罗珍,服務(wù)發(fā)現(xiàn),斷路器脚粟,智...
    卡卡羅2017閱讀 134,626評(píng)論 18 139
  • 人一閑下來就會(huì)胡思亂想覆旱,就像我現(xiàn)在『宋蓿總是幻想種種方式再次和你相遇扣唱,可這么久過去了,我發(fā)現(xiàn)真的是幻想团南。你不知道我有多...
    堇小琦閱讀 252評(píng)論 0 1
  • 黎明 前提有很多 結(jié)果就一個(gè) 天就快要亮了 據(jù)說一個(gè)懶漢撿了一條死魚 翻在鍋底 風(fēng) 給你一些氣味 嗅出了花開花落 ...
    兔子的蹩腳閱讀 208評(píng)論 0 0