线程相关(二)——线程控制

2  线程控制

2.1  线程属性

        在线程相关(一)的帖子中,所有调用pthread_create函数的例子中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针。可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用pthread_attr_init函数初始化pthread_attr_t结构。调用pthread_attr_init以后,pthread_attr_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。如果要修改其中个别属性的值,需要调用其他的函数,这方面的细节将在后续内容讨论。

#include <pthread.h>

int pthread_attr_init (pthread_attr_t *attr);

int pthread_attr_destroy (pthread_attr_t *attr);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        如果要去除对pthread_atrr_t结构的初始化,可以调用pthread_attr_destroy函数。如果pthread_attr_init实现时为属性对象分配了动态内存空间,pthread_attr_destroy将会释放该内存空间。除此之外,pthread_attr_destroy还会用无效的值初始化属性对象,因此如果该属性对象被误用,将会导致pthread_create函数返回错误。

        pthread_attr_t结构对应用程序是不透明的,也就是说应用程序并不需要了解有关属性对象内部结构的任何细节,因而可以增强应用程序的可移植性。POSIX.1沿用了这种模型,并且为查询和设置每种属性定义了独立的函数。

        线程相关(一)的帖子中介绍过分离线程的概念,如果对现有的某个线程的终止状态不感兴趣的话,可以使用pthread_detach函数让操作系统在线程退出时收回它所占用的资源。

        如果在创建线程时就知道不需要了解线程的终止状态,则可以修改pthread_attr_t结构中的detachstate线程属性,让线程以分离状态启动。可以使用pthread_attr_setdetachstate函数把线程属性detachstate设置为下面的两个合法值之一:设置为PTHREAD_CREATE_DETACHED,以分离状态启动线程;或者设置为PTHREAD_CREATE_JOINABLE,正常启动线程,应用程序可以获取线程的终止状态。

#include <pthread.h>

int pthread_attr_getdetachstate (const pthread_attr_t *restrict attr, int *detachstate);

int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        可以调用pthread_attr_getdetachstate函数获取当前的detachstate线程属性,第二个参数所指向的整数也许被设置为PTHREAD_CREATE_DETACHED,也可能设置为PTHREAD_CREATE_JOINABLE,具体要取决于给定pthread_attr_t结构中的属性值。

        接下来演示分离状态创建线程。

#include <pthread.h>

int makethread(void *(*fn)(void *), void *arg)
{
    int err;
    pthread_t tid;
    pthread_attr_t attr;
    
    err = pthread_attr_init(&attr);
    if (err != 0)
        return (err);
    err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    if (err == 0)
        err = pthread_create(&tid, &attr, fn, arg);
    pthread_attr_destroy(&attr);
    return (err);
}
        注意,这里忽略了pthread_attr_destroy函数调用的返回值。在这种情况下,由于对线程属性进行了合理的初始化,pthread_attr_destroy一般不会失败。但是如果pthread_attr_destroy确实出现了失败的情况,清理工作就会变得很困难:必须销毁刚刚创建的线程,而这个线程可能已经运行,并且与pthread_attr_destroy函数可能是异步执行的。忽略pthread_attr_destroy的错误返回可能出现的最坏情况是:如果pthread_attr_init分配了内存空间,这些内存空间会被泄漏。另一方面,如果pthread_attr_init成功地对线程属性进行了初始化,但pthread_attr_destroy在做清理工作时却出现了失败,就没有任何补救策略,因为线程属性结构对应用来说是不透明的,可以对线程属性结构进行清理的唯一接口是pthread_attr_destroy,但它失败了。

        对于遵循POSIX标准的操作系统来说,并不一定要支持线程栈属性,但是对遵循XSI的系统,支持线程栈属性就是比需的。可以在编译阶段使用_POSIX_THREAD_ATTR_STACKADDR和_POSIX_THREAD_ATTR_STACKSIZE符号来检查系统是否支持线程栈属性,如果系统定义了这些符号,就说明它支持相应的线程栈属性。也可以通过在运行阶段把_SC_THREAD_ATTR_STACKADDR和_SC_THREAD_ATTR_STACKSIZE参数传给sysconf函数,检查系统对线程栈属性的支持情况。

        POSIX.1定义了线程栈属性的一些操作接口。虽然很多pthread实现中仍然提供两个早些时侯的函数pthread_attr_getstackaddr和pthread_attr_setstackaddr,但在Single UNIX Specification第3版中这两个函数已被标记为过时,线程栈属性的查询和修改一般是通过较新的函数pthread_attr_getstack和pthread_attr_setstack来进行。这些新的函数消除了老接口定义中存在的二义性。

#include <pthread.h>

int pthread_attr_getstack (const pthread_attr_t *restrict attr, void **restrict stackaddr, size_t *restrict stacksize);

int pthread_attr_setstack (const pthread_attr_t *attr, void *stackaddr, size_t *stacksize);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        这两个函数可以用于管理stackaddr线程属性,也可以用于管理stacksize线程属性。

        对进程来说,虚拟地址空间的大小是固定的,进程中只有一个栈,所以它的大小通常不是问题。但对线程来说,同样大小的虚拟地址空间必须被所有的线程栈共享。如果应用程序使用了太多的线程,导致线程栈的累计大小超过了可用的虚拟地址空间,这时就需要减少线程默认的栈的大小。另一方面,如果线程调用的函数分配了大量的自动变量或者调用的函数涉及很深的栈帧(stack frame),那么这时需要的栈大小可能要比默认的大。

        如果用完了线程栈的虚拟地址空间,可以使用malloc或者mmp来为其他栈分配空间,并用pthread_attr_setstack函数来改变新建线程的栈位置。线程栈所占内存范围中可寻址的最低地址可以由stackaddr参数指定,该地址与处理器结构相应的边界对齐。

        stackaddr线程属性被定义为栈的内存单元的最低地址,但这并不必然是栈的开始位置。对于某些处理器结构来说,栈是从高地址向低地址方向伸展的,那么stackaddr线程属性就是栈的结尾而不是开始为止。

        pthread_attr_getstackaddr和pthread_attr_setstackaddr的缺陷在于stackaddr参数并没有明确地指定。它可以解释为栈的开始地址,还可以解释成用作栈的内存范围的最低地址。在栈内存地址空间从高地址向低地址扩展的处理器结构中,如果stackaddr参数是栈地址空间的最低地址,那么就需要知道栈的大小才能确定栈的开始位置。pthread_attr_getstack和pthread_attr_setstack函数纠正了这些缺陷。

        应用程序也可以通过pthread_attr_getstacksize和pthread_attr_setstacksize函数读取或设置线程属性stacksize。

#include <pthread.h>

int pthread_attr_getstacksize (const pthread_attr_t *restrict attr, size_t *restrict stacksize);

int pthread_attr_setstacksize (pthread_attr_t *attr, size_t stacksize);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        如果希望改变栈的默认大小,但又不想自己处理线程栈的分配问题,这时使用pthread_attr_setstacksize函数就非常有用。

        线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个属性默认设置为PAGESIZE个字节。可以把guardsize线程属性设为0,从而不允许属性的这种特征行为发生:在这种情况下不会提供警戒缓冲区。同样地,如果对线程属性stackaddr作了修改,系统就会假设我们自己会自己管理栈,并使警戒栈缓冲区机制无效,等同于把guardsize线程属性设为0。

#include <pthread.h>

int pthread_attr_getguardsize (const pthread_attr_t *restrict attr, size_t *restrict guardsize);

int pthread_attr_setguardsize (pthread_attr_t *attr, size_t guardsize);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        如果guardsize线程属性被修改了,操作系统可能把它取为页大小的整数倍。如果线程的栈指针溢出到警戒区域,应用程序就可能通过信号接收到出错信息。

        Single UNIX Specification还定义了其他的一些可选的线程属性作为实时线程可选属性的一部分,这里不做更多讨论。

        线程的并发读控制着用户级线程可以映射的内核线程或进程的数目。如果操作系统的实现在内核级的线程和用户级的线程之间保持一对一的映射,那么改变并发度并不会有什么效果,因为所有的用户级的线程都可能被调度到。但是,如果操作系统的实现让用户级线程到内核级线程或进程之间的映射关系是多对一的话,那么在给定时间内增加可运行的用户级线程数,可能会改善性能。pthread_setconcurrency函数可以用于提示系统,表明希望的并发度。

#include <pthread.h>

int pthread_getconcurrency (void);

        返回值:当前的并发度。

int pthread_setconcurrency (int level);

        返回值:若成功则返回0,否则返回错误编号。

        pthread_getconcurrency函数返回当前的并发度。如果操作系统当前正控制着并发度(即之前没有调用过pthread_setconcurrency函数),那么pthread_getconcurrency将返回0。

        pthread_setconcurrency函数设定的并发度只是对系统的一个提示,系统并不保证请求的并发度一定会被采用。如果希望系统自己决定使用什么样的并发度,就把传入的参数level设为0。这样,应用程序调用level参数为0的pthread_setconcurrency函数,就可以撤销在这之前level参数非零的pthread_setconcurrency调用所产生的作用。

2.2  同步属性

2.2.1  互斥量属性

        用pthread_mutexattr_init初始化pthread_mutexattr_t结构,用pthread_mutexattr_destroy来对该结构进行回收。

#include <pthread.h>

int pthread_mutexattr_init (pthread_mutexattr_t *attr);

int pthread_mutexattr_destroy (pthread_mutexattr_t *attr);

        返回值:若成功则返回0,否则返回错误编号。

        pthread_mutexattr_init函数用默认的互斥量属性初始化pthread_mutexattr_t结构。值得注意的两个属性是进程共享属性和类型属性。POSIX.1中,进程共享属性是可选的,可以通过检查系统中是否定义了_POSIX_THREAD_PROCESS_SHARED符号来判断这个平台是否支持进程共享这个属性,也可以在运行时把_SC_THREAD_PROCESS+SHARED参数传给sysconf函数进行检查。虽然这个选项并不是遵循POSIX标准的操作系统必须提供的,但是Single UNIX Specification要求尊需XSI标准的操作系统支持这个选项。

        在进程中,多个线程可以访问同一个同步对象。在线程相关(一)中已说明,这是默认的行为。在这种情况下,进程共享互斥量属性需设置为PTHREAD_PROCESS_PRIVATE。

        系统存在这样的机制,允许相互独立得多个进程把同一个内存区域映射到它们各自独立的地址空间中。就像多个线程访问共享数据一样,多个进程访问共享数据通常也需要同步。如果进程共享互斥量属性设置为PTHREAD_PROCESS_SHARED,从多个进程共享的内存区域中分配的互斥量就可以用于这些进程的同步。

        可以使用pthread_mutexattr_getpshared函数查询pthread_mutexattr_t结构,得到它的进程共享属性,可以用pthread_mutexattr_setpshared函数修改进程共享属性。

#include <pthread.h>

int pthread_mutexattr_getpshared (const pthread_mutexattr_t *restrict attr, int *restrict pshared);

int pthread_mutexattr_setpshared (pthread_mutexattr_t *attr, int pshared);

        返回值:若成功则返回0,否则返回错误编号。

        进程共享互斥量属性设为PTHREAD_PROCESS_PRIVATE时,允许pthread线程库提供更加有效的互斥量实现,这在多线程应用程序中是默认的情况。在多个进程共享多个互斥量的情况下,pthread线程库可以限制开销较大的互斥量实现。

        类型互斥量属性控制着互斥量的特性。POSIX.1定义了四种类型。

  • PTHREAD_MUTEX_NORMAL类型是标准的互斥量类型,并不做任何特殊的错误检查或死锁检测。
  • PTHREAD_MUTEX_ERRORCHECK互斥量类型提供错误检查。
  • PTHREAD_MUTEX_RECURSIVE互斥量类型允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。用一个递归互斥量维护锁的计数,在解锁的次数和加锁次数不相同的情况下不会释放锁。所以如果对一个递归互斥量加锁两次,然后对它解锁一次,这个互斥量依然处于加锁状态,在对它再次解锁以前不能释放该锁。
  • PTHREAD_MUTEX_DEFAULT类型可以用于请求默认语义。操作系统在实现它的时侯可以把这种类型自由地映射到其他类型。例如,在Linux中,这种类型映射为普通的互斥量类型。

        四种类型的行为如下表,“不占用时解锁”这一栏指的是一个线程对被另一个线程加锁的互斥量进行解锁的情况;“在已解锁时解锁”这一栏指的是当一个线程对已经解锁的互斥量进行解锁时将会发生的情况,这通常是编码错误所致。

表1 互斥量类型行为
互斥量类型 没有解锁时再次加锁 不占用时解锁 在已解锁时解锁
PTHREAD_MUTEX_NORMAL 死锁 未定义 未定义
PTHREAD_MUTEX_ERRORCHECK 返回错误 返回错误 返回错误
PTHREAD_MUTEX_RECURSIVE 允许 返回错误 返回错误
PTHREAD_MUTEX_DEFAULT 未定义 未定义 未定义

        可以用pthread_mutexattr_gettype函数得到互斥量类型属性,用pthread_mutexattr_settype函数修改互斥量类型属性。

#include <pthread.h>

int pthread_mutexattr_gettype (const pthread_mutexattr_t *restrict attr, int *restrict type);

int pthread_mutexattr_settype (pthread_mutexattr_t *attr, int type);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        在线程相关(一)中讲过,互斥量用于保护与条件变量关联的条件。在阻塞线程之前,pthread_cond_wait和pthread_cond_timedwait函数释放与条件相关的互斥量,这就允许其他线程获取互斥量、改变条件、释放互斥量并向条件变量发送信号。既然改变条件时必须占有互斥量,所以使用递归互斥量并不是好的办法。如果递归互斥量被多次加锁,然后用在调用pthread_cond_wait函数中,那么条件永远都不会得到满足,因为pthread_cond_wait所做的解锁操作并不能释放互斥量。

        如果需要把现有的单线程接口放到多线程环境中,递归互斥量是非常有用的,但由于程序兼容性的限制,不能对函数接口进行修改。然后由于递归锁的使用需要一定技巧,它只应在没有其他可行方案的情况下使用。

        接下来使用递归互斥量的情况,这里有一个“超时”(timeout)函数,它允许另一个函数可以安排在未来的某个时间运行。假设线程并不是很昂贵的资源,可以为每个未决的超时函数创建一个线程。线程在时间未到时将一直等待,时间到了以后就调用请求的函数。

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/time.h>

int makethread(void *(*fn)(void *), void *arg)
{
	int err;
	pthread_t tid;
	pthread_attr_t attr;

	err = pthread_attr_init(&attr);
	if (err != 0)
		return (err);
	err = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	if (err == 0)
		err = pthread_create(&tid, &attr, fn, arg);
	pthread_attr_destroy(&attr);
	return (err);
}

struct to_info{
	void (*to_fn)(void *);
	void *to_arg;
	struct timespec to_wait;
};

#define SECTONSEC	1000000000
#define USECTONSEC	1000

void *timeout_helper(void *arg)
{
	struct to_info *tip;

	tip = (struct to_info *)arg;
	nanosleep(&tip->to_wait, NULL);
	(*tip->to_fn)(tip->to_arg);
	return (0);
}

void timeout(const struct timespec *when, void (*func)(void *), void *arg)
{
	struct timespec now;
	struct timeval tv;
	struct to_info *tip;
	int err;

	gettimeofday(&tv, NULL);
	now.tv_sec = tv.tv_sec;
	now.tv_nsec = tv.tv_usec * USECTONSEC;
	if ((when->tv_sec > now.tv_sec) || (when->tv_sec == now.tv_sec && when->tv_nsec > now.tv_nsec))
	{
		tip = malloc(sizeof(struct to_info));
		if (tip != NULL)
		{
			tip->to_fn = func;
			tip->to_arg = arg;
			tip->to_wait.tv_sec = when->tv_sec - now.tv_sec;
			if (when->tv_nsec >= now.tv_nsec)
			{
				tip->to_wait.tv_nsec = when->tv_nsec - now.tv_nsec;
			}
			else
			{
				tip->to_wait.tv_sec--;
				tip->to_wait.tv_nsec = SECTONSEC - now.tv_nsec + when->tv_nsec;
			}
			err = makethread(timeout_helper, (void *)tip);
			if (err == 0)
				return;
		}
	}
	(*func)(arg);
}

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

void retry(void *arg)
{
	pthread_mutex_lock(&mutex);
	/* perform retry steps ... */
	pthread_mutex_unlock(&mutex);
}

int main(void)
{
	int err, condition, arg;
	struct timespec when;

	if ((err = pthread_mutexattr_init(&attr)) != 0)
	{
		perror("pthread_mutexattr_init failed");
		exit(EXIT_FAILURE);
	}
	if ((err = pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE)) != 0)
	{
		perror("cannot set recursive type");
		exit(EXIT_FAILURE);
	}
	if ((err = pthread_mutex_init(&mutex, &attr)) != 0)
	{
		perror("catnot create recursive mutex");
		exit(EXIT_FAILURE);
	}
	/* ... */
	pthread_mutex_lock(&mutex);
	/* ... */
	if (condition)
	{
		/* calculate traget time "when" */
		timeout(&when, retry, (void *)arg);
	}
	/* ... */
	pthread_mutex_unlock(&mutex);
	/* ... */
	exit(EXIT_SUCCESS);
}
        如果不能创建线程,或者安排函数运行的时间已过,问题就出现了。在这种情况下,要从当前环境中调用之前请求运行的函数,因为函数要获取的锁和现在占有的锁是同一个,除非该锁是递归的,否则就会出现死锁。这里的makethread函数以分离状态创建线程,希望函数在未来的某个时间运行,而且不希望一直等待线程结束。

        可以调用sleep等待超时到达,但它提供的时间粒度是秒级的,如果希望等待的时间不是整数秒,需要用nanosleep(2)函数,它提供了类似的功能。

        timeout的调用者需要占有互斥量来检查条件,并且把retry函数安排为原子操作。retry函数试图对同一个互斥量进行加锁,因此,除非互斥量是递归的,否则如果timeout函数直接调用retry就会导致死锁。

2.2.2  读写锁属性

        读写锁与互斥量相似,也具有属性。用pthread_rwlockattr_init初始化pthread_rwlockattr_t结构,用pthread_rwlockattr_destroy回收结构。

#include <pthread.h>

int pthread_rwlockatttr_init (pthread_rwlockattr_t *attr);

int pthread_rwlockattr_destroy (pthread_rwlockattr_t *attr);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        读写锁支持的唯一属性是进程共享属性,该属性与互斥量的进程共享属性相同。就像互斥量的进程共享属性一样,用一对函数来读取和设置读写锁的进程共享属性。

#include <pthread.h>

int pthread_rwlockattr_getpshared (const pthread_rwlockattr_t *restrict attr, int *restrict pshared);

int pthread_rwlockattr_setpshared (pthread_rwlockattr_t *attr, int pshared);

        两者的返回值都是:若成功返回0,否则返回错误编号。

        虽然POSIX只定义了一个读写锁属性,但不同平台的实现可以自由地定义额外的、非标准的属性。

2.2.3  条件变量属性

        条件变量也有属性。与互斥量和读写锁类似,有一对函数用于初始化和回收条件变量属性。

#include <pthread.h>

int pthread_condattr_init (pthread_condattr_t *attr);

int pthread_condattr_destroy (pthread_condattr_t *attr);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

        与其他的同步原语一样,条件变量支持进程共享属性。

#incldue <pthread.h>

int pthread_condattr_getpshared (const pthread_condattr_t *restrict attr, int *restrict pshared);

int pthread_condattr_setpshared (pthread_condattr_t *attr, int pshared);

        两者的返回值都是:若成功则返回0,否则返回错误编号。

2.3  重入

        在遇到重入问题时线程与信号处理程序类似。有了信号处理程序和线程,多个控制线程在同一时间可能潜在地调用同一个函数。

        如果一个函数在同一时刻可以被多个线程安全地调用,就称该函数是线程安全的。在Single UNIX Specification中定义的所有函数,除了下图列出的函数以外,其他函数都保证是线程安全的。另外,ctermid和tmpnam函数在参数传入空指针时并不能保证线程是安全的。类似地,wcrtombhewcsrtombs函数如果参数mbstate_t传入的是空指针的话,也不能保证它们是线程安全的。

        支持线程安全函数的操作系统实现会在<unistd.h>中定义符号_POSIX_THREAD_SAFE_FUNCTIONS。应用程序可以在sysconf函数中传入_SC_THREAD_SAFE_FUNCTIONS参数,以在运行时检查是否支持线程安全函数。所有遵循XSI的实现要求必须支持线程安全函数。

        操作系统实现支持线程安全函数这一特性时,对POSIX.1中的一些非线程安全函数,它会提供可替换的线程安全版本,下图列出了这些函数的线程安全版本。很多函数并不是线程安全的,因为他们返回的数据是存放在静态的内存缓冲区中,通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全的。

        上图列出的函数的命名方式与它们的非线程安全版本的名字相似,只不过在名字最后加了_r,以表明这个版本是可重入的。

        如果一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理程序来说该函数也是可重入的。如果函数对异步信号处理程序的重入是安全的,那么就可以说函数是异步-信号处理安全的。

        除了图中列出的函数,POSIX.1还提供了以线程安全的方式管理FILE对象的方法。可以使用flockfile和ftrylockfile获取与给定FILE对象关联的锁。这个锁是递归的,当占有这把锁的时侯,还可以再次获取该锁,这并不会导致死锁。虽然这种锁的具体实现并不规定,但要求所有操作FILE对象的标准I/O例程表现得就像它们内部调用了flockfile和funlockfile一样。

#include <stdio.h>

int ftrylockfile (FILE *fp);

        返回值:若成功则返回0,否则返回非0值。

void flockfile (FILE *fp);

void funlockfile (FILE *fp);

        虽然标准的I/O例程从它们各自的内部数据结构这一角度出发,可能是以线程安全的方式实现的,但有时把锁开放给应用程序仍然是非常有用的。这允许应用程序把多个对标准I/O函数的调用组合诚原子序列。当然,在处理多个FILE对象时,需要注意可能出现的死锁,并且需要对所有的锁仔细地排序。

        如果标准I/O例程都获取它们各自的锁,那么在做一次一个字符的I/O操作时性能就会出现严重的下降。在这种情况下,需要对每一个字符的读或写操作进行获取所和释放锁的动作。为了避免这种开销,出现了不加锁版本的基于字符的标准I/O例程。

#include <stdio.h>

int getchar_unlocked (void);

int getc_unlocked (FILE *fp);

        两者的返回值都是:若成功则返回下一个字符,若已到达文件结尾或出错则返回EOF。

int putchar_unlocked (int c);

int putc_unlocked (int c, FILE *fp);

        两者的返回值都是:若成功则返回c,若出错则返回EOF。

        除非被flockfile(或ftrylockfile)和funlockfile的调用包围,否则尽量不要调用这四个函数,因为它们会导致不可预期的结果(即由多个控制线程非同步地访问数据所引起的种种问题)。

        一旦对FILE对象进行加锁,就可以在释放锁之前对这些函数进行多次调用。这样就可以在多次的数据读写上分摊总的加解锁的开销。

        接下来看一段getenv的可能的实现。因为所有调用getenv的线程返回的字符串都存放在同一个静态缓冲区中,所以这个版本不是可重入的。如果两个线程同时调用这个函数,就会看到不一致的结果。

#include <limits.h>
#include <string.h>

static char envbuf[ARG_MAX];
extern char **environ;

char *getenv(const char *__name)
{
    int i, len;
    
    len = strlen(name);
    for (i = 0; environ[i] != NULL; i++)
    {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
        {
            strcpy(envbuf, &environ[i][len + 1]);
            return (envbuf);
        }
    }
    return (NULL);
}
        接下来给出一份genenv的可重入版本,这个版本命名为getenv_r。它使用pthread_once函数(下一节会描述)来确保每个进程只调用一次thread_init函数。

#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>

extern char **environ;

pthread_mutex_t env_mutex;
static pthread_once init_done = PTHREAD_ONCE_INIT;

static void thread_init(void)
{
    pthread_mutexattr_t attr;
    
    pthread_mutexattr_init(&attr);
    pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init(&env_mutex, &attr);
    pthread_mutexattr_destroy(&attr);
}

int getenv_r(const char *name, char *buf, int buflen)
{
    int i, len, olen;
    
    pthread_once(&init_done, thread_init);
    len = strlen(name);
    pthread_mutex_lock(*env_mutex);
    for (i = 0; environ[i] != NULL; i++)
    {
        if ((strncmp(name, environ[i], len) == 0) && (environ[i][len] == '='))
        {
            olen = strlen(&environ[i][len + 1]);
            if (olen >= buflen)
            {
                pthread_mutex_unlock(&env_mutex);
                return (ENOSPC);
            }
            strcpy(buf, &environ[i][len + 1]);
            pthread_mutex_unlock(&env_mutex);
            return (0);
        }
    }
    pthread_mutex_unlock(&env_mutex);
    return (ENOENT);
}
        要使getenv_r可重入,需改变接口,调用者必须自己提供缓冲区,这样每个线程可以使用各自不同的缓冲区从而避免其他线程的干扰。但是注意这还不足以使getenv_r成为线程安全的,要使getenv_r成为线程安全的,需要在搜索请求的字符串时保护环境不被修改。我们可以使用互斥量,通过getenv_r和putenv函数对环境列表的访问进行序列化。

        可以使用读写锁,从而允许对getenv_r的多次并发访问,但并发性的增强可能并不会在很大程度上改善程序的性能。这里面有两个原因:首先,环境列表通常不会很长,所以扫描列表时不需要长时间地占有互斥量;其次,对getenv和putenv的调用不是频繁发生的,所以改善它们的性能并不会对程序的整体性能产生很大的影响。

        即使把getenv_r变成线程安全的,也并不意味着它对信号处理程序是可重入的。如果使用的是非递归的互斥量,当线程从信号处理程序中调用getenv_r时,就有可能出现死锁。如果信号处理程序在线程执行getenv_r时中断了该线程,由于这时已经占有加锁的env_mutex,这样其他线程试图对这个互斥量的加锁就会被阻塞,最终导致线程进入死锁状态。所以,必须使用递归互斥量阻止其他线程改变当前正在查看的数据结构,同时还要阻止来自信号处理程序的死锁。问题是pthread函数并不保证是异步信号安全的,所以不能把pthread函数用于其他函数,让该函数称为异步信号安全的。

2.4  线程私有数据

        线程私有数据(也称线程特定数据)是存储和查询与某个线程相关的数据的一种机制。把这种数据称为线程私有数据或线程特定数据的原因是,希望每个线程可以独立地访问数据副本,而不需要担心与其他线程的同步访问问题。

        线程模型促进了进程中数据和属性的共享,许多人在设计线程模型时会遇到各种麻烦。但在这样的模型中,为什么还需要提出一些合适的用于阻止共享的接口呢?其中有两个原因:

        第一,有时候需要维护基于每个线程的数据。因为线程ID并不能保证是小而连续的整数,所以不能简单地分配一个线程数据数组,用线程ID作为数组的索引。即使线程ID确实是小而连续的整数,可能还希望有一些额外的保护,以防止某个线程的数据与其他线程的数据相混淆。

        第二,它提供了让基于进程的接口适应多线程环境的机制。一个很明显的实例就是errno。线程出现以前的接口把errno定义为进程环境中全局可访问的整数。系统调用和库例程在调用或执行失败时设置errno,把它作为操作失败时的附属结果。为了让线程也能够使用那些原本基于进程的系统调用和库例程,errno被重新定义为线程私有数据。这样,一个线程做了设置errno的操作并不会影响进程中其他线程的errno值。

        进程中的所有线程都可以访问进程的整个地址空间。除了使用寄存器以外,线程没有办法阻止其他线程访问它的数据,线程私有数据也不例外。虽然底层的实现部分并不能阻止这种访问能力,但管理线程私有数据的函数可以提高线程间的数据独立性。

        在分配线程私有数据之前,需要创建与该数据关联的键。这个键将用于获取对线程私有数据的访问权。使用pthread_key_create创建一个键。

#include <pthread.h>

int pthread_key_create (pthread_key_t *keyp, void (*destructor)(void *));

        返回值:若成功则返回0,否则返回错误编号。

        创建的键存放在keyp指向的内存单元,这个键可以被进程中的所有线程使用,但每个线程把这个键与不同的线程私有数据地址进行关联。创建新键时,每个线程的数据地址设为null值。

        除了创建键以外,pthread_key_create可以选择为该键关联析构函数,当线程退出时,如果数据地址已经被置为非null数值,那么析构函数就会被调用,它唯一的参数就是该数据地址。如果传入的destructor参数为null,就表明没有析构函数与键关联。当线程调用pthread_exit或者线程执行返回,正常退出时,析构函数就会被调用,但如果线程调用了exit、_exit、_Exit、abort或出现其他非正常的退出时,就不会调用析构函数。

        线程通常使用malloc为线程私有数据分配内存空间,析构函数通常释放已分配的内存。如果线程没有释放内存就退出了,那么这块内存将会丢失,即线程所属进程出现了内存泄漏。

        线程可以为线程私有数据分配多个键,每个键都可以有一个析构函数与它关联。各个键的析构函数可以互不相同,当然它们也可以使用相同的析构函数。每个操作系统在实现的时侯可以对进程可分配的键的数量进行限制。

        线程退出时,线程私有数据的析构函数将按照操作系统实现中定义的顺序被调用。析构函数可能会调用另一个函数,该函数可能会创建新的线程私有数据而且把这个数据与当前的键关联起来。当所有的析构函数都调用完成以后,系统会检查是否还有非null的线程私有数据值与键关联,如果有的话,再次调用析构函数。这个过程将会一直重复直到线程所有的键都为null值线程私有数据,或者已经做了PTHREA_DESTRUCTOR_ITERATIONS中定义的最大次数的尝试。

        对所有的线程,都可以通过调用pthread_key_delete来取消键与线程私有数据值之间的关联关系。

#include <pthread.h>

int pthread_key_delete (pthread_key_t *key);

        返回值:若成功则返回0,否则返回错误编号。

        注意调用pthread_key_delete并不会激活与键关联的析构函数。要释放任何与键对应的线程私有数据值的内存空间,需要在应用程序中采取额外的步骤。

        需要确保分配的键并不会由于在初始化阶段的竞争而发生变动。下列代码可以导致两个线程都掉用pthread_key_create:

void destructor(void *);
pthead_key_t key;
int init_done = 0;

int threadfunc(void *arg)
{
    if (!init_done)
    {
        init_done = 1;
        err = pthread_key_create(&key, destructor);
    }
    ...
}

        有些线程可能看到某个键值,而其他的线程看到的可能是另一个不同的键值,这取决于系统是如何调度线程的,解决这种竞争的办法是使用pthread_once。

#include <pthread.h>

pthread_once_t initflag = PTHREAD_ONCE_INIT;

int pthread_once (pthread_once_t *initflag, void (*initfn)(void));

        返回值:若成功则返回0,否则返回错误编号。

        initflag必须是一个非本地变量(即全局变量或静态变量),而且必须初始化为PTHREAD_ONCE_INIT。

        如果每个线程都调用pthread_once,系统就能保证初始化例程initfn制备调用一次,即在系统首次调用pthread_once时。创建时避免出现竞争的一个恰当的方法可以描述如下:

void destructor(void *);
pthead_once_t init_done = PTHREAD_ONCE_INIT;

void thread_init(void)
{
    err  = pthread_key_create(&key, destructor);
}

int threadfunc(void *arg)
{
    pthread_once(&init_done, thread_init);
    ...
}
        键一旦创建,就可以通过调用pthread_setspecific函数把键和线程私有数据关联起来。可以通过pthread_getspecific函数获得线程私有数据的地址。

#include <pthread.h>

void *pthread_getspecific (pthread_key_t key);

        返回值:线程私有数据值,若没有值与键关联则返回NULL。

int pthread_setspecific (pthread_key_t key, const void *value);

        返回值:若成功则返回0,否则返回错误编号。

        如果没有线程私有数据值与键关联,pthread_getspecific将返回一个空指针。可以据此来确定是否需要调用pthread_setspecific。

        接下来演示pthread_getspecific与pthread_setspecific的使用。

#include <stdio.h>
#include <pthread.h>

pthread_key_t p_key;

void fun1()
{
	int *tmp = (int *)pthread_getspecific(p_key);
	printf("%d is running in %s\n", *tmp, __func__);
}

void *thread_fun(void *arg)
{
	pthread_setspecific(p_key, arg);

	int *tmp = (int *)pthread_getspecific(p_key);
	printf("%d is running in %s\n", *tmp, __func__);

	*tmp = (*tmp) * 100;
	fun1();

	return (void *)0;
}

int main(void)
{
	pthread_t p1, p2;
	int a = 1, b = 2;
	pthread_key_create(&p_key, NULL);
	pthread_create(&p1, NULL, thread_fun, &a);
	pthread_create(&p2, NULL, thread_fun, &b);
	pthread_join(p1, NULL);
	pthread_join(p2, NULL);

	return 0;
}
        编译并运行:

$
1 is running in thread_fun
100 is running in fun1
2 is running in thread_fun
200 is running in fun1
$

        代码较简单,自行体会。

2.5  取消选项

        有两个线程属性并没有包含在pthread_attr_t结构中,它们是可取消状态和可取消类型。这两个属性影响着线程在相应pthread_cancel函数调用时所呈现的行为。

        可取消状态属性可以是PTHREAD_CANCEL_ENABLE,也可以是PTHREAD_CANCEL_DISABLE。线程可以通过调用pthread_setcancelstate修改它的可取消状态。

#include <pthread.h>

int pthread_setcancelstate (int state, int *oldstate);

        返回值:若成功则返回0,否则返回错误编号。

        pthread_setcancelstate把当前的可取消状态设置为state,把原来的可取消状态存放在由oldstate指向的内存单元中,这两步是原子操作。

        pthread_cancel调用并不等待线程终止,在默认情况下,线程在取消请求发出以后还是继续运行,直到线程到达某个取消点。取消点是线程检查是否被取消并按照请求进行动作的一个位置。POSIX.1保证在线程调用下图列出的任何函数时,取消点都会出现。

        线程启动时默认的可取消状态是PTHREAD_CANCEL_ENABLE。当状态设为PTHREAD_CANCEL_DISABLE时,对pthread_cancel的调用并不会杀死线程;相反,取消请求对这个线程来说处于未决状态。当取消状态再次变为PTHREAD_CANCEL_ENABLE时,线程将在下一个取消点上对所有未决的取消请求进行处理。

        除了上图中列出的函数,POSIX.1还指定了下图中列出的函数作为可选的取消点。

        如果应用程序在很长一段时间内都不会调用到上面两图中的函数(例如计算数学领域的应用程序),那么可以调用pthread_testcancel函数在程序中自己添加取消点。

#include <pthread.h>

void pthread_testcancel (void);

        调用pthread_testcancel时,如果有某个取消请求正处于未决状态,而且取消并没有置为无效,那么线程就会被取消。但是如果取消被置为无效时,pthread_testcancel调用就没有任何效果。

        这里所描述的默认取消类型也称为延迟取消。调用pthread_cancel以后,在线程到达取消点之前,并不会出现真正的取消。可以通过调用pthread_setcanceltype来修改取消类型。

#include <pthread.h>

int pthread_setcanceltype (int type, int *oldtype);

        返回值:若成功则返回0,否则返回错误编号。

        type参数可以是PTHREAD_CANCEL_DEFERRED,也可以是PTHREAD_CANCEL_ASYNCHRONOUS,pthread_setcanceltype函数把取消类型设置为type,把原来的取消类型返回到oldtype指向的整型单元。

        异步取消与延迟取消不同,使用异步取消时,线程可以在任意时间取消,而不是非得遇到取消点才能被取消。

2.6  线程和信号

        即使是在基于进程的编程模式中,信号的处理也可能是很复杂的。把线程引入编程范型,就使信号的处理变得更加复杂。

        每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有线程共享的。这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为以后,所有的线程都必须共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他的线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。

        进程中的信号是递送到单个线程的。如果信号与硬件故障或计时器超时相关,该信号就被发送到引起该事件的线程中取,而其他的信号则被发送到任意一个线程。

        线程中使用pthread_sigmask来阻止信号发送。

#include <pthread.h>

int pthread_sigmask (int how, const sigset_t *restrict set, sigset_t *restrict oset);

        返回值:若成功则返回0,否则返回错误编号。

        pthread_sigmask函数与sigprocmask函数基本相同,除了pthread_sigmask工作在线程中,并且失败时返回错误码,而不向sigprocmask中那样设置errno并返回-1。

        线程可以通过调用sigwait等待一个或多个信号发生。

#include <pthread.h>

int sigwait (const sigset_t *restrict set, int *restrict signop);

        返回值:若成功则返回0,否则返回错误编号。

        set参数指出了线程等待的信号集,signop指向的整数将作为返回值,表明发送信号的数量。

        如果信号集中的某个信号在sigwait调用的时侯处于未决状态,那么sigwait将无阻塞地返回,在返回之前,sigwait将从进程中移除那些处于未决状态的信号。萎了避免错误动作发生,线程在调用sigwait之前,必须阻塞那些它正在等待的信号。sigwait函数会自动取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。如果信号在sigwait调用的时侯没有被阻塞,在完成对sigwait调用之前会出现一个时间窗,在这个窗口期,某个信号可能在线程完成sigwait调用之前就被递送了。

        要把信号发送到进程,可以调用kill;要把信号发送到线程,可以调用pthread_kill。

#include <signal.h>

int pthread_kill (pthread_t thread, int signo);

        返回值:若成功则返回0,否则返回错误编号。

        可以传一个0值的signo来检查线程是否存在。如果信号的默认处理动作是终止该进程,那么把信号的传递给某个线程仍然会杀掉整个进程。

        注意闹钟定时器是进程资源,并且所有的线程共享相同的alarm。所以进程中的多个线程不可能互不干扰(或互不合作)地使用闹钟定时器。

        接下来演示同步信号处理。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>

int quitflag;
sigset_t mask;

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t wait = PTHREAD_COND_INITIALIZER;

void *thr_fn(void *arg)
{
	int err, signo;

	for (;;)
	{
		err = sigwait(&mask, &signo);
		if (err != 0)
		{
			perror("sigwait failed");
			exit(1);
		}

		switch (signo)
		{
			case SIGINT:
				printf("\ninterrupt\n");
				break;
			case SIGQUIT:
				pthread_mutex_lock(&lock);
				quitflag = 1;
				pthread_mutex_unlock(&lock);
				pthread_cond_signal(&wait);
				return 0;
			default:
				printf("unexpected signal %d\n", signo);
				exit(1);
		}
	}
}

int main(void)
{
	int err;
	sigset_t oldmask;
	pthread_t tid;

	sigemptyset(&mask);
	sigaddset(&mask, SIGINT);
	sigaddset(&mask, SIGQUIT);
	if ((err = pthread_sigmask(SIG_BLOCK, &mask, &oldmask)) != 0)
	{
		perror("SIG_BLOCK error");
		exit(1);
	}

	err = pthread_create(&tid, NULL, thr_fn, 0);
	if (err != 0)
	{
		perror("pthread_create failed");
		exit(1);
	}

	pthread_mutex_lock(&lock);
	while (quitflag == 0)
	{
		printf("...\n");
		pthread_cond_wait(&wait, &lock);
	}
	pthread_mutex_unlock(&lock);

	quitflag = 0;

	if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
	{
		perror("SIG_SETMAKS error");
		exit(1);
	}
	exit(0);
}
        编译并运行:

$
...
^C
interrupt
^C
interrupt
^\ $

        这里不让信号处理程序中断主控线程,而是由专门的独立控制线程进行信号处理。改动quitflag的值是在互斥量的保护下进行的,这样主控线程不会在调用pthread_cond_signal时错失唤醒调用。在主控线程中使用相同的互斥量来检查标志的值,并且原子地释放互斥量,等待条件的发生。

        注意在主线程开始时阻塞SIGINT和SIGQUIT。当创建线程进行信号处理时,新键线程继承了现有的信号屏蔽字。因为sigwait会解除信号的阻塞状态,所以只有一个线程可以用于信号的接收。这使得对主线程进行编码时不必担心来自这些信号的中断。

2.7  线程和fork

        当线程调用fork时,就为子进程创建了整个进程地址空间的副本。

        子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含多个线程,子进程在fork返回以后,如果紧接着不是马上调用exec的话,就需要清理锁状态。

        在子进程内部只存在一个线程,它是由父进程中调用fork的线程的副本构成的。如果父进程中的线程占有锁,子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法直到它占有了哪些锁并且需要释放哪些锁。

        如果子进程从fork返回以后马上调用某个exec函数,就可以避免这样的问题。这种情况下,老的地址空间被丢弃,所以锁的状态无关紧要。但如果子进程需要继续做处理工作的话,这种方法就行不通,还需要使用其他的策略。

        要清除锁的状态,可以通过调用pthread_atfork函数建立fork处理程序。

#include <pthread.h>

int pthread_atfork (void (*prepare)(void), void (*parent)(void), void (*child)(void));

        返回值:若成功则返回0,否则返回错误编号。

        用pthread_atfork函数最多可以安装三个帮助清理锁的函数。prepare fork处理程序由父进程在fork创建子进程前调用,这个fork处理程序的任务是获取父进程定义的所有锁。parent fork处理程序是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的,这个fork处理程序的任务是对prepare fork处理程序获得的所有锁进行解锁。child fork处理程序在fork返回之前在子进程环境中调用,与parent fork处理程序一样,child fork处理程序也必须释放prepare fork处理程序获得的所有锁。

        注意不会出现加锁一次解锁两次的情况,虽然看起来也许会出现。当子进程地址空间创建的时侯,它得到了父进程定义的所有锁的副本。因为prepare fork处理程序获取所有的锁,父进程中的内存和子进程中的内存内容在开始的时侯是相同的。当父进程和子进程对他们的锁的副本进行解锁的时侯,新的内存是分配给子进程的,父进程的内存内容被复制到子进程的内存中(写时复制),所以就会陷入这样的假象,看起来父进程对它所有的锁的副本进行了加锁,子进程对它所有的锁的副本进行了加锁。父进程和子进程对在不同内存位置的重复的锁都进行了解锁操作,就好像出现了下列的事件序列:

(1)父进程获得所有的锁。

(2)子进程获得所有的锁。

(3)父进程释放它的锁。

(4)子进程释放它的锁。

        可以多次调用pthread_atfork函数从而设置多套fork处理程序。如果不需要使用其中某个处理程序,可以给特定的处理程序参数传入空指针,这样它就不会起任何作用。使用多个fork处理程序时,处理程序的调用顺序并不相同。parent和child fork处理程序是以它们注册时的顺序进行调用的,而prepare fork处理程序的调用顺序与它们注册时的顺序相反。这样可以允许多个模块注册它们自己的fork处理程序,并且保持锁的层次。

        例如,假设模块A调用模块B中的函数,而且每个模块有自己的一套锁。如果锁的层次是A在B之前,模块B必须在模块A之前设置fork处理程序。当父进程调用fork时,就会执行一下的步骤,假设子进程在父进程之前运行。

(1)调用模块A的prepare fork处理程序获取模块A的所有锁。

(2)调用模块B的prepare fork处理程序获取模块B的所有锁。

(3)创建子进程。

(4)调用模块B中的child fork处理程序释放子进程中模块B的所有锁。

(5)调用模块A中的child fork处理程序释放子进程中模块A的所有锁。

(6)fork函数返回到子进程。

(7)调用模块B中的parent fork处理程序释放父进程中模块B的所有锁。

(8)调用模块A中的parent fork处理程序释放父进程中模块A的所有锁。

(9)fork函数返回到父进程。

        如果fork处理程序是为了清理锁状态,那么又由谁来负责清理条件变量的状态呢?在有些操作系统的实现中,条件变量可能并不需要做任何清理。但是有些操作系统实现把锁作为条件变量的一部分,这种情况下的条件变量就需要清理。问题是目前不存在这样的接口允许做锁的清理工作,如果锁是嵌入到条件变量的数据结构中的,那么在调用fork之后就不能使用条件变量,因为还没有可移植的方法对锁进行状态清理。另外,如果操作系统的实现是使用全局锁保护进程中所有的条件变量数据结构,那么操作系统实现本身可以在fork库例程中做清理锁的工作,但是应用程序不应该依赖操作系统实现中这样的细节。

        接下来演示如何使用pthread_atfork和fork处理程序。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t lock1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t lock2 = PTHREAD_MUTEX_INITIALIZER;

void prepare(void)
{
	printf("preparing locks...\n");
	pthread_mutex_lock(&lock1);
	pthread_mutex_lock(&lock2);
}

void parent(void)
{
	printf("parent unlocking locks...\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void child(void)
{
	printf("child unlocking locks...\n");
	pthread_mutex_unlock(&lock1);
	pthread_mutex_unlock(&lock2);
}

void *thr_fn(void *arg)
{
	printf("thread started...\n");
	pause();
	return (0);
}

int main(void)
{
	int err;
	pid_t pid;
	pthread_t tid;

#if defined(BSD) || defined(MACOS)
	printf("pthread_atfork is unsupported\n");
#else
	if ((err = pthread_atfork(prepare, parent, child)) != 0)
	{
		perror("cannot install fork handlers");
		exit(1);
	}
	err = pthread_create(&tid, NULL, thr_fn, 0);
	if (err != 0)
	{
		perror("pthread_create failed");
		exit(1);
	}
	sleep(2);
	printf("parent about to fork...\n");
	if ((pid = fork()) < 0)
	{
		perror("fork failed");
		exit(1);
	}
	else if (pid == 0)
	{
		printf("child returned from fork\n");
	}
	else
	{
		printf("parent returned from fork\n");
	}
#endif
	exit(0);
}
        编译并运行:

$
thread started...

<等待2秒>
parent about to fork...
preparing locks...
parent unlocking locks...
parent returned from fork
child unlocking locks...
child returned from fork
$

        程序定义了两个互斥量,lock1和lock2,prepare fork处理程序获取这两把锁,child fork处理程序在子进程环境中释放锁,parent fork处理程序在父进程中释放锁。

        可以看出,prepare fork处理程序在调用fork以后运行,child fork处理程序在fork调用返回到子程序之前运行,parent fork处理程序在fork调用返回到父进程之前运行。

2.8  线程和I/O

        考虑两个线程,在同一时间对同一个文件描述符进行读写操作。

线程A 线程B
lseek(fd, 300, SEEK_SET); lseek(fd, 700, SEEK_SET);
read(fd, buf1, 100); read(fd, buf2, 100);
        如果线程A执行lseek,然后线程B在线程A调用read之前调用lseek,那么两个线程最终会读取同一条记录。很显然这不是我们希望的。

        为了解决这个问题,可以使用pread,使偏移量的设定和数据的读取称为一个原子操作。

线程A 线程B
pread(fd, buf1, 100, 300); pread(fd, buf2, 100, 700);
        使用pread可以确保线程A读取偏移量为300的记录,而线程B读取偏移量为700的记录。可以使用pwrite来解决并发线程对同一文件进行写操作的问题。

#include <unistd.h>

ssize_t pread (int filedes, void *buf, size_t nbytes, off_t offset);

        返回值:读到的字节数,若已到文件结尾则返回0,若出错则返回-1。

ssize_t pwrite (int filedes, const void *buf, size_t nbytes, off_t offset);

        返回值:若成功则返回已写的字节数,若出错则返回-1。

        调用pread相当于顺序调用lseek和read,但是pread又与这种顺序调用有下列重要区别:

  • 调用pread时,无法中断其定位和读操作。
  • 不更新文件指针。

        调用pwrite相当于顺序调用lseek和write,但也与它们有类似的区别。


——整理自《UNIX环境高级编程(第二版)》

猜你喜欢

转载自blog.csdn.net/regandu/article/details/50196469
今日推荐