第31章 线程:线程安全和每线程存储

        本章将拓展POSIX线程API的探讨,描述线程安全(thread-safe)函数以及一次性初始化。同时在讨论不改变接口定义的前提下,如何让通过线程特有数据(thread-specific data)或线程局部存储(thread-local storage)实现已有函数的线程安全。

31.2 线程安全(再讨论可重入性)

        若函数可同时供多个线程安全调用,则称之为线程安全函数:反之如果函数不是线程安全的,则不能并发调用,例如如下函数(30.1节也有类似函数)就不是线程安全的:

staticint glob = 0;

static void incr(int loops)
{
   int loc, j;
    for (j = 0; j < loops; j++) {
        loc = glob;
        loc++;
        glob = loc;
    }
}

如果多个线程并发调用该函数,glob的最终值将不得而知。本例展示了导致线程不安全的典型原因:使用了在所有线程之间共享的全局或静态变量。

        实现线程安全有多种方式。其一是将函数与互斥量关联使用(如果函数库中的所有函数都共享同样的全局变量,那么或许应将所有函数斗鱼该互斥量相关联),在调用函数时将其锁定,在函数返回时解锁。这一方法的优点在于简单。另一方面,这也意味着同时只能有一个线程执行该函数,亦即,对该函数的访问是串行的。如果各线程在执行此函数时都耗费了相当多的时间,那么穿行化会导致并发能力的丧失,所有线程将不再并发执行。

        另一种更为复杂的解决方案是:将共享变量与互斥量关联起来,这需要程序员们确认函数的那些部分是使用了共享变量的临界区,且仅在执行到临界区时区获取和释放互斥量。这将允许多线程同时执行一个函数并实现并行,除非出现多个线程需要同时执行同意临界区的情况。

非线程安全函数

        为方便开发多线程应用程序,除了表31-1所列函数意外(其中大部分并未在本书中提及),SUSv3中的所有函数都需要实现线程安全。

        除了表31-1中所列函数,SUSv3还做了如下规定。

  • 如传参为NULL时、,函数ctemid()和tmpnam()无需是线程安全的。
  • 如果函数wcrtomb()和wcsrtombs的最后一个参数(ps)为NULL,那么这两个函数也无需是线程安全的。

SUSv4对表31-1中的函数做了以下修改。

  • 移除函数ecvt()、fcvt()、gcvt()、gethostbyname()以及gethostbyaddr,因为已从标准中删除了这些函数。
  • 增减函数strsignal()和system()。由于system()函数就信号处置所作的操作系统将影响整个进程,故而是不可重入的。

标准并未禁止将表31-1中的函数时限为线程安全。不过,即使在某些实现中有些是线程安全的,为确保应用程序的可移植性,也不应该假设这些函数在所有实现中都是如此。

表31-1:SUSv3不要求这些函数是线程安全的

 可重入和不可重入函数

        较之于对整个函数使用互斥量,使用临界区实现线程 安全虽然有明显改进,但由于存在对互斥量的加锁和解锁开销,所以多少还是有点低效。可重入函数则无需使用互斥量即可实现线程安全。其要诀在于避免对全局和静态变量的使用。需要返回给调用者的让你和信息,亦或是需要在对函数的里此调用间加以维护的信息,都存储与调用者分配的缓冲区内。(初次碰到可重入问题,是在21.1.2节讨论信号处理器变量时。)不过,并非所有函数都可实现为可重入,通常原因如下。

  • 根据其性质,有些函数必须访问全局数据结构。malloc函数库中的函数就是这方面的典范。这些函数为堆中的空闲块维护一个全局链表。malloc库函数的线程安全是通过互斥量来实现的。
  • 一些函数(在线程发明之前就已问世)的接口本身就定义为不可重入,要么返回指针,指向有函数自身静态分配的存储空间,要么利用静态存储对该函数(或相关函数)历次调用间的信息加以维护。表31-1所列函数大多属于此类,例如asctine()就返回一个指针,指向经由静态分配的缓冲区,其内容为日期和时间字符串。

        对于一些接口不可重入的函数,SUSv3为其定义了以后缀_r结尾的可重入”替身“。这些“替身”函数要求由调用者来分配缓冲区,并将缓存区地址传给函数用已返回结果。这使得调用线程可以使用局部(栈)来存放函数结果。处于这一目的,SUSv3定义了如下函数:astime_r()、ctime_r()、getgrgid_r()、getgmam_r()、getlogin_r()、getpwnam_r()、getpwuid_r()、gmtime_r()、localtime_r()、rand_r(),readdir_r()、strerror_r()、strtok_r()和ttyname_r()。

        有些系统实现为一些系统的不可重入函数也提供了附加的可重入“替身”。例如,glibc就提供了函数crypt_r()、gethostbyname_r()、getserverbyname_r()、getutent()、getuid_r()、getutline_r()和ptsname_r()。不过,为确保应用程序的可移植性,不应假设这些函数在其他实现中也存在。

31.2 一次性初始化

        多线程程序有时有这样的需求:不管创建了多少线程,有些初始化动作只能发生一次。例如,可能需要执行pthread_mutex_init()对带有特殊性的互斥量进行初始化。如果主线程来创建新线程,那么这一点易如反掌:可以在创建依赖于该初始化的线程之前进行初始化。不过,对于库函数而言,这样处理就不可行,因为调用者在初次调用库函数之前可能已经创建了这些线程。故而需要这样的库函数:无论首次为任何新线程所调用。都会执行初始化动作。

        库函数可以通过函数pthread_once()实现一次性初始化。

#include <pthread.h>
int pthread_once(pthred_once_t *once_control,void(*init)(void));
        Returns 0 on success,or a positive error number on error

        利用参数once_control的状态,函数pthread_once()可以确保无论有多少前程对pthread_once()调用了多少次,也只会执行一次由init指向的调用者定义函数。

        init函数没有任何参数,形式如下:

void init(void)
{
    //function body
}

        另外,参数once_control必须是一指针,指向初始化为PTHREAD_ONCE_INIT的静态变量。

pthread_once__t once_var = PTHREAD_ONCE_INIT;

        调用函数pthread_once()时要制定一个指针,指向类型为pthread_once_t的特定变量,对该函数的首次调用将修改once_control所指向的内容,以便对其后续调用不会再次执行init。

        常常将Pthread_once()和线程特有数据结合使用,相关内容会在下一节描述。

31.3 线程特有数据

        实现线程安全的最为有效的方式是使其可重入,应以这种方式来实现所有新的函数库。不过,对于已有的不可重入函数库(可能问世于主线程流行之前)来说,采用这种方式通常需要修改函数接口,这也意味着,需要修改函数接口,这也意味着需修改所有使用此类函数的应用程序。

        使用线程特有数据技术,可以无需修改函数接口而是先已有的线程安全。较之于可重入函数,采用线程特有数据的函数效率可能要略低一些,不过对于使用了这些调用的程序而言,则省去了修改程序之劳。

如图31-1所示线程特有数据使函数得以为每个调用线程分别维护一份变量的副本(copy)。线程特有数据是长期存在的。在同一线程对相同函数的历次调用期间,每个线程的变量会持续存在,函数可以向每个调用线程返回各自的结果缓冲区(如果需要的话)

31.3.1 库函数视角下的线程特有数据

        要了解线程特有数据线管API使用,需要从使用这一技术的库函数角度来考虑如下问题。

  • 该函数必须为每个调用者线程分配单独的存储,且只需在县城出此调用此函数时分配一次即可
  • 在同一线程对此函数的后续所有调用中,该函数都需要获取初次调用时线程分别配的存储块地址。由于函数调用结束时会释放自动变量,故而函数不应利用自动变量存放存储块指针,也不能将指针存放于静态变量中,因为静态变量在进程中只有一个特例。Pthread_API提供了函数来处理这一情况。
  • 不同(无相互依赖关系)函数各自可能都需要使用线程特有数据。每个函数都需要方法来标识其自身的线程特有数据(键),以便与其他函数所使用的线程特有数据有所区分。
  • 当线程退出时,函数无法控制要发生的情况。这是,线程可能会执行该函数之外的代码。不过,一定存在某些机制(解构器),在线程退出时会自动释放为该线程所分配的存储。若非如此,随着持续不断地创建线程,调用函数和终止线程,将会引发内存泄漏。

31.3.2 线程特有数据API概述

        要使用线程特有数据,可函数执行的一般步骤如下。

  1. 函数创建一个键(key),用以将不同函数使用的线程特有数据项区分开来。调用函数pthread_key_create()可创建此键,且秩序在首个调用该函数的线程中创建一次,函数pthread_once()的使用正是出于这一目的,即允许在创建时并未分配任何线程特有数据块。
  2. 调用pthread_key_create()还有另一个目的,即允许调用者指定一个自定义解构函数,用于释放为该键所分配的各个存储块。当使用线程特有数据的线程终止时,Pthreads API 会自动调用此解构函数,同时将该线程的数据块指针作为参数传入。
  3. 函数会为每个调用者线程创建线程特有数据块。这一分配通过malloc()(或类似函数)完成,每个线程只分配一次,且只会在线程初次调用此函数时分配。
  4. 为了保存上一步所分配存储块的地址,函数会使用两个Pthreads函数:pthread_setspecific()和pthread_getspecific()。调用函数pthread_setspecific()实际上是对Pthreads实际上发起这样的请求:保存该指针并记录与特定键(该函数的键)以及特定线程(调用者线程)的关联性。调用pthread_getspecific()所执行的操作是互补操作:返回之前保存的,与给定键以及调用线程i相关联的指针。如果还没有指针与特定的键及线程相关联,那么pthread_getspecific()返回NULL.函数可以利用这一点来判断自身是否初次为某个线程所调用,若为初次,则必须为该线程分配空间。

31.3.3 线程特有数据API详述

        本节将讲述上节提及的各个函数,并通过对线程特有数据的典型实现来说明其操作方法。协议姐会演示如何使用线程特有数据来实现线程安全的标准C语言函数strerror(0函数。

        调用pthread_key_create()函数为线程特有数据创建一个新键,并通过key所指向的缓冲区返回给调用者。

        因为进程中的所有线程都可使用返回的键,所以key应执行一个全局变量。

#include <pthread.h>
int pthread_key_create(pthread_key_t *key,void(*destructor)(void *));
    Returns 0 on success, or a positive error number on error

参数destructor指向一个自定义函数,其格式如下:

void dest(void * value)
{
    /*Release store=age pointed to by value*/
}

 只要线程终止时与key的关联值不为NULL,Pthreads API 会自动执行解构函数,并将与key的关联值作为参数传入解构函数,传入的值通常与该键关联,且指向线程特有数据块的指针。如无需解构,那么可将destructor设置为NULL.

如果一个线程有多个线程特有数据块,那么对各个解构函数的调用顺序是不确定的。对每个解构函数的设计应相互独立。

        观察线程特有数据的实现有助于理解他们的使用方法,典型的实现(NPTL即在此列)会包含以下数组。

  1. 一个全局(进程范围)数组,存放线程特有数据的键信息。
  2. 每个线程包含一个数组,存有为每个线程分配的线程特有数据块的指针(通过调用pthread_setspecific()来存储指针)。

        在这一实现中,pthread_key_create()返回的pthread_key_t类型值只是对全局数组的索引(index),标记为pthread_keys,其格式如图31-2所示。数组的每个元素都是一个包含两个字段(field)的结构。第一个字段标记该数组元素是否在用(即已由之前对pthread_key+create()的调用分配)。第二个字段用于存放针对此键、线程特有数据块的解构函数指针(是函数pthread_key_create()中参数destructor的一份拷贝)。

        函数pthread_setspecific()要求Pthreads API将value的副本存储与一数据结构中,并将value与调用线程以及key相关联(key由之前对pthread_key_create()的调用返回)。pthread_getspecific()函数执行的操作与之相反,返回之前与本线程及给定key相关的值(value).

#include <pthread.h>

int pthread_setspecific(pthread_key_t key,const void *value);
    Returns 0 on success,or a positive error number on error
int pthread_getspecific(pthread_key_t key)
    Rrturns pointer ,or NULL if no thread-specific data isassociated with key

        函数pthread_setspecific()的参数value通常是一直真,指向由调用者分配的一块内存。当线程终止时,会将该指针作为参数传递给与key对应的解构函数。‘

参数value也可以不是指向内存区域的指针,而是任何可以赋值(通过强制转换)给void*的标量值。在这种情况下,先前对pthread_key_create()函数的调用应将destructor指定为NULL。

        图31-3展示了用于存储value的数据皆否的常见实现,图中假设将pthread_keys[1]分配给函数myfunc()。Pthreads API为每个函数维护致电给线程特有数据块的一个指针数组,其中每个数组元素斗鱼图31-2中全局pthread_keys数组的元素一一对应。函数pthread_setspecific()在指针数组中为每个调用线程设置于key对应的元素。

         当线程刚刚创建时,会将所有线程特有数据的指针都初始化为NULL.A这意味着当线程初次调用库函数时,必须使用pthread_getspecific()函数来检查线程是否已有与key对应的关联值。如果没有,那么此函数会分配一块内存并通过pthread_setspecific(0保存指向该内存块的指针,在下一节实现线程安全版的strerror()函数时,将给出示例。

31.3.4 使用线程特有数据API

         3.4节在首都论及标准strerror()函数时曾指出,可能会返回一个指向静态分配字符串的指针作为函数结果,这意味着strerror()可能不是线程安全的。后面将以数页篇幅讨论下非线程安全的strerror()实现,接着说明如何shiyongxiancheng-特有数据来实现该函数的线程安全。

在包括Linux 在内的许多UNIX实现中,由标准C语言函数库提供的strerror函数都是线程安全的。不过,由于SUSv3并未规定该函数必须是线程安全的,而且这一strerror()的实现又为使用线程特有数据提供了一个简单范例。

程序清单31-1演示了非线程安全版strerror()函数的一个简单实现。该函数利用了由glibc定义的一对全局变量:_sys_errlist是一个指针数组,其每个元素指向一个与errno错误号相匹配的字符串(因此,例如_sys_errlist[EINVAL]即指向字符串 Invalid operation);_sys_nerr表示_sys_errlist中的元素个数。

程序清单31-1:非线程安全版-strerror()函数的一种实现

#define _GNU_SOURCE   /*Get _sys_nerr and _sys_errlist
                        declaration from <stdio.h>*/
#include <stdio.h>
#include <string.h>  //get declaration of strerror()

#define MAX_ERROR_LEN 256 /*Maxmum length of string returned by strerror()*/

static char buf[MAX_ERROR_LEN];  /*Statically allocated return buffer*/

char * strerror(int err)
{
    if(err<0 || err>=_sys_nerr || _sys_errlist[err] == NULL){
        snprintf(buf,MAX_ERROR_LEN,"Unknown error %d",err);
    }else{
        strncpy(buf,_sys_errlist[err],MAX_ERROR_LEN-1);
        buf[MAX_ERROR_LEN-1] = '\0'; /*eNSURE NULL TERMINATION*/
    }

    return buf;
}

        可以利用程序清单31-2中程序来展示程序清单中非线程安全版的strerror()实现所造成的后果,该程序分别从不同线程调用strerror(),并且均在两个线程调用strerror()之后才显式返回结果。虽然两个线程为strerror()指定的参数值不同(EINVAL和EPERM),在与程序清单31-1版的strerror()连接编译后,运行该程序将产生如下结果:

 两个线程都显示与EPERM对应的errno字符串,因为第二个线程对strerror()的调用(在函数threadFunc()中)覆盖了主线程调用strerror()时写入缓冲区的内容,检查输出结果可以发现,两个线程的局部变量str均指向同一内存地址

程序清单31-2:从两个不同线程调用strerror()---strerror_test

#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
static void *threadFunc(void *arg)
{
    char * str;
    printf("Other thread about to call strerror()\n");

    str = strerror(EPERM);
    printf("Other thread:str (%p)=%s\n",str,str);

    return NULL;
}

int main(int argc, char *argv[])
{
    pthread_t t;
    int s ;
    char *str;
    str = strerror(EINVAL);
    printf("Main thread has called strerror()\n");

    s = pthread_create(&t,NULL,threadFunc,NULL);
    if(s!=0)
    {
        printf("pthread_create:");
        return -1;
    }

    s = pthread_join(t,NULL);
    if(s!=0)
    {
        printf("pthread_join:");
        return -1;
    }
     printf("Main thread:str (%p)=%s\n",str,str);
     exit(EXIT_SUCCESS);
}

程序清单31-3:使用线程特有数据以实现线程安全的strerror()函数--strerror_tsd.c

#define _GNU_SOURCE   /*Get _sys_nerr and _sys_errlist
                        declaration from <stdio.h>*/
#include <stdio.h>
#include <string.h>  //get declaration of strerror() 
#include <pthread.h>
#include <stdlib.h>
#include <errno.h>
static pthread_once_t once =PTHREAD_ONCE_INIT;
static pthread_key_t strerrorKry;

#define MAX_ERROR_LEN 256 /*Maxmum length of string returned by strerror()*/

static void destructor(void *buf) /*free thread-specific data buffer*/
{
    free(buf);
}

static void createKey(void) /*One-time key creation function*/
{
    int s;
    /*Allocate a unique thread-specific data key and save the address
        of the destructor for thread-specific data buffers*/
    
    s=pthread_key_create(&strerrorKry,destructor);
    if(s!=0)
    {
        printf("pthread_key_create:");
        return ;
    }
}

char * strerror(int err)
{
    int s;
    char *buf;
    /*make first caller allocate key for thread-specific data*/

    s = pthread_once(&once,createKey);
    if(s!=0)
    {
        printf("pthread_once:");
        return NULL;
    }
    buf = pthread_getspecific(strerrorKry);
    if(buf ==NULL){ /*if first call from this thread,allocate buffer for  thread, and save its location*/
        buf = malloc(MAX_ERROR_LEN);
        if(buf == NULL)
        {
            perror("pthread_once:");
            return NULL;
        }
        s = pthread_setspecific(strerrorKry,buf);
        if(s!=0)
        {
            printf("pthread_serspecific:");
            return NULL;
        }

    }
    if(err<0 || err>=_sys_nerr || _sys_errlist[err] == NULL){
        snprintf(buf,MAX_ERROR_LEN,"Unknown error %d",err);
    }else{
        strncpy(buf,_sys_errlist[err],MAX_ERROR_LEN-1);
        buf[MAX_ERROR_LEN-1] = '\0'; /*Ensure null termination*/
    }

    return buf;
}

        改进版strerror()所作的第一步是调用pthread_once(),以确保(从任何线程)对该函数的首次调用将执行createKey().函数createKey()会调用pthread_key_create()来分配一个线程特有数据的键(key),并将其存储于全局变量strerrorKey中。对pthread_key_create()的调用同时会记录解构函数的地址,将使用该解构函数来释放与键对应的线程特有数据缓冲区。

        接着,函数strerror()调用pthread_getspecific()以获取该线程中对应于strerrorKey的唯一缓冲地址,如果strerrorKey()返回NULL,这表明该线程是首次调用strerror()函数,因为此函数会调用malloc()分配一个新缓冲区,并使用pthread_setspecific()来保存该缓冲区的地址,如果pthread_getspecific()的返回值为非NULL,那么该值指向业已存在的缓冲区,此缓冲区由之前对strerror()的调用所分配。

        这一strerror()函数实现的剩余部分与废县丞安全版的前述实现乐死,唯一的区别在于,buf是现成特有数据的缓冲区地址,而非静态变量。

        如果使用新版strerror(程序清单31-3)编译连接程序(31-2)strerror_test_tsd,程序运行会有以下结果:

         根据这一输出,可以看出新版strerror(0是线程安全的:两个线程中局部变量str所指向的地址是不同的。

31.3.5 线程特有数据的实现限制

        正如对线程特有数据典型实现过程的描述所揭示的,实现可能要对其所支持的线程特有数据键的数量加以限制。SUSv3要求至少支持128(_POSIX_THREAD_KEYS_MAX)个键。应用程序要么通过对PTHREAD_KEY_MAX(定义于<limits.h>)的定义,要么通过调用sysconf(_SC_THREAD_KEYS_MAX),来确定实际支持的键的数量。Linux支持多大1024个键。

        即使128个键对大多数应用来说已经绰绰有余。这是因为,每个库函数应该只会用到少量的键,通常只会用一个。如果一噶函数需要多个线程特有的数据的值,同城可以将这些值置于一个结构中,并将该解构与一个线程特有数据的键关联。

31.4线程局部存储

        类似于线程特有数据,线程局部存储提供了持久的每线程存储。作为非标准特性,诸如其他的UNIX实现(例如Solaris和FreeBSD)为其提供了相同或类似的接口形式。

        线程局部存储的主要优点在于,比线程特有数据的使用要简单。要创建线程局部变量,秩序简单地在全局静态变量地声明中包含__thread说明符即可。

static __thread buf[MAX_ERROR_LEN];

        但凡带有这种说明符地变量,每个线程都拥有一份对变量地拷贝。线程局部存储地变量将一直存在,直至线程终止,届时会自动释放这一存储。

        关于线程局部变量地声明和使用,需要注意如下几点。

  • 如果变量变量声明中使用了关键字static或extern,那么关键字————thread必须紧随其后。
  • 与一般地全局变量声明一样,线程局部变量在声明时可设置一个初始值。
  • 可以使用C语言取地址操作符(&)来获取线程局部变量地地址。

        线程局部存储需要内核(Linux 2.6提供)、Pthread实现(由NPTL提供)以及C编译器地支持。

        程序清单31-4提供了使用线程局部存储实现线程安全版strerror()函数地例子。如果用该版strerror与测试程序(31-2)编译、连接、生成strerror_test_tls,那么运行时将产生如下结果:

 程序清单31-4:使用线程局部存储实现线程安全版地strerror()函数--strerror_tls.c

#define _GNU_SOURCE   /*Get _sys_nerr and _sys_errlist
                        declaration from <stdio.h>*/
#include <stdio.h>
#include <string.h>  //get declaration of strerror() 
#include <pthread.h>

#define MAX_ERROR_LEN 256 /*Maxmum length of string returned by strerror()*/

static __thread char buf[MAX_ERROR_LEN];  
                /*thread-local return buffer*/

char * strerror(int err)
{
    if(err<0 || err>=_sys_nerr || _sys_errlist[err] == NULL){
        snprintf(buf,MAX_ERROR_LEN,"Unknown error %d",err);
    }else{
        strncpy(buf,_sys_errlist[err],MAX_ERROR_LEN-1);
        buf[MAX_ERROR_LEN-1] = '\0'; /*Ensure null termination*/
    }

    return buf;
}

31.5 总结

        若一函数可有多个线程同时安全调用,则称之为线程安全地函数,使用全局或静态变量是导致函数非线程安全地通常原因。在多线程应用中,保障非线程安全函数安全地手段之一是运用互斥锁来防护对该函数地多有调用。这种方法。带来了并发性能地下降,因为同一时间点只有一个线程运行该函数。提升并发性能的另一方法是:金子啊函数中操作共享变量(临界区)的代码前后加入互斥锁。

        使用互斥量可以实现大部分函数的线程安全,不过由于互斥量的加、解锁开销。故而也带来了性能的下降。如能避免使用全局或静态变量,可重入函数则无需使用互斥量即可实现线程安全。

        SUSv3所规范的大部分函数都需要实现线程安全。SUSv3同时也列出了小部分无需实现线程安全的函数。一般情况下,这些函数将静态存储返回给调用者,或者在对函数的连续调用间进行信息维护。根据定义,这些函数是不可宠儿u的,也不能使用互斥量来确保其线程安全。本章讨论了两种大致相当的编程技术---线程特有数据和线程局部存储---可在无需改变函数接口定义的情况下保障不安全桉树的线程安全。这两种技术军允许函数分配持久的、基于线程的存储。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值