特定于线程的数据
许多应用程序要求在每个线程的基础上跨函数调用维护某些数据。
例如,为每个文件使用一个线程 的多线程 grep 命令必须拥有特定于线程的文件处理程序和找到的字符串列表。 线程库提供特定于线程 的数据接口以满足这些需要。
特定于线程的数据可以看作是值的两维数组,键作为行索引,线程标识作为列索引。 特定于线程的数据键是一个不透明对 象,是 pthread_key_t 数据类型。 同样的键可以由进程中的所有线程使用。 尽管所有的线程都使用相同的键,但是它们设置并访与该键关联的不同的特定于线程的数据值。 特定于线程的数据是空指针,从而允许对任意类型的数据进行引用,例如动态 分配的字符串或结构。
| 键 | T1 线程 | T2 线程 | T3 线程 | T4 线程 |
|---|---|---|---|---|
| K1 | 6 | 56 | 4 | 1 |
| K2 | 87 | 21 | 0 | 9 |
| K3 | 23 | 12 | 61 | 2 |
| K4 | 11 | 76 | 47 | 88 |
创建和破坏键
在使用特定于线程的数据键之前必须先创建它。 当 相应的线程终止时可以自动删除它们的值。 还可以通过响应回收键的存储空间来删除该键。
创建密钥
通过调用 pthread_key_create 子例程来创建特定于线程的数据密钥。 该子例程返回一个键。 对于所有线程,将特定于线程的数据设为 NULL 值,包括还未创建的线程。
例如,考虑两个线程 A 和 B。 线程 A 按时间顺序执行以下操作:
- 创建特定于线程的数据键 K。
线程 A 和 B 可以使用键 K。 这两个线程的值都是 NULL。
- 创建一个线程 C。
线程 C 还可以使用密钥 K。 线程 C 的值为 NULL。
特定于线程的数据键的数目限制为每个进程 450 个。 该数目可以通 过 PTHREAD_KEYS_MAX 符号常量来检索。
pthread_key_create 子例程只能调用一次。 否则,将创建两个不同的键。 例如,考 虑以下代码片段:
/* a global variable */
static pthread_key_t theKey;
/* thread A */
...
pthread_key_create(&theKey, NULL); /* call 1 */
...
/* thread B */
...
pthread_key_create(&theKey, NULL); /* call 2 */
...在我们的示例中,线程 A 和 B 并行运行,但调用 1 会在调用 2 之前发生。 调用 1 将创建密钥 K1 并将其存储在 theKey 变量中。 调用 2 将创建另一个 键 K2,也将它存储在 theKey 变量中,这样就覆盖了 K1 的值。 结果是,线程 A 将使用 K2,假定它是 K1。 由于 以下原因应当避免这种情况:
- 键 K1 丢失,所以直到进程终止才会回收它的存储空间。 由于键的数目有限,所以您可能不会有足够的键。
- 如果线程 A 在调用 2 之前使用 theKey 变量存储特定于线程的数据,那么该数据将绑定到键 K1。 在调用 2 之后,theKey 变量包含 K2;接着如果 A 试图获取特定于线程的数据,将会始终得到 NULL。
确保按照以下方式唯一地创建这些键:
- 使用一次性初始化工具。
- 在线程使用键之前创建它。 通常这是可能的,例如,使用带有特定于线程的数据的线程池来执行类似的操作。 线程池通常由一个线程,即初始(或另一个“驱动程序”)线程创建。
程序员的责任是确保键创建的唯一性。 线程库没有提供检查键是否被多次创建的方法。
析构函数例程
析构函数例程可能与每个特定于线程的数据键关联。 无论何时终止线程时,如果有非 NULL 的特定于线程的数据使该线程绑定到任何键,那么调用与该键关联的析构函数例程。 这使得在线程终止时自动释放动态分配的特定于线程的数据。 析构函数例程只有一个参数,即特定于线程的数据的值。
pthread_key_create(&key, free);typedef struct {
FILE *stream;
char *buffer;
} data_t;
...
void destructor(void *data)
{
fclose(((data_t *)data)->stream);
free(((data_t *)data)->buffer);
free(data);
*data = NULL;
}析构函数调用最多可重复四次。
键析构
/* bad example - do not write such code! */
pthread_key_t key;
while (pthread_key_create(&key, NULL))
pthread_key_delete(key);使用特定于线程的数据
使用 pthread_getspecific 和 pthread_setspecific 子例程访问特定于线程的数据。 pthread_getspecific 子例程读取绑定到指定键的值并特定于调用线程; pthread_setspecific 子例程设置该值。
设置连续值
private_data = malloc(...);
pthread_setspecific(key, private_data);pthread_setspecific(key, old);
...
pthread_setspecific(key, new);int swap_specific(pthread_key_t key, void **old_pt, void *new)
{
*old_pt = pthread_getspecific(key);
if (*old_pt == NULL)
return -1;
else
return pthread_setspecific(key, new);
}线程库中不存在这样的例程,因为并不是总是需要检索特定于线程的数据的先前值。 只在某些时候才会发生这样情况,例如当特定于线程的数据指向内存池中由初始线程分配的特定位置时。
使用析构函数例程
pthread_key_create(&key, free);
...
...
private_data = malloc(...);
pthread_setspecific(key, private_data);
...
/* bad example! */
...
pthread_getspecific(key, &data);
free(data);
...当线程终止时,将调用析构函数例程以用于特定于线程的数据。 由于该值是一个指向已释放的内存的指针,所以会发生一个错误。 要更正此错误,应该替换以下代码片段:
/* better example! */
...
pthread_getspecific(key, &data);
free(data);
pthread_setspecific(key, NULL);
...线程终止时,未调用析构函数例程,因为没有特定于线程的数据。
使用非指针值
尽管可以存储不是指针的值,但是由于以下原因并不建议这么做:
- 将指针数据类型转换为标量类型可能是无法移植的。
- NULL 指针值与实现有关,有些系统将非零值指定给 NULL 指针。
如果您确保您的程序永远不会移植到其他系统,你可以将整数值用于特定于线程的数据。