目次
__thread と pthread 固有の API の比較
C/C++ プログラムでは、グローバル変数はデフォルトですべてのスレッドで共有されるため、開発者はマルチスレッドの競合の問題に対処する必要があります。場合によっては、1 つのスレッドがデータの排他的コピーを保持し、他のスレッドがそれにアクセスできないようにする必要があります。典型的なものは errno グローバル変数です。これは常に現在のスレッドの最後の呼び出しのエラー コードを保存し、スレッドの競合は発生しません。現時点では、これを解決するにはスレッド ローカル ストレージ (TLS) を使用する必要があります。
pthreadのメモリ構造
TLS を説明する前に、まず pthread のメモリ構造を理解します。struct pthread
glibc/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_struct
seq と同じであり、対応するキーが作成されるかどうかを識別します。
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 と矛盾しているのではないでしょうか?
__thread
pthread 固有の 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 つのキーに複数の値を格納できます。