Linux多线程编程
#include <pthread.h>
基本线程函数
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*func)(void *), void *arg);
线程创建成功返回0,出错返回某个非0值,一般为-1
tid 创建成功的线程通过tid指针返回线程ID
attr 指定线程的属性
func 线程的入口函数
arg 传递给线程的参数
int pthread_join(pthread_t *tid, void **status);
tid 必须指定的要等待的线程的ID
status 如果非空,则等待线程的返回值(一个指向某个对象的指针)存入status指向的位置
pthread_t pthread_self();
线程获取自身的线程ID函数
int pthread_detach(pthread_t tid);
一个线程是joinable(默认值),或是detached,当一个joinable的线程终止时,它的线程ID
和退出状态将留存到另一个线程对它调用pthread_join。detached的线程却像守护进程,当它
们终止时,所有相关资源都被释放,detached的线程是不能被等待终止的。
void pthread_exit(void *status);
如果一个线程没有detached,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。
status指针不能指向调用线程的局部对象,因为线程终止该资源也被释放掉。
线程终止其它方式:
线程的入口函数返回,即pthread_create函数的第三个参数。
进程main函数调用exit,进程终止,当然其中所有线程就终止。
给新线程传递参数
典型错误代码:
main ()
{
int confd;
. . . . . .
while(1)
{
. . . . . .
confd = accept(listenfd, cliaddr, &len);
pthread_create(&tid, NULL, &func, &confd);
}
}
void *func(void *arg)
{
int confd;
confd = *((int *)arg);
/*处理操作*/
}
上面参数参数传递方式是错误的。我们很容易就发现如果新线程的confd = *((int *)arg);在执行前,
accept又建立的了另一个连接并返回相应描述符存入confd,那么我们的新线程得到的只能是最后一次
accept返回的值,而之前建立的连接并未得到处理。
可以通过动态分配内存然后传递一个描述符的值来解决这个问题。
main()
{
int *ptr;
. . . . . .
while(1)
{
. . . . . .
iptr = malloc(sizeof(int));
iptr* = accept(listenfd, cliaddr, &len);
pthread_create(&tid, NULL, &func, iptr);
}
}
void *func(void *arg)
{
int confd;
confd = *((int *)arg);
free(arg);
/*处理操作*/
}
线程特定数据 & 静态变量
静态变量的特点:
1.限定同一源文件的后续代码可以访问
2.无论函数是否被调用,静态变量一直存在,不会随着函数调用退出而消失
因此当在多个线程中使用同一个函数中有静态变量时,就会涉及到静态变量的共享问题。可以使用线程特定数据解决这个问题。POSIX要求每个系统支持有限数量的线程特定数据,POSIX要求这个限制不少于128(每个进程)。系统会为每个进程维护一个称之为key的数据结构数组,如图:
当一个线程调用pthread_key_create创建线程特定数据时,系统搜索进程的key结构数组,找出第一个不在使用的元素。该元素的索引(key[i]中的i)称为key,这个索引被返回给调用线程。除了进程范围的key结构数组外,系统还在进程内维护关于每一个线程的多条信息。这些特定于线程的信息我们称之为pthread结构,其中的部分内容是我们称之为pkey数组的一个128个元素的指针数组。如图:
pkey指针数组的所有元素都被初始化为NULL指针。这里的128个指针是和进程内的128个key值逐一关联的。举一个具体例子,假设我们的func函数使用线程特定数据用于维护它在每个调用它的线程中的状态。
<1>一个进程被启动,多个线程被创建
<2>其中线程0是首个调用func函数的线程,该函数转而调用pthread_key_create。系统在如图2所示 的结构数组中找到第一个为未使用的元素,并把它的索引(0-127)返回给调用线程。这里假设找到 的索引是4。我们将使用pthread_once函数确保pthread_key_create函数只是被第一个调用func函数
的线程调用。
<3>func函数调用pthread_getspecific获取本线程的pkey[4]的值,返回的是一个空指针(NULL,初 始化值)。func于是调用malloc函数分配内存,用于为本线程的func函数调用保存特定于线程的信息。
func按照需求初始化该内存区域,并调用pthread_setspecific把对应所创建key的线程特定数据指针
(pkey[4])设置指向刚刚分配的内存区域。
<4>另一个线程n调用func,当时也许线程0还在func内执行。func调用pthread_once试图初始化它的
线程特定数据元素所用的键,不过既然初始化函数在线程0时已被调用过,它就不会再调用。
<5>func调用pthread_getspecific获取本线程的pkey[4]的值,返回的是一个空指针。线程n于是就像 线程0那样先调用malloc,再调用pthread_setspecific,以初始化相应key的线程特定数据。
<6>线程n继续在func中执行,使用和修改自己的线程特定数据。
当一个线程终止时,这时可以看到已经分配的给该线程特定数据的内存空间需要我们自己释放,这就
是之前提过的key结构数组中的析构函数的用途。一个线程调用pthread_key_create创建某个线程特
定数据元素时,所指定的函数参数之一就是指向某个析构函数的指针。当一个线程终止时,系统将扫
描该线程的pkey数组,为每个非空的pkey指针调用相应的析构函数。这是一个线程释放线程特定数据
的手段。
int pthread_once(pthread_once_t *onceptr, void *(*init)(void));
int pthrea_key_create(pthread_key_t *keyptr, void (*destructor)(void *value));
:成功均返回0,出错则为正的Exxx的值。
void *pthread_getspecific(pthread_key_t key);
:返回指向线程特定数据的指针(可能为NULL)
int pthread_setspecific(pthread_key_t key, const void *value);
:成功返回0,出错则为正的Exxx值
pthread_once和 pthrea_key_create这两个函数的典型用法:
pthread_key_t key;
pthread_once_t once = PTHREAD_ONCE_INIT;
void destructor(void *ptr)
{ free(ptr); }
void func_once(void)
{ pthread_key_create(&key, destructor); }
void func(...) //your function
{
pthread_once(&once, func_once);
if ((ptr=pthread_getspecific(key)) == NULL)
{
ptr = malloc(...);
pthread_setspecific(key, ptr);
// initialize memory pointed by ptr
}
//use values pointed by ptr
}
我们可以这样理解,线程特定数据的是共享函数在每个特定线程中用到的只属于该线程私有的数据,而进程中的key结构数组则维护共享函数的线程特定数据在所有使用到该共享函数的线程中的索引。每个使用到共享函数(这里假定该函数需要线程特定数据)的线程中指向该函数的线程特定数据的索引都是相同的,假设key中维护的索引是i,则所有使用到该函数的线程中指向该函数的线程特定数据的指针都是pkey[i]。
互斥锁
互斥锁就是用来解决共享变量的同步问题。一个简单的例子就是对一个变量执行递增的过程:
1.把值A从内存装载到寄存器
2.寄存器内容A加1
3.把递增后的值送回内存
如果线程4在执行完1步的时候被操作系统调出,转而操作系统调入线程5执行,线程5所做的动作和线程4的一样,此时线程5修改了A并保存,线程5退出,恢复原来寄存器的内容,转而到线程4执行,此时寄存器还是A最初的值,执行完线程4之后,A值加1。但是我们本意是两个线程分别递增A值,即两个线程执行过后结果为A+2,但是结果却是A+1。为了达到这个目的,我们需要使用互斥锁来保护共享变量。
int pthread_mutex_lock(pthread_mutex_t *mptr);
int pthread_mutex_unlock(pthread_mutex_t *mptr);
用法:
pthread_mutex_t mutex;
. . . . . .
void func(void)
{
pthread_mutex_lock(&mutex);
//对共享变量的操作
pthread_mutex_unlock(&mutex);
}
条件变量
互斥锁适合于防止同时访问某个共享变量,而条件变量则是把线程投入睡眠直到等待的某个条件发生才会唤醒相应线程。
这里举一个计算已经终止的线程的数量的例子:
首先声明一个全局的共享变量,每一个线程在终止前都会使用互斥锁对这个变量进行递增的操作,并且设置条件变量。
int ndone = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
. . . . . .
void *thread(void *arg)
{
. . . . . .
pthread_mutex_lock(&mutex);
done++;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
若不使用条件变量,则为了测试是否有进程退出,主循环需要一直不断的上锁、判断然后作相应的操作、解锁,意味着主循环不会休眠而是一直在死循环,这样会浪费大量cpu资源在判断上。因此我们使用的是条件变量的方式,把主循环投入睡眠,等到ndone共享变量被设置了才把主循环唤醒处理。
使用条件变量的两个函数:
int pthread_cond_wait(pthread_cond_t *cptr, pthread_mutex_t *mptr);
int pthread_cond_signal(pthread_cond_t *cptr);
:成功均返回0,出错则为正的Exxx值
. . . . . .
//主循环
void *mainloop(void *arg)
{
int count=0;
again:
pthread_mutex_lock(&mutex);
while(ndone == 0)
pthread_cond_wait(&cond, &mutex);
count += ndone;
if (count == 8)
return NULL;
ndone = 0;
pthread_mutex_unlock(&mutex);
goto again;
}
为什么条件变量要关联一个互斥锁呢?因为“条件”通常是线程之间共享的某个变量。允许不同的线程设置和测试该变量要求有一个与该变量关联的互斥锁。举例来说如果刚才的主循环没有关联互斥锁,测试ndone变量的代码变为下面这样:
. . . . . .
while(ndone == 0)
pthread_cond_wait(&cond, &mutex);
如果主线程外最后一个线程在ndone==0测试通过之后和pthread_cond_wait调用之前期间设置ndone后退
出,那么最后一个线程的信号就会丢失,主循环就会永远阻塞在 pthread_cond_wait。
同样的理由要求 pthread_cond_wait被调用时其所关联的互斥锁必须是上锁,pthread_cond_wait
函数作为单个原子操作解锁该互斥锁并把调用线程投入睡眠。要是该函数不先解锁该互斥锁,到返回时
再给它上锁,调用线程就必须事先解锁事后上锁该互斥锁,测试ndone变量的代码变为下面这样:
pthread_mutex_lock(&mutex);
while(ndone == 0)
{
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond, &mutex);
pthread_mutex_lock(&mutex)
}
. . . . . .
这里同样存在可能,如果主线程外最后一个线程在ndone==0测试通过pthread_mutex_unlock解锁之后和
pthread_cond_wait调用之前期间设置ndone后退出,那么最后一个线程的信号就会丢失,主循环就会永
远阻塞在 pthread_cond_wait。
pthread_cond_signal通常唤醒等在相应条件变量上的单个线程。有时候一个线程知道自己该唤醒多个线
程,这种情况下pthread_cond_brocast唤醒等在相应条件变量上的所有线程。
int pthread_cond_brocast(pthread_cond_t *cptr);
int pthread_cond_timewait(pthread_cond_t *cptr, pthread_mutex_t *mptr,
const struct timespec *abstime);
pthread_cond_timewait允许线程设置一个阻塞时间的限制。abstime是一个timespec结构,指定该函数必须返回时刻的系统时间,即使到时候相应条件变量尚未收到信号。如果发生这样的超时,那就返回ETIME错误。
这个时间是一个绝对时间,而不是一个时间增量。也就是说abstime参数是函数应该返回时刻的系统时间—从1970年1月1日UTC时间以来的秒数和纳秒数。通常调用过程是:调用gettimeofday获取当前时间(作为一个timeval结构),把它复制到一个timespec结构中,再加上期望的限制时间。
struct timeval tv;
struct timespec ts;
if (gettimeofday(&tv, NULL) < 0)
exit(0);
ts.tv_sec = tv.tv_sec + 5; //5 seconds in the furture
ts.tv_nsec = tv.tv_usec * 1000 //microsec to nanosec
pthread_cond_timewait ( . . . , &ts);