匆匆了结了第十章,APUE的水确实挺深的,也许我应该几种书结合者看。开始第十一章的内容,刚刚大概翻了翻,线程开始的部分还不是很困难。
11.2 线程的概念
其实书中并没有明确的定义线程的概念,不过有关于线程的概念,我之前也分析过一段:http://blog.csdn.net/u012927281/article/details/51602898
由于Linux对于线程的实现也经历过几个版本的变化,所以我的这一篇文章可能也有疏漏的地方,欢迎大家对其中不对的地方予以指出
11.3 线程标识
与进程ID不同,线程ID是仅在它所属的进程上下文中才有意义。
pthread标准给出了一个函数用于比较两个线程的ID是否相等,函数原型如下:
#include <pthread.h>
extern int pthread_equal (pthread_t __thread1, pthread_t __thread2)
__THROW __attribute__ ((__const__));
通过以下函数可以获得线程自身的ID
#include <pthread.h>
extern pthread_t pthread_self (void) __THROW __attribute__ ((__const__));
11.4 线程创建
在创建多个控制线程以前,程序的行为与传统的进程并没有什么区别。之前大概看了这样一篇文章,有点体会,有机会分享给大家。
新增的线程可以通过调用pthread_create函数。函数原型如下:
<pre name="code" class="cpp">#include <pthread.h>
extern int pthread_create (pthread_t *__restrict __newthread, const pthread_attr_t *__restrict __attr, void *(*__start_routine) (void *), void *__restrict __arg) __THROWNL __nonnull ((1, 3));
当pthread_create返回时,新创建线程的线程ID会被设置成“__newthread”指向的内存单元。但要注意的一点是子线程不能直接访问“__newthread”的值,因为主线程与子线程的调度顺序不可知,所以可能子线程先调度而主线程后调度,造成“__newthread”的值并不是想要得到的结果。新创建的线程从“__start_routine”函数的地址开始运行,如果需要向该函数传递一个以上的参数,则需要将这些参数放到一个结构中,然后将这个结构的地址作为“__arg”参数传入。
书中给出了一个实例,我也来实验一下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
pthread_t ntid;
void printids(const char* s)
{
pid_t pid;
pthread_t tid;
pid = getpid();
tid = pthread_self();
printf("%s pid %lu tid %ld (0x%lx)\n",s,(unsigned long)pid,(unsigned long)tid,(unsigned long)tid);
}
void* thr_fn(void* arg)
{
printids("new thread: ");
return ((void*)0);
}
int main()
{
int err;
err = pthread_create(&ntid,NULL,thr_fn,NULL);
if(err!=0) perror("error exist");
printids("main thread: ");
sleep(1);
exit(0);
}
一字不落的敲了一遍。
编译命令如下:
gcc -o test_11_2 test_11_2.c -lpthread
运行结果如下:
./test_11_2
main thread: pid 2142 tid -1218562304 (0xb75e3700)
new thread: pid 2142 tid -1218565312 (0xb75e2b40)
书中还提到了一点:主线程还需要休眠,若主线程不休眠,它就可能退出,如此新线程可能还未被调度,整个进程可能就已经终止。
所以将sleep注释掉再实验一下,运行结果如下,真的出现了子线程没有执行的情况。
main thread: pid 2229 tid -1218578688 (0xb75df700)
此处子线程退出的原因我推测是主线程的退出先于子进程被调度,主线程退出后可能向所有子线程发送信号,因此子线程就得不到执行了。
11.5 线程终止
如果进程的任一线程调用exit、_exit或_Exit函数,那么整个进程就会终止。注意此处是整个进程都终止了。
单个线程可以通过3种方式退出,此处是仅退出线程,而不会终止整个进程。
- 线程可以简单地从启动历程中返回,返回值是线程的退出码。
- 线程可以被同一进程中的其他线程取消。
- 线程调用pthread_exit。
pthread_exit函数原型如下:
#include <pthread>
extern void pthread_exit (void *__retval) __attribute__ ((__noreturn__));
"__retval"参数是一个无类型指针,与传送给启动例程中的单个参数相类似,可作为启动例程的返回值。进程中的其他线程可以调用pthread_join等待特定的线程终止(通过上面介绍的三种方法),pthread_join函数原型如下:
<pre name="code" class="cpp">#include <pthread>
extern int pthread_join (pthread_t __th, void **__thread_return);
如果线程简单地从它的启动例程返回,“__retval”就包含返回码;如果线程被取消,由“__retval”指定的内存单元就被设置为“PTHREAD_CANCELED”。
通过一个实例对上面的内容进行一下简单的验证,源码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void* thr_fn1(void* arg)
{
printf("thread 1 returning\n");
return ((void*)1);
}
void* thr_fn2(void* arg)
{
printf("thread 2 exiting\n");
pthread_exit((void*)2);
}
int main()
{
int err;
pthread_t ptd1,ptd2;
void* tret;
err = pthread_create(&ptd1,NULL,thr_fn1,NULL);
if(err!=0) perror("create thread1 error");
err = pthread_create(&ptd2,NULL,thr_fn2,NULL);
if(err!=0) perror("create thread2 error");
err = pthread_join(ptd1,&tret);
if(err!=0) perror("join thread1 error");
printf("thread1 exit code %ld\n",(long)tret);
err = pthread_join(ptd2,&tret);
if(err!=0) perror("join thread2 error");
printf("thread2 exit code %ld\n",(long)tret);
exit(0);
}
运行结果如下(与书中给出的结果稍有不同)。从以下的运行结果可以得到结论,
./test_11_3
thread 1 returning
thread1 exit code 1
thread 2 exiting
thread2 exit code 2
在敲代码的过程中发现了一点觉得比较怪异的地方
void* tret;
直接申请了一个void指针用于存储数据,虽然没有为void指针指向的内存申请指针,但void指针本身具有地址,pthread_join的第二个参数是“void** ”类型,因此将tret的地址赋给第二个参数,程序最后的运行结果可以发现返回码直接放置在tret所在的地址空间中。
这里有一点使用上的问题要留心,pthread_create、pthread_exit函数中使用的void型指针参数所使用的内存在调用者完成调用以后必须是仍然有效的,所以为了解决这个问题,可以使用全局结构,或用malloc函数分配结构。书中也给出了一个实例,实验一下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
struct foo{
int a,b,c,d;
};
void printfoo(const char* s,const struct foo* fp)
{
printf("%s",s);
printf("struct at 0x%lx\n",(unsigned long)fp);
printf(" foo.a = %d\n",fp->a);
printf(" foo.b = %d\n",fp->b);
printf(" foo.c = %d\n",fp->c);
printf(" foo.d = %d\n",fp->d);
}
void* thr_fn1(void* arg)
{
struct foo foo={1,2,3,4};
printfoo("thread1 :\n",&foo);
pthread_exit((void*)&foo);
}
void* thr_fn2(void* arg)
{
printf("thread2 ID is %lu\n",(unsigned long)pthread_self());
pthread_exit((void*)0);
}
int main()
{
int err;
pthread_t ptd1,ptd2;
struct foo* fp;
err = pthread_create(&ptd1,NULL,thr_fn1,NULL);
if(err!=0) perror("create thread1 error");
err = pthread_join(ptd1,(void**)&fp);
if(err!=0) perror("join thread1 error");
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&ptd2,NULL,thr_fn2,NULL);
if(err!=0) perror("create thread2 error");
sleep(1);
printfoo("main :\n",fp);
exit(0);
}
又是原封不动的敲上去,实验结果如下:
thread1 :
struct at 0xb757834c
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread2 ID is 3075967808
main :
struct at 0xb757834c
foo.a = -1217649422
foo.b = 0
foo.c = -1218999488
foo.d = -1219001304
线程可以通过调用pthread_cancel函数来请求取消同一进程中的其他线程。函数原型如下:
extern int pthread_cancel (pthread_t __th);
注意pthread_cancel并不等待线程终止,它仅仅提出请求,被取消线程可以选择如何相应这一请求。
线程同样可以安装“线程清理处理程序”,处理程序的调用顺序与安装顺序正好相反。函数原型如下:
#include <pthread.h>
pthread_cleanup_push(routine, arg)
pthread_cleanup_pop(execute)
在我的机器上,这两个函数确实是通过宏实现的,但在我的机器上这两个函数的实现有好几个,所以我也仅仅是将函数的原型贴出来,具体怎么实现的,有机会再详细研究。
以下几种情况可以调用pthread_cleanup_push注册的清理函数:
- 调用pthread_exit函数。
- 响应取消请求时
- 用非零execute参数调用pthread_cleanup_pop函数。如果execute参数被设置为0,清理函数将不被调用。
处理程序记录在栈上。不管发生上述哪种情况,pthread_cleanup_pop都将删除上次pthread_cleanup_push调用建立的清理处理函数。
书中给出了一个例子,让我们也实验一下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
void cleanup(void* arg)
{
printf("cleanup:%s \n",(char*)arg);
}
void* thr_fn1(void* arg)
{
printf("thread1 start\n");
pthread_cleanup_push(cleanup,"thread1 first handler");
pthread_cleanup_push(cleanup,"thread1 second handler");
printf("thread1 push complete\n");
if(arg) return ((void*)1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
return ((void*)1);
}
void* thr_fn2(void* arg)
{
printf("thread2 start\n");
pthread_cleanup_push(cleanup,"thread2 first handler");
pthread_cleanup_push(cleanup,"thread2 second handler");
printf("thread2 push complete\n");
if(arg) pthread_exit((void*)2);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit((void*)2);
}
int main()
{
int err;
pthread_t ptd1,ptd2;
void* iret;
err = pthread_create(&ptd1,NULL,thr_fn1,(void*)1);
if(err!=0) perror("create thread1 error");
err = pthread_create(&ptd2,NULL,thr_fn2,(void*)1);
if(err!=0) perror("create thread2 error");
err = pthread_join(ptd1,&iret);
if(err!=0) perror("join thread1 error");
printf("thread1 exit code %ld\n",(long)iret);
err = pthread_join(ptd2,&iret);
if(err!=0) perror("join thread1 error");
printf("thread1 exit code %ld\n",(long)iret);
exit(0);
}
程序运行结果如下:
./test_11_5
thread1 start
thread1 push complete
thread1 exit code 1
thread2 start
thread2 push complete
cleanup:thread2 second handler
cleanup:thread2 first handler
thread1 exit code 2
一开始程序的编写出现了一些小问题:
err = pthread_create(&ptd1,NULL,thr_fn1,NULL);
直接使用NULL参数调用thr_fn1函数,造成如下两句的调用:
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
由于使用0参数调用以上函数,所以直接删除了已经注册的清理函数,调用pthread_exit也就没有任何清理函数可以调用。
再来实验一下调用多个pthread_cleanup_pop会有什么结果?
此处直接编译错误,看来pthread_cleanup_push、pthread_cleanup_pop函数必须成对出现。
通过thread1的运行结果还可以发现:如果线程是通过从它的启动例程中返回而终止的话,它的清理处理程序就不会被调用。
线程可以通过可以调用pthread_detach函数分离线程:
extern int pthread_detach (pthread_t __th) __THROW;
如果线程已经被分离,线程的底层存储资源可以在线程终止时被立即收回。在线程被分离后,不能使用pthread_join等待它的终止状态。
11.6 线程同步
在开始学习这一章的内容之前,我们需要了解线程之间哪些数据是共享的,哪些数据是独享的,可以参考这篇blog:http://www.cnblogs.com/tracylee/archive/2012/10/29/2744228.html
11.6.1 互斥量
互斥量使用pthread_mutex_t数据类型表示,在使用互斥量以前,必须首先对其进行初始化,可以把他设置为常量“PTHREAD_MUTEX_INITIALIZER”(只适用于静态分配的互斥量),在我的机器上有两个定义:
#ifdef __PTHREAD_MUTEX_HAVE_PREV
# define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }
# endif
#else
# define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, { __PTHREAD_SPINS } } }
看来pthread_mutex_t的定义并不简单。
也可以调用函数初始化互斥量。函数如下:
extern int pthread_mutex_init (pthread_mutex_t *__mutex,
const pthread_mutexattr_t *__mutexattr)
__THROW __nonnull ((1));
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex)
__THROW __nonnull ((1));
如果动态分配互斥量(例如,通过malloc函数),再释放内存前需要调用pthread_mutex_destory。
对互斥量进行加锁,需要调用pthread_mutex_init。如果互斥量已经上锁,调用线程将阻塞直到互斥量被解锁。对互斥量解锁,需要解锁pthread_mutex_unlock。使用pthread_mutex_trylock尝试对互斥量进行加锁,如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,返回EBUSY。
extern int pthread_mutex_lock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex)
__THROWNL __nonnull ((1));
11.6.2 避免死锁
书中通过具体的方法给出了避免死锁的方法。首先给出了一个具体的解除死锁的方法:使用相同的顺序加锁。
11.6.3 pthread_mutex_timedlock
当线程试图获取一个已加锁的互斥量是,pthread_mutex_timedlock原语允许绑定线程阻塞时间。函数原型如下:
extern int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));
当到达超时时间值时,pthread_mutex_timedlock返回错误码ETIMEDOUT。
11.6.4 读写锁
读写锁可以有3种状态:读模式下加锁状态、写模式下加锁状态、不加锁状态。
可通过以下函数初始化、销毁读写锁。
extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock,
const pthread_rwlockattr_t *__restrict
__attr) __THROW __nonnull ((1));
extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock)
__THROW __nonnull ((1));
当读写锁处于写状态加锁时,在该锁被解除之前,所有试图对这个锁加锁的线程都会被阻塞。当以读模式加锁时,以读模式进行加锁的线程都可以获得访问权,而以写模式加锁的线程都会被阻塞。直到所有的线程释放它们的锁为止。虽然各操作系统对读写锁的实现不同,但当读写锁处于读模式加锁状态,而这时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
通过以下函数对读写锁进行加锁、解锁。
extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock) //获取读模式锁
__THROWNL __nonnull ((1));
extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock) //获取写模式锁
__THROWNL __nonnull ((1));
extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock) //解锁
__THROWNL __nonnull ((1));
11.6.5 带有超时的读写锁
函数原型如下:
extern int pthread_rwlock_timedrdlock (pthread_rwlock_t *__restrict __rwlock,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));
extern int pthread_rwlock_timedwrlock (pthread_rwlock_t *__restrict __rwlock,
const struct timespec *__restrict
__abstime) __THROWNL __nonnull ((1, 2));
11.6.6 条件变量
条件变量是线程可用的另一种同步机制,这是本章中提到的第三种同步方式,前两种分别是:互斥量、读写锁。
使用条件变量前还是需要初始化,初始化的方法还是分为静态与动态两种,静态是通过PTHREAD_COND_INITIALIZER进行初始化,相关定义如下:
PTHREAD_COND_INITIALIZER
动态是通过函数进行初始化:
extern int pthread_cond_init (pthread_cond_t *__restrict __cond,
const pthread_condattr_t *__restrict __cond_attr)
__THROW __nonnull ((1));
既然有初始化,就有销毁操作,函数原型如下:
extern int pthread_cond_destroy (pthread_cond_t *__cond)
__THROW __nonnull ((1));
可以使用pthread_cond_wait函数等待条件变量变为真,如果在给定的时间内条件不能满足,那么会生成一个返回错误码的变量。调用者将锁住的互斥量传给函数,函数然后自动把调用线程放到等待条件的线程列表上,对互斥量解锁。pthread_wait_cond返回时,互斥量再次被锁住。
extern int pthread_cond_wait (pthread_cond_t *__restrict __cond,
pthread_mutex_t *__restrict __mutex)
__nonnull ((1, 2));
extern int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,
pthread_mutex_t *__restrict __mutex,
const struct timespec *__restrict __abstime)
__nonnull ((1, 2, 3));
有两个函数用于通知线程条件已经满足。
pthread_cond_signal函数至少能唤醒一个等待该条件的线程。
extern int pthread_cond_signal (pthread_cond_t *__cond)
__THROWNL __nonnull ((1));
pthread_cond_broadcast函数能唤醒等待该条件的所有线程。
extern int pthread_cond_broadcast (pthread_cond_t *__cond)
__THROWNL __nonnull ((1));
11.6.7 自旋锁
自旋锁与互斥量类似,但它不是通过休眠使进程阻塞,而是在获取锁之前一直处于忙等待状态。自旋锁可用于以下情况:锁被持有的时间短。而且线程并不希望在重新调度上花费太多成本。
还是一样的做法,在使用自旋锁之前同样需要初始化,使用后需要销毁。
extern int pthread_spin_init (pthread_spinlock_t *__lock, int __pshared)
__THROW __nonnull ((1));
extern int pthread_spin_destroy (pthread_spinlock_t *__lock)
__THROW __nonnull ((1));
加锁及解锁函数如下:
extern int pthread_spin_lock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));
extern int pthread_spin_trylock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));
extern int pthread_spin_unlock (pthread_spinlock_t *__lock)
__THROWNL __nonnull ((1));
需要注意,不要调用在持有自旋锁情况下可能进入休眠状态的函数,如果调用了这些函数,会浪费CPU资源,因为其他线程需要获取自旋锁需要等待的时间就延长了。
11.6.8 屏障
屏障允许每个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。其实在MPI标准中也有一个类似的函数:MPI_Barrier。
屏障的使用也需要初始化与销毁。
extern int pthread_barrier_init (pthread_barrier_t *__restrict __barrier,
const pthread_barrierattr_t *__restrict
__attr, unsigned int __count)
__THROW __nonnull ((1));
extern int pthread_barrier_destroy (pthread_barrier_t *__barrier)
__THROW __nonnull ((1));
可以使用pthread_barrier_wait函数来表明,线程已完成工作,准备等其他所有线程赶上来。函数原型如下:
extern int pthread_barrier_wait (pthread_barrier_t *__barrier)
__THROWNL __nonnull ((1));
对于一个任意线程,pthread_barrier_wait函数返回PTHREAD_BARRIER_SERIAL_THREAD(貌似无法指定某个线程返回这个值),这个值在我的机器上是-1。剩下的线程看到的返回值是0。这使得一个线程可以作为主线程,它可以工作在其他所有线程已完成的工作结果上。