特定于线程的数据

许多应用程序要求在每个线程的基础上跨函数调用维护某些数据。

例如,为每个文件使用一个线程 的多线程 grep 命令必须拥有特定于线程的文件处理程序和找到的字符串列表。 线程库提供特定于线程 的数据接口以满足这些需要。

特定于线程的数据可以看作是值的两维数组,键作为行索引,线程标识作为列索引。 特定于线程的数据是一个不透明对 象,是 pthread_key_t 数据类型。 同样的键可以由进程中的所有线程使用。 尽管所有的线程都使用相同的键,但是它们设置并访与该键关联的不同的特定于线程的数据值。 特定于线程的数据是空指针,从而允许对任意类型的数据进行引用,例如动态 分配的字符串或结构。

在下图中,线程 T2 有一个与键 K3 关联的特定于线程的数据值 12。 线程 T4 有与相同的键关联的值 2。
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 值,包括还未创建的线程。

例如,考虑两个线程 AB。 线程 A 按时间顺序执行以下操作:

  1. 创建特定于线程的数据键 K

    线程 AB 可以使用键 K。 这两个线程的值都是 NULL

  2. 创建一个线程 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 */
...

在我们的示例中,线程 AB 并行运行,但调用 1 会在调用 2 之前发生。 调用 1 将创建密钥 K1 并将其存储在 theKey 变量中。 调用 2 将创建另一个 键 K2,也将它存储在 theKey 变量中,这样就覆盖了 K1 的值。 结果是,线程 A 将使用 K2,假定它是 K1。 由于 以下原因应当避免这种情况:

  • 键 K1 丢失,所以直到进程终止才会回收它的存储空间。 由于键的数目有限,所以您可能不会有足够的键。
  • 如果线程 A 在调用 2 之前使用 theKey 变量存储特定于线程的数据,那么该数据将绑定到键 K1。 在调用 2 之后,theKey 变量包含 K2;接着如果 A 试图获取特定于线程的数据,将会始终得到 NULL

确保按照以下方式唯一地创建这些键:

  • 使用一次性初始化工具。
  • 在线程使用键之前创建它。 通常这是可能的,例如,使用带有特定于线程的数据的线程池来执行类似的操作。 线程池通常由一个线程,即初始(或另一个“驱动程序”)线程创建。

程序员的责任是确保键创建的唯一性。 线程库没有提供检查键是否被多次创建的方法。

析构函数例程

析构函数例程可能与每个特定于线程的数据键关联。 无论何时终止线程时,如果有非 NULL 的特定于线程的数据使该线程绑定到任何键,那么调用与该键关联的析构函数例程。 这使得在线程终止时自动释放动态分配的特定于线程的数据。 析构函数例程只有一个参数,即特定于线程的数据的值。

例如,特定于线程的数据关键字可以用于动态分配的缓冲区。 应提供析构函数例程以确保当线程终止释放缓冲区时,可按如下方式使用 free 子例程:
pthread_key_create(&key, free);
可以使用更复杂的析构函数。 如果多线程的 grep 命令(每个文件使用一个线程进行扫 描)具有特定于线程的数据,用来存储包含工作缓冲区和线程的文件描述符的结构,那么析构函数例程可能如下:
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;
}

析构函数调用最多可重复四次。

键析构

可以通过调用 pthread_key_delete 子例程来销毁特定于线程的数据密钥。 pthread_key_delete 子例程并没有真正为拥有数据的每个线程调用析构函数例程。 当数据键被删除后,它可以被另一个对 pthread_key_create 子例程的调用重用。 所以,使用多个数据键 时,pthread_key_delete 子例程尤其有用。 例如,在以下代码片段中循环将永远不会结束:
/* bad example - do not write such code! */
pthread_key_t key;
 
while (pthread_key_create(&key, NULL))
        pthread_key_delete(key);

使用特定于线程的数据

使用 pthread_getspecificpthread_setspecific 子例程访问特定于线程的数据。 pthread_getspecific 子例程读取绑定到指定键的值并特定于调用线程; pthread_setspecific 子例程设置该值。

设置连续值

绑定到特定关键字的值应该是一个指针,它可以指向任意类型的数据。 特定于线程的数据通常用于动态分配的存储空间,如以下代码片段所示:
private_data = malloc(...);
pthread_setspecific(key, private_data);
设置一个值时,先前的值将丢失。 例如,在以下的代码片段中,old 指针的值丢失,该 指针指向的存储空间可能无法恢复:
pthread_setspecific(key, old);
...
pthread_setspecific(key, new);
程序员的责任是检索旧的特定于线程的数据值,从而在设置新值之前回收存储空间。 例如,可以用以下方法 实现 swap_specific 例程:
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 子例程时提供析构函数例程。 程序员还必须确保当释放为特定于线程的数据分配的存储空间时,指针必须设为 NULL。 否则,析构函数例程可能会通过非法的参数调用。 例如:
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 指针。

如果您确保您的程序永远不会移植到其他系统,你可以将整数值用于特定于线程的数据。