コードで遊んでみる | Linux のスレッド ローカル ストレージ (スレッド ローカル ストレージ)

目次

pthreadのメモリ構造

__糸

pthread 固有の API

__thread と pthread 固有の API の比較

さまざまなストレージ領域/アドレス指定方法

異なるパフォーマンス/効率

保存できるデータが異なります

対応データ数が異なります


 

C/C++ プログラムでは、グローバル変数はデフォルトですべてのスレッドで共有されるため、開発者はマルチスレッドの競合の問題に対処する必要があります。場合によっては、1 つのスレッドがデータの排他的コピーを保持し、他のスレッドがそれにアクセスできないようにする必要があります。典型的なものは errno グローバル変数です。これは常に現在のスレッドの最後の呼び出しのエラー コードを保存し、スレッドの競合は発生しません。現時点では、これを解決するにはスレッド ローカル ストレージ (TLS) を使用する必要があります。

 

pthreadのメモリ構造

TLS を説明する前に、まず pthread のメモリ構造を理解します。struct pthreadglibc/nptl/descr.hは、ユーザー モード スレッドの完全な情報を記述するスレッドの重要なデータ構造を定義します。pthread スレッドが作成されるたびに、対応する pthread 構造がメモリ内に存在します。pthread の構造は非常に複雑で、TLS に関係するものとしては、後述する specific_1stblock 配列と specific Secondary 配列があります。

#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

GCC/Clang コンパイル環境では、__threadキーワードを使用して TLS 変数を宣言できます。__threadキーワードは C 標準ではなく、コンパイラごとに名前が異なります。

Xcode 13.2 でのみテストされましたが、i386 アーキテクチャはサポートされていません__thread

#if defined(__i386__)
static char *g_thread_data = NULL;
#else
static __thread char *g_thread_data = NULL;
#endif

キーワードで宣言された変数は__thread、pthred 構造体の後のスタック空間間のメモリ領域に格納されます。つまり、メモリレイアウトの観点から見ると、上位アドレスと下位アドレスのメモリ配置は、pthred構造体、__thread変数領域、スタック領域(スタックの下位と__thread変数領域の先頭がつながっている)となります。

Xcode 13.2/arm64 で実行されるプログラムでこれを説明してみましょう。

__thread uint64_t g_tls_int = 6;
__thread char *g_tls_string = "kanchuan.com";;

void tls_test(void)
{
    uint64_t value = g_tls_int;
    printf("%llu", value);
    char *string = g_tls_string;
    printf("%s", string);
}

tls_test の入り口にあるブレークポイントで、次のように対応するアセンブラを確認します。

   0x104235240 <+0>:   sub    sp, sp, #0x40             ; =0x40 
    0x104235244 <+4>:   stp    x29, x30, [sp, #0x30]
    0x104235248 <+8>:   add    x29, sp, #0x30            ; =0x30 
    0x10423524c <+12>:  adrp   x0, 529
    0x104235250 <+16>:  add    x0, x0, #0xd70            ; =0xd70 
    0x104235254 <+20>:  ldr    x8, [x0]
    0x104235258 <+24>:  blr    x8
    0x10423525c <+28>:  str    x0, [sp, #0x10]
    0x104235260 <+32>:  adrp   x0, 529
    0x104235264 <+36>:  add    x0, x0, #0xd88            ; =0xd88 
    0x104235268 <+40>:  ldr    x8, [x0]
    0x10423526c <+44>:  blr    x8
    0x104235270 <+48>:  mov    x8, x0
    0x104235274 <+52>:  ldr    x0, [sp, #0x10]
    0x104235278 <+56>:  str    x8, [sp, #0x18]
    0x10423527c <+60>:  ldr    x8, [x0]
    0x104235280 <+64>:  stur   x8, [x29, #-0x8]
    0x104235284 <+68>:  ldur   x8, [x29, #-0x8]
    0x104235288 <+72>:  adrp   x0, 471
    0x10423528c <+76>:  add    x0, x0, #0x7fc            ; =0x7fc 
    0x104235290 <+80>:  mov    x9, sp
    0x104235294 <+84>:  str    x8, [x9]
    0x104235298 <+88>:  bl     0x104403be0               ; symbol stub for: printf
    0x10423529c <+92>:  ldr    x0, [sp, #0x18]
    0x1042352a0 <+96>:  ldr    x8, [x0]
    0x1042352a4 <+100>: stur   x8, [x29, #-0x10]
    0x1042352a8 <+104>: ldur   x8, [x29, #-0x10]
    0x1042352ac <+108>: adrp   x0, 471
    0x1042352b0 <+112>: add    x0, x0, #0x801            ; =0x801 
    0x1042352b4 <+116>: mov    x9, sp
    0x1042352b8 <+120>: str    x8, [x9]
    0x1042352bc <+124>: bl     0x104403be0               ; symbol stub for: printf
    0x1042352c0 <+128>: ldp    x29, x30, [sp, #0x30]
    0x1042352c4 <+132>: add    sp, sp, #0x40             ; =0x40 
    0x1042352c8 <+136>: ret 

0x104235274 で、sp レジスタ オフセット 0x10 バイトが x0 に読み取られます。x0 レジスタ (g_tls_int) の値 0x104235278 を読み取ります。

(lldb) register read x0
      x0 = 0x0000000281cf41a0
(lldb) memory read/1xg 0x0000000281cf41a0
0x281cf41a0: 0x0000000000000006

0x10423529c で、sp レジスタ オフセット 0x18 バイトが x0 に読み取られます。0x1042352a0 にある x0 レジスタ (g_tls_string) の値を読み取ります。

(lldb) register read x0
      x0 = 0x0000000281cf41a8
(lldb) memory read/1xg 0x0000000281cf41a8
0x281cf41a8: 0x000000010440c7f0
(lldb) memory read 0x000000010440c7f0
0x10440c7f0: 65 61 73 65 61 70 69 2e 63 6f 6d 00 25 6c 6c 75  kanchuan.com.%llu
0x10440c800: 00 25 73 00 4d 79 41 70 70 6c 69 63 61 74 69 6f  .%s.MyApplicatio

__thread上記のテスト結果から、変数の読み取りは fp ポインタをオフセットする (上位アドレスにオフセットする) ことによって行われることがわかります。

__thread変更された変数は POD (Plain Old Data) タイプである必要があり、クラスなどの高度な言語機能はサポートされていません。__thread変数はスレッドの存続期間中存在し、スレッドが破棄されると解放されます。__thread破棄方法を指定できないため、__thread変更されたポインタ変数を定義してスレッドの実行中にメモリを malloc する場合、__thread変数ポインタはスレッドの終了時にのみ NULL に設定されるため、開発者は手動でメモリを解放する必要があることに注意してください

__thread char *g_tls_string = NULL;

void tls_test(void)
{
    if (g_tls_string == NULL) g_tls_string = calloc(1024, 1);
    //线程销毁时,需要手动释放malloc的内存
}

スレッドの終了時に malloc メモリの解放を自動的に完了したい場合は、pthread 固有の関連 API を使用する必要があります。

pthread 固有の API

pthread は、TLS 機能を実装するための次の API も提供します。

//nptl/bits/pthreadtypes.h
/* Keys for thread-specific data */
typedef unsigned int pthread_key_t;

int pthread_key_create(pthread_key_t *, void (* _Nullable)(void *));
int pthread_key_delete(pthread_key_t);

int pthread_setspecific(pthread_key_t , const void * _Nullable);
void* _Nullable pthread_getspecific(pthread_key_t);

pthread_key_create の最初のパラメータは pthread_key_t ポインタで、作成が成功したときに返される pthread_key_t を受け取るために使用されます。2 番目のパラメータはデータ デストラクタ ポインタで、スレッドが破棄されたときに実行されます。pthread_key_create が成功すると、pthread_key_t が取得され、pthread_key_t を通じてスレッドのプライベート データを読み書きできるようになります。サンプルコードは次のとおりです。

//create key
pthread_key_t key = 0;
pthread_key_create(&key, NULL); 

//write
struct kanchuan_struct data;
pthread_setspecific(key, &data);

//read
struct kanchuan_struct* = (struct kanchuan_struct *)pthread_getspecific(key)

各プロセスには、pthread_key_t を管理するためのグローバル配列 __pthread_keys があります。

//nptl/internaltypes.h:
/* 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 *);
};


//sysdeps/unix/sysv/linux/bits/local_lim.h
/* This is the value this implementation supports.  */
#define PTHREAD_KEYS_MAX 1024


//nptl/pthread_keys.c
/* Table of the key information.  */
struct pthread_key_struct __pthread_keys[PTHREAD_KEYS_MAX];

struct pthread_key_structこの構造体は、seq と渡されるデストラクターへのポインターを定義します。プログラムは同時に最大 PTHREAD_KEYS_MAX pthread_key_t を作成できます。pthread_key_t はグローバルですが、異なるスレッドが pthread_key_t を介して読み取り/書き込みインターフェイスにアクセスすると、実際には異なるメモリを操作します。

pthread_key_create を実行すると、__pthread_keys 配列から未使用の pthread_key_struct 構造体が見つかり、その seq に 1 が追加されます。返される pthread_key_t は、実際には __pthread_keys 配列内のこの pthread_key_struct のシリアル番号です。次のコード:

//nptl/pthread_key_create.c:
int
___pthread_key_create (pthread_key_t *key, void (*destr) (void *))
{
  /* Find a slot in __pthread_keys which is unused.  */
  for (size_t cnt = 0; cnt < PTHREAD_KEYS_MAX; ++cnt)
    {
      uintptr_t seq = __pthread_keys[cnt].seq;

      if (KEY_UNUSED (seq) && KEY_USABLE (seq)
   /* We found an unused slot.  Try to allocate it.  */
   && ! atomic_compare_and_exchange_bool_acq (&__pthread_keys[cnt].seq,
           seq + 1, seq))
 {
   /* Remember the destructor.  */
   __pthread_keys[cnt].destr = destr;

   /* Return the key to the caller.  */
   *key = cnt;

   /* The call succeeded.  */
   return 0;
 }
    }

  return EAGAIN;
}

pthread_key_delete を実行すると、pthread_key_t のシリアル番号に従って __pthread_keys から対応する pthread_key_struct が検索され、その seq に 1 が加算されます。次のコード:

//nptl/pthread_key_delete.c
int
___pthread_key_delete (pthread_key_t key)
{
  int result = EINVAL;

  if (__glibc_likely (key < PTHREAD_KEYS_MAX))
    {
      unsigned int seq = __pthread_keys[key].seq;

      if (__builtin_expect (! KEY_UNUSED (seq), 1)
   && ! atomic_compare_and_exchange_bool_acq (&__pthread_keys[key].seq,
           seq + 1, seq))
 /* We deleted a valid key.  */
 result = 0;
    }

  return result;
}

ここではアトミックな操作を保証するために が使用されていることに注意してくださいatomic_compare_and_exchange_bool_acq

seq のデフォルトは 0 で、pthread_key_create と pthread_key_delete の両方で seq に 1 が追加されます。seq の値が偶数(0 を含む)の場合は、現在の pthread_key_struct が使用されていないことを意味し、奇数の場合は使用中であることを示します。

pthread_key_create による pthread_key_t の割り当てはグローバルですが、キーと値の関連付けは各スレッドから独立しています。struct pthreadこの構造には次の定義があります

 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];

struct pthread_key_dataこの構造体は、現在のスレッドが TLS データを格納するためのポインター データを定義します。seq はstruct pthread_key_structseq と同じであり、対応するキーが作成されるかどうかを識別します。

specific_1stblock は PTHREAD_KEYS_MAX と同じサイズではなく、PTHREAD_KEY_2NDLEVEL_SIZE (32) に設定されており、メモリ節約の観点から設計する必要があり、多くの場合、TLS 変数は使用しません。

pthread_setspec を実行する際、pthread_key_t の数が PTHREAD_KEY_2NDLEVEL_SIZE 未満の場合は specific_1stblock 配列を直接使用し、pthread_key_t の数が PTHREAD_KEY_2NDLEVEL_SIZE を超える場合はメモリ空間を適用して特定のセカンダリ配列を使用し、その値を specific[idx1st][idx2nd].data に格納します。

//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;
}

上記の分析により、pthread_getspecific を実行するロジックがより明確になります。

//nptl/pthread_getspecific.c

void *
___pthread_getspecific (pthread_key_t key)
{
  struct pthread_key_data *data;

  /* Special case access to the first 2nd-level block.  This is the
     usual case.  */
  if (__glibc_likely (key < PTHREAD_KEY_2NDLEVEL_SIZE))
    data = &THREAD_SELF->specific_1stblock[key];
  else
    {
      /* Verify the key is sane.  */
      if (key >= PTHREAD_KEYS_MAX)
 /* Not valid.  */
 return NULL;

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

      /* If the sequence number doesn't match or the key cannot be defined
  for this thread since the second level array is not allocated
  return NULL, too.  */
      struct pthread_key_data *level2 = THREAD_GETMEM_NC (THREAD_SELF,
         specific, idx1st);
      if (level2 == NULL)
 /* Not allocated, therefore no data.  */
 return NULL;

      /* There is data.  */
      data = &level2[idx2nd];
    }

  void *result = data->data;
  if (result != NULL)
    {
      uintptr_t seq = data->seq;

      if (__glibc_unlikely (seq != __pthread_keys[key].seq))
 result = data->data = NULL;
    }

  return result;
}

glibc の実装によれば、最初に specific_1stblock 配列を使用するために、pthread_key_create の実行時に取得される pthread_key_t は比較的小さい値である必要があります。ただし、筆者が macOS 環境でテストしたところ、取得された pthread_key_t が比較的大きいことがわかりました。macOS の特定の実装が glibc と矛盾しているのではないでしょうか?

__threadpthread 固有の API との比較

  • さまざまなストレージ領域/アドレス指定方法

pthread 固有の API によって定義されたデータは、struct pthread構造体の specific_1stblock 配列および特定の 2 次配列を通じてアドレス指定され、__thread変数は fp レジスター オフセットを通じてアドレス指定されます。

  • 異なるパフォーマンス/効率

fp レジスタ オフセットでアドレス指定されるため__thread、pthread 固有の API よりもパフォーマンスが高くなります。

  • 保存できるデータが異なります

__thread変更できるのは POD タイプの変数のみです。ポインタ タイプのデータの場合は、メモリを適用するときに手動で破棄する必要があります。一方、pthread 固有の API は破棄メソッドの受け渡しをサポートし、すべてのデータ タイプをサポートします。

  • 対応データ数が異なります

理論的には、スタックがいっぱいでない限り、__thread無限に定義できます (疑わしい?); pthread 固有の API は PTHREAD_KEYS_MAX キーしか作成できませんが、構造体を使用して 1 つのキーに複数の値を格納できます。

おすすめ

転載: blog.csdn.net/qq_22903531/article/details/131719140
おすすめ