1、引言
2、线程概念
- 关于线程和进程的区别:
https://blog.csdn.net/mu_wind/article/details/124616643 - 多CPU ,多核和进程以及多线程之间的关系:
https://blog.csdn.net/jiangxixiaolinzi/article/details/119678291 - 每个线程都包含有表示执行环境所必须的信息(即每个线程都有属于自己的以下信息,不同线程之间不共享不一致):
- 线程
ID
、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno
变量以及线程私有数据。
- 线程
- 一个进程的所有信息对该进程的所有线程都是共享的:
- 可执行程序的代码、程序的全局内存和堆内存、栈内存以及文件描述符
3、线程标识
- 每个线程都有一个线程ID。与进程ID不同,进程ID在整个系统中是唯一的,但是线程ID只有在它所属的进程上下文中才有意义。
- 线程ID通过
pthread_t
数据类型表示。对于该类型的实现不同操作系统不同,Linux是unsigned long int
,某些操作系统用一个结构体实现pthread_t
。因此不能将pthread_t
类型当做整数处理(如用数值的方式进行比较)。因此必须通过一个函数来对两个线程ID进行比较 - 通过
pthread_equal
比较两个线程IDint pthread_equal(pthread_t t1, pthread_t t2);
- 通过
pthread_self
函数获得自身线程IDpthread_t pthread_self(void);
- 关于工作队列的介绍
- 主线程可能把工作任务放在一个队列中,用线程
ID
来控制每个工作线程处理哪些作业。 - 如图所示,主线程把新的作业放到一个工作队列中,由3个工作线程组成的线程池从队列中移出作业。
- 主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配,主线程会在每个待处理作业的结构中防止该作业的线程
ID
,每个工作线程只能移出标有自己线程ID
的作业。 - 该工作队列模型会在
6.4
节中遇到,可以参阅对应的解决方式。
- 主线程可能把工作任务放在一个队列中,用线程
4、线程创建
- 通过
pthread_create
函数创建一个线程int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
thread
参数:若线程创建成功,保存该线程ID
attr
:定制各种不同的线程属性。NULL
即为默认属性start_routine
:线程从该地址开始运行,该函数接受一个void *
参数并返回一个void *
类型arg
:传递给start_routine
的参数值。如果需要向start_routine
函数传递的参数有一个以上,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg
参数传入。
- 返回值:pthread函数在调用失败是会返回错误码,而不像其他
POSIX
函数一样设置errno
- 注意,创建线程时不能保证哪个线程会先执行。新创建的线程可以访问进程的地址空间,并且继承调用线程的浮点环境和信号屏蔽字,但是该线程的挂起信号集会被清除。
- 注意,对于
pthread_create
函数的第一个参数,不能在start_routine
线程函数中使用!因为无法保证哪个线程先执行,因此如果新线程在主线程调用pthread_create
返回之前就运行了,那么新线程看到的是未经初始化的thread
成员。应该通过pthread_self
获取自身线程ID。 - 实例:创建了一个线程,打印了进程
ID
、新线程的线程ID
以及初始线程的线程ID
。
命令行输出#include "apue.h" #include <pthread.h> pthread_t ntid; void printids(const char *s) { pid_t pid; pthread_t tid; pid = getpid(); /*这里使用pthread_self来获取自己的线程ID,而不是从共享内存或者从线程的启动例程中以参数的形式接收到的。 在这个例子中,主线程把新线程ID存放在了ntid中,如果新线程在主线程调用pthread_create之前就返回了, 那么显然ntid中的内容并不是正确的线程ID*/ tid = pthread_self(); printf("%s pid %lu tid %lu (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(void) { int err; err = pthread_create(&ntid, NULL, thr_fn, NULL); if (err != 0) err_exit(err, "can't create thread"); printids("main thread:"); /*为什么主线程要休眠:创建线程时不能保证哪个线程会先执行,如果主线程不休眠,他就可能会退出, 这样新线程还没机会运行,整个进程就已经终止了*/ sleep(1); exit(0); }
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./threadid main thread: pid 3343 tid 140680582641408 (0x7ff2c027b700) new thread: pid 3343 tid 140680574322432 (0x7ff2bfa8c700)
- 可以看出:两个线程的进程ID相同,但线程ID不同
5、线程终止
-
如果进程中的任何线程调用了
exit
、_Exit
或_exit
,那么整个进程就会终止。 -
对于某个信号,如果默认的动作是终止进程,那么发送到线程的该信号就会终止整个进程。
-
单个线程可以通过以下3种方式退出,并且不导致整个进程终止:
- 线程简单地从启动例程中返回,返回值是线程的退出码
- 线程可以被同一进程中的其他线程取消
- 线程调用
pthread_exit
-
pthread_exit
void pthread_exit(void *retval);
retval
是一个void *
类型指针,即为线程退出码,进程中的其他线程可以通过pthread_join
函数访问这个指针。
-
pthread_join
int pthread_join(pthread_t thread, void **retval);
- 调用线程一直阻塞,直到指定线程退出(调用
pthread_exit
、从启动例程中返回或被取消)。retval
保存了指定线程return
值、pthread_exit
参数值、如果线程是被pthread_cancel
的则是PTHREAD_CANCELED
。 - 如果线程处于分离状态(
pthread_detach
),则pthread_join
失败,返回EINVAL
。
- 调用线程一直阻塞,直到指定线程退出(调用
-
实例:获取已终止线程的退出码。
#include "apue.h" #include <pthread.h> void * thr_fn1(void *arg) { printf("thread 1 returning\n"); /*线程退出方式1:简单地从启动例程中返回*/ return((void *)1); } void * thr_fn2(void *arg) { printf("thread 2 exiting\n"); /*线程退出方式2:线程调用pthread_exit*/ pthread_exit((void *)2); } int main(void) { int err; pthread_t tid1, tid2; void *tret; err = pthread_create(&tid1, NULL, thr_fn1, NULL); if (err != 0) err_exit(err, "can't create thread 1"); err = pthread_create(&tid2, NULL, thr_fn2, NULL); if (err != 0) err_exit(err, "can't create thread 2"); err = pthread_join(tid1, &tret); if (err != 0) err_exit(err, "can't join with thread 1"); printf("thread 1 exit code %ld\n", (long)tret); err = pthread_join(tid2, &tret); if (err != 0) err_exit(err, "can't join with thread 2"); printf("thread 2 exit code %ld\n", (long)tret); exit(0); }
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./exitstatus thread 1 returning thread 2 exiting thread 1 exit code 1 thread 2 exit code 2
- 可以看出,当一个线程通过调用
pthread_exit
退出或者简单地从启动例程中返回时,进程中的其他线程可以通过调用pthread_join
函数获得该线程的退出状态。 - 注意:
pthread_join
可以使调用线程一直阻塞,直到指定线程退出,所以不用担心同步问题。
- 可以看出,当一个线程通过调用
-
实例:给出了局部变量(分配在栈上)作为
pthread_exit
的参数时出现的问题。#include "apue.h" #include <pthread.h> struct foo { int a, b, c, d; }; void printfoo(const char *s, const struct foo *fp) { printf("%s", s); printf(" structure 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("thread 1:\n", &foo); pthread_exit((void *)&foo); } void * thr_fn2(void *arg) { printf("thread 2: ID is %lu\n", (unsigned long)pthread_self()); pthread_exit((void *)0); } int main(void) { int err; pthread_t tid1, tid2; struct foo *fp; err = pthread_create(&tid1, NULL, thr_fn1, NULL); if (err != 0) err_exit(err, "can't create thread 1"); err = pthread_join(tid1, (void *)&fp); if (err != 0) err_exit(err, "can't join with thread 1"); sleep(1); printf("parent starting second thread\n"); err = pthread_create(&tid2, NULL, thr_fn2, NULL); if (err != 0) err_exit(err, "can't create thread 2"); sleep(1); printfoo("parent:\n", fp); exit(0); }
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./badexit2 thread 1: structure at 0x7f7355e59f30 foo.a = 1 foo.b = 2 foo.c = 3 foo.d = 4 parent starting second thread thread 2: ID is 140133339080448 parent: structure at 0x7f7355e59f30 foo.a = 1445050624 foo.b = 32627 foo.c = 1442644546 foo.d = 32627
- 可以看到,当主线程访问这个结构时,结构的内容(在线程
tid1
的栈上分配的)已经改变了。注意第二个线程(tid2
)的栈是如何覆盖第一个线程的。为了解决这个问题,可以使用全局结构,或者用malloc
函数分配结构。
- 可以看到,当主线程访问这个结构时,结构的内容(在线程
-
pthread_cancel
通过pthread_cancel
函数来请求取消同一进程中的其他线程。int pthread_cancel(pthread_t thread);
- 该函数会使得
thread
标识的线程行为如同调用了pthread_exit(PTHREAD_CANCEL)
,但是线程可以选择忽略取消或者控制如何被取消。 - 注意
pthread_cancel
并不等待线程终止,仅仅提出请求,而该请求可能会被忽略。
- 该函数会使得
-
线程清理处理程序
- 线程可以安排它退出时需要调用的函数,这与进程在退出时可以通过
atexit
函数安排退出类似。 - 一个线程可以建立多个清理处理程序,处理程序记录在栈中,因此它们的执行顺序与它们注册时相反。
void pthread_cleanup_push(void (*routine)(void *), void *arg); void pthread_cleanup_pop(int execute);
pthread_cleanup_push
- 其中
arg
参数即为传递给routine
线程清理处理程序的参数值 - 线程清理处理程序在以下情况下才会被调用:
- 调用
pthread_exit
- 响应取消请求(
pthread_cancel
) - 用非零参数调用
pthread_cleanup_pop
- 调用
- 如果线程是从启动例程中返回而终止(
return
语句),那么不会调用线程清理处理程序。
- 其中
pthread_cleanup_pop
:- 该函数将删除上次
pthread_cleanup_push
建立的线程清理处理程序。但是如果execute
参数设置为0
,清理函数将不被调用。 - 注意,
pthread_cleanup_push
和pthread_cleanup_pop
必须成对出现: - 因为这两个函数可以被实现为宏,其中
pthread_cleanup_push
包含{
,pthread_cleanup_pop
包含}
,因此:push
与pop
一定是成对出现的push
可以有多个,同样的pop
也要对应的数量,遵循"先进后出原则"。
- 该函数将删除上次
- 线程可以安排它退出时需要调用的函数,这与进程在退出时可以通过
-
给出了一个如何使用线程处理程序的例子
#include "apue.h" #include <pthread.h> void cleanup(void *arg) { printf("cleanup: %s\n", (char *)arg); } void * thr_fn1(void *arg) { printf("thread 1 start\n"); /*建立清理处理程序*/ pthread_cleanup_push(cleanup, "thread 1 first handler"); pthread_cleanup_push(cleanup, "thread 1 second handler"); printf("thread 1 push complete\n"); if (arg) /*从启动例程中返回而终止,清理处理程序不会被调用*/ return((void *)1); /*传递参数0,将不调用清理函数,那为什么还要调用pthread_cleanup_pop? 因为pthread_cleanup_pop需要和pthread_cleanup_push匹配起来,否则可能编译不通过*/ pthread_cleanup_pop(0); pthread_cleanup_pop(0); return((void *)1); } void * thr_fn2(void *arg) { printf("thread 2 start\n"); pthread_cleanup_push(cleanup, "thread 2 first handler"); pthread_cleanup_push(cleanup, "thread 2 second handler"); printf("thread 2 push complete\n"); if (arg) /*调用pthread_exit后,清理处理程序被调用*/ pthread_exit((void *)2); pthread_cleanup_pop(0); pthread_cleanup_pop(0); pthread_exit((void *)2); } int main(void) { int err; pthread_t tid1, tid2; void *tret; err = pthread_create(&tid1, NULL, thr_fn1, (void *)1); if (err != 0) err_exit(err, "can't create thread 1"); err = pthread_create(&tid2, NULL, thr_fn2, (void *)1); if (err != 0) err_exit(err, "can't create thread 2"); err = pthread_join(tid1, &tret); if (err != 0) err_exit(err, "can't join with thread 1"); printf("thread 1 exit code %ld\n", (long)tret); err = pthread_join(tid2, &tret); if (err != 0) err_exit(err, "can't join with thread 2"); printf("thread 2 exit code %ld\n", (long)tret); exit(0); }
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/threads$ ./cleanup thread 2 start thread 2 push complete thread 1 start thread 1 push complete thread 1 exit code 1 cleanup: thread 2 second handler cleanup: thread 2 first handler thread 2 exit code 2
- 两个线程都正确地启动和退出了,但是只有第二个线程的清理处理程序被调用了,理由在代码中已经说明。
- 注意:从命令行输出可以看到清理处理程序是按照与他们安装时相反的顺序被调用的。
-
pthread_detach
- 默认情况下,线程的终止状态会保存直到对该线程调用
pthread_join
。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用pthread_join
获取它的终止状态。 - 使用
pthread_detach
函数分离线程int pthread_detach(pthread_t thread);
- 默认情况下,线程的终止状态会保存直到对该线程调用
6、线程同步
- 学习操作系统的时候,同步的概念比较熟悉了,这里直接展示线程同步的例子。
- 下图中,线程A读取变量然后给这个变量赋予一个新的数值,但写操作需要两个存储器周期。当线程B在两个存储器写周期中读取这个变量时,它就会得到不一致的值。
- 为了解决这个问题,线程不得不使用锁,同一时间只允许一个线程访问该变量。下图描述了这个同步。如果线程B希望读取变量,它首先要获取。同样,当线程A更新变量时,也需要获取同样的这把锁。这样,线程B在线程A释放锁以前就不能读取变量。
- 比如两个或多个线程试图在同一时间修改同一变量时,也需要进行同步。对一个数值进行增量操作通常分解为以下三步:
- 从内存单元读入寄存器
- 在寄存器中对该变量做增量操作
- 把新值写回内存单元
- 如果不进行线程同步(即对数值进行增量操作不是原子的),将会导致结果错误:
- 除了计算机体系结构外,程序使用变量的方式也会引起竞争,也会导致不一致的情况发生。
6.1、互斥量
- 互斥量
mutex
本质是一把锁,在访问共享资源前对互斥量加锁,在访问完成后解锁互斥量。 - 互斥量用
pthread_mutex_t
数据类型表示,在使用之前必须进行初始化。如果是静态分配的互斥量,可以将它设置为PTHREAD_MUTEX_INITIALIZER
进行初始化。也可以通过pthread_mutex_init
函数初始化。如果是动态分配的互斥量(如用malloc
分配),则在释放内存前需要调用pthread_mutex_destroy
。/*成功则返回0,错误则返回编号*/ int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr); int pthread_mutex_destroy(pthread_mutex_t* mutex);
- 要用默认属性初始化互斥量,只需要把
attr
设为NULL
。
- 要用默认属性初始化互斥量,只需要把
- 通过以下函数对互斥量加锁解锁
int pthread_mutex_lock (pthread_mutex_t *__mutex); int pthread_mutex_trylock (pthread_mutex_t *__mutex); int pthread_mutex_unlock (pthread_mutex_t *__mutex)
pthread_mutex_lock
:对互斥量加锁。如果互斥量已经上锁,则调用线程将阻塞直到互斥量被解锁pthread_mutex_trylock
:对互斥量尝试加锁,如果调用该函数时互斥量处于未锁状态,则锁住互斥量并返回0
;如果互斥量处于加锁状态,则不阻塞直接返回EBUSY
。pthread_mutex_unlock
:对互斥量解锁
- 实例:描述了用于保护某个数据结构的互斥量。当一个以上的线程需要访问动态分配的对象时,我们可以在对象中嵌入引用计数,确保在所有使用该对象的线程完成数据之前,该对象内存空间不会被释放。
#include <stdlib.h> #include <pthread.h> struct foo { int f_count; pthread_mutex_t f_lock; int f_id; /* ... more stuff here ... */ }; struct foo * foo_alloc(int id) /* allocate the object */ { struct foo *fp; /*没必要加锁,在这个操作之前分配线程是唯一引用该对象的线程。 如果在这之后要将该对象放到一个列表中,那就有可能被其他线程发现,需要加锁。*/ if ((fp = malloc(sizeof(struct foo))) != NULL) { fp->f_count = 1; fp->f_id = id; if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { free(fp); return(NULL); } /* ... continue initialization ... */ } return(fp); } /*线程需要调用foo_hold对这个对象的引用计数加1。*/ void foo_hold(struct foo *fp) /* add a reference to the object */ { pthread_mutex_lock(&fp->f_lock); fp->f_count++; pthread_mutex_unlock(&fp->f_lock); } /*当对象使用完毕时,必须调用foo_rele释放引用。如果有另一个线程在在调用foo_hold时 阻塞等待互斥锁,这时即使该队形引用计数为0,fool_rele释放该对象的内存仍然是不对的。 可以通过确保在释放内存前不会被找到 这种方式来避免上述问题。*/ void foo_rele(struct foo *fp) /* release a reference to the object */ { pthread_mutex_lock(&fp->f_lock); /*当最后一个引用被释放时,对象所占的内存空间就被释放*/ if (--fp->f_count == 0) { /* last reference */ pthread_mutex_unlock(&fp->f_lock); pthread_mutex_destroy(&fp->f_lock); free(fp); } else { pthread_mutex_unlock(&fp->f_lock); } }
6.2、互斥量
-
产生死锁的条件
- 线程试图对同一个互斥量 加锁两次 。
- 程序中使用一个以上的互斥量时,如果允许线程一直占有第一个互斥量,并且在试图锁住第二个互斥量时处于阻塞状态,但拥有第二个互斥量的线程也试图锁住第一个互斥量。因为两个线程都在相互请求另一个线程拥有的资源,所以两个线程都无法向前运行。
-
如何避免死锁?
- 仔细控制互斥量加锁的顺序来避免死锁的发生。例如:假设需要对两个互斥量A和B同时加锁。所有线程总是在对互斥量B加锁之前锁住互斥量A,或者反之,那么就不会发生死锁。
-
实例:更新
6.1
节中的程序,展示了两个互斥量的使用方法。在同时需要两个互斥量时,总是让他们以相同的顺序加锁,这样可以避免死锁。此外,本例涉及散列表相关知识,详见散列表。本例中使用的散列函数的构造方法是除留余数法,哈希碰撞的处理基于拉链法。#include <stdlib.h> #include <pthread.h> #define NHASH 29 /*定义散列函数:除留余数法*/ #define HASH(id) (((unsigned long)id)%NHASH) /*定义一个散列表*/ struct foo *fh[NHASH]; /*初始化互斥量hashlock*/ pthread_mutex_t hashlock = PTHREAD_MUTEX_INITIALIZER; /*定义散列链的各字段*/ struct foo { int f_count; pthread_mutex_t f_lock; int f_id; struct foo *f_next; /* protected by hashlock */ /* ... more stuff here ... */ }; struct foo * foo_alloc(int id) /* allocate the object */ { struct foo *fp; int idx; if ((fp = malloc(sizeof(struct foo))) != NULL) { fp->f_count = 1; fp->f_id = id; /*初始化互斥量fp->f_lock*/ if (pthread_mutex_init(&fp->f_lock, NULL) != 0) { free(fp); return(NULL); } /*通过key获取散列值*/ idx = HASH(id); /*锁定互斥量hashlock,可以看出它用于保护散列链字段f_next和散列表fh。 加锁的原因:在这之后要将该对象放到一个列表中,那就有可能被其他线程发现,需要加锁。*/ pthread_mutex_lock(&hashlock); /*维护散列链*/ fp->f_next = fh[idx]; fh[idx] = fp; /*锁定互斥量f_lock,它用于保护对foo结构中的其他字段的访问*/ pthread_mutex_lock(&fp->f_lock); pthread_mutex_unlock(&hashlock); /* ... continue initialization ... */ pthread_mutex_unlock(&fp->f_lock); } return(fp); } void foo_hold(struct foo *fp) /* add a reference to the object */ { pthread_mutex_lock(&fp->f_lock); fp->f_count++; pthread_mutex_unlock(&fp->f_lock); } /*搜索被请求的结构,如果找到了,那么就增加其引用计数,并返回该结构的指针*/ struct foo * foo_find(int id) /* find an existing object */ { struct foo *fp; /*锁住散列列表锁,然后搜索被请求的结构。注意,加锁的顺序是: 先锁定散列列表锁hashlock,再通过调用foo_hold锁定f_lock互斥量。 这样就与foo_alloc中的操作保持了一致。*/ pthread_mutex_lock(&hashlock); for (fp = fh[HASH(id)]; fp != NULL; fp = fp->f_next) { if (fp->f_id == id) { foo_hold(fp); break; } } pthread_mutex_unlock(&hashlock); return(fp); } void foo_rele(struct foo *fp) /* release a reference to the object */ { struct foo *tfp; int idx; /*如果这是最后一个引用,就需要对这个结构互斥量进行解锁,因为我们需要从散列列表中删除这个结构,这样才可以获取散列列表锁,然后重新获取结构互斥量。*/ pthread_mutex_lock(&fp->f_lock); if (fp->f_count == 1) { /* last reference */ pthread_mutex_unlock(&fp->f_lock); pthread_mutex_lock(&hashlock); pthread_mutex_lock(&fp->f_lock); /* need to recheck the condition */ /*从上一次获得结构互斥量以来我们可能被阻塞着,所以需要重新 检查条件,判断是否还需要释放这个结构*/ if (fp->f_count != 1) { fp->f_count--; pthread_mutex_unlock(&fp->f_lock); pthread_mutex_unlock(&hashlock); return; } /* remove from list */ idx = HASH(fp->f_id); tfp = fh[idx]; if (tfp == fp) { fh[idx] = fp->f_next; } else { while (tfp->f_next != fp) tfp = tfp->f_next; tfp->f_next = fp->f_next; } pthread_mutex_unlock(&hashlock); pthread_mutex_unlock(&fp->f_lock); pthread_mutex_destroy(&fp->f_lock); free(fp); } else { /*如果不是最后一个引用,那么只需要简单地对整个引用计数减1, 并解锁所有互斥量*/ fp->f_count--; pthread_mutex_unlock(&fp->f_lock); } }
可以看出
foo_rele
函数设计地十分复杂,为了简化代码,我们可以在一开始就使用hashlock
,代码更正如下:void foo_rele(struct foo *fp) /* release a reference to the object */ { struct foo *tfp; int idx; pthread_mutex_lock(&hashlock); if (--fp->f_count == 0) { /* last reference, remove from list */ idx = HASH(fp->f_id); tfp = fh[idx]; if (tfp == fp) { fh[idx] = fp->f_next; } else { while (tfp->f_next != fp) tfp = tfp->f_next; tfp->f_next = fp->f_next; } pthread_mutex_unlock(&hashlock); pthread_mutex_destroy(&fp->f_lock); free(fp); } else { pthread_mutex_unlock(&hashlock); } }
- 通过该例子可以发现以下特性:
- 如果锁的粒度太粗,就会出现很多线程阻塞等待相同的锁,着并不能改善并发性。
- 如果锁的粒度太细,那么过多的锁的开销会使系统性能受到影响,并且代码变得复杂
- 所以,需要在满足锁的需求的情况下,在代码复杂性和性能之间找到平衡。
- 通过该例子可以发现以下特性:
6.3、函数pthread_mutex_timelock
- 可以通过
pthread_mutex_timelock
指定阻塞时间int pthread_mutex_timedlock (pthread_mutex_t * mutex, const struct timespec *abstime)
- 该函数介于
pthread_mutex_lock
和pthread_mutex_trylock
之间 - 注意
abstime
指定的时间是绝对时间而不是相对时间 - 在到达超时时间
abstime
后,该函数不会对互斥量加锁,而是返回错误ETIMEDOUT
- 该函数介于
6.4、读写锁(共享互斥量)
-
读写锁可以由3种状态(回忆生产者-消费者模型):
- 读模式下加锁
- 写模式下加锁
- 不加锁状态
-
一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁
- 当读写锁是写加锁状态时,在这个锁被解锁前,所有试图对这个锁加锁的线程都会被阻塞。
- 当读写锁在读加锁状态时,所有试图以读模式对它进行加锁的线程都可以得到访问权,但是任何希望以写模式对此锁进行加锁的线程都会阻塞,直到所有的线程释放他们的读锁为止。
- 当读写锁处于读模式锁住的状态,此时有一个线程试图以写模式获取锁时,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
- 读写锁非常适合对于数据结构读次数远大于写次数的情况。
- 读写锁也叫作共享互斥锁。当读写锁是读模式锁住时,就可以说成是以共享模式锁住的。当它是以写模式锁住的时候,就可以说成是以互斥模式锁住的。
-
通过
pthread_rwlock_t
表示一个读写锁,但是使用之前必须初始化,并且在释放它们底层的内存之前必须销毁。int pthread_rwlock_init (pthread_rwlock_t * rwlock,const pthread_rwlockattr_t *attr); int pthread_rwlock_destroy (pthread_rwlock_t *rwlock)
- 如果希望读写锁有默认属性,
attr
参数设为NULL
。 - 对于静态分配的
pthread_rwlock_t
变量,可以通过PTHREAD_RWLOCK_INITIALIZER
对其进行初始化。
- 如果希望读写锁有默认属性,
-
通过以下函数给读写锁加锁解锁
//读加锁 int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock); int pthread_rwlock_timedrdlock (pthread_rwlock_t *rwlock,const struct timespec *abstime); //写加锁 int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock); int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock); int pthread_rwlock_timedwrlock (pthread_rwlock_t *rwlock,const struct timespec *abstime); //解锁 int pthread_rwlock_unlock (pthread_rwlock_t *rwlock) //以上函数:成功则返回0,否则返回错误编号
- 注意,带有超时的加锁函数中,时间值是绝对时间而不是相对时间。
-
实例:解释读写锁的使用。作业请求队列由单个读写锁保护。这个例子给出了第
3
节中工作队列的一种可能的实现方式,多个工作线程获取单个主线程分配给它们的作业。#include <stdlib.h> #include <pthread.h> /*模拟第3节中的 作业 */ struct job { struct job *j_next; struct job *j_prev; pthread_t j_id; /* tells which thread handles this job */ /* ... more stuff here ... */ }; /*模拟第3节中的 工作队列 */ struct queue { struct job *q_head; struct job *q_tail; pthread_rwlock_t q_lock; }; /* * Initialize a queue. */ int queue_init(struct queue *qp) { int err; qp->q_head = NULL; qp->q_tail = NULL; /*初始化读写锁*/ err = pthread_rwlock_init(&qp->q_lock, NULL); if (err != 0) return(err); /* ... continue initialization ... */ return(0); } /* * Insert a job at the head of the queue. */ void job_insert(struct queue *qp, struct job *jp) { /*向队列头部增加作业,采用写模式来锁住队列的读写锁*/ pthread_rwlock_wrlock(&qp->q_lock); jp->j_next = qp->q_head; jp->j_prev = NULL; if (qp->q_head != NULL) qp->q_head->j_prev = jp; else qp->q_tail = jp; /* list was empty */ qp->q_head = jp; pthread_rwlock_unlock(&qp->q_lock); } /* * Append a job on the tail of the queue. */ void job_append(struct queue *qp, struct job *jp) { /*向队列尾部增加作业,采用写模式来锁住队列的读写锁*/ pthread_rwlock_wrlock(&qp->q_lock); jp->j_next = NULL; jp->j_prev = qp->q_tail; if (qp->q_tail != NULL) qp->q_tail->j_next = jp; else qp->q_head = jp; /* list was empty */ qp->q_tail = jp; pthread_rwlock_unlock(&qp->q_lock); } /* * Remove the given job from a queue. */ void job_remove(struct queue *qp, struct job *jp) { /*从队列中删除作业,采用写模式来锁住队列的读写锁*/ pthread_rwlock_wrlock(&qp->q_lock); if (jp == qp->q_head) { qp->q_head = jp->j_next; if (qp->q_tail == jp) qp->q_tail = NULL; else jp->j_next->j_prev = jp->j_prev; } else if (jp == qp->q_tail) { qp->q_tail = jp->j_prev; jp->j_prev->j_next = jp->j_next; } else { jp->j_prev->j_next = jp->j_next; jp->j_next->j_prev = jp->j_prev; } pthread_rwlock_unlock(&qp->q_lock); } /* * Find a job for the given thread ID. */ struct job * job_find(struct queue *qp, pthread_t id) { struct job *jp; /*不管何时搜索队列,都需要获取读模式下的锁,允许所有的工作线程并发地搜索队列*/ if (pthread_rwlock_rdlock(&qp->q_lock) != 0) return(NULL); for (jp = qp->q_head; jp != NULL; jp = jp->j_next) if (pthread_equal(jp->j_id, id)) break; pthread_rwlock_unlock(&qp->q_lock); return(jp); }
- 在该例中,只有在线程搜索作业地频率远远高于增加或删除作业时,使用读写锁才有可能改善性能。
- 工作线程只能从队列中读取与它们的线程ID匹配的作业。由于作业结构同一时间只能由一个线程使用,所以不需要额外的加锁。
6.5、带有超时的读写锁
- 在
6.4
节中已有介绍。
6.6、条件变量
-
通过
pthread_cond_t
表示条件变量类型。使用之前必须进行初始化。在释放条件变量最底层的内存空间之前,可以使用pthread_cond_destroy
对其进行销毁。int pthread_cond_init (pthread_cond_t *cond,const pthread_condattr_t *attr); int pthread_cond_destroy (pthread_cond_t *cond);
- 对于静态分配的
pthread_cond_t
对象,可以使用PTHREAD_COND_INITIALIZER
对其进行初始化。也可以使用pthread_cond_init
函数进行初始化。 - 如果要使用默认属性的条件变量,
attr
属性用NULL
。
- 对于静态分配的
-
可以使用
pthread_cond_wait
等待条件变量变为真int pthread_cond_wait (pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait (pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime);
- pthread_cond_wait
当pthread_cond_wait
等待条件变为真时,线程阻塞,对mutex参数指定的互斥量解锁。pthread_cond_wait
函数返回时,互斥量再次被锁住。 - pthread_cond_timedwait
与pthread_cond_wait
基本类似,只是制定了超时限制abstime
(绝对时间而不是相对时间)。如果超时到期时条件还没有出现,则函数返回ETIMEDOUT
,互斥量被锁住。
- pthread_cond_wait
-
使用以下函数唤醒等待该条件的线程(即为给线程或条件发信号)
int pthread_cond_signal (pthread_cond_t *cond); int pthread_cond_broadcast (pthread_cond_t *cond)
pthread_cond_signal
至少唤醒一个等待该条件的线程pthread_cond_broadcast
唤醒等待该条件的所有线程。
-
实例:给出如何结合使用条件和互斥量对线程进行同步。
#include <pthread.h> struct msg { struct msg *m_next; /* ... more stuff here ... */ }; struct msg *workq; pthread_cond_t qready = PTHREAD_COND_INITIALIZER; pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER; void process_msg(void) { struct msg *mp; for (;;) { /*用互斥量保护条件*/ pthread_mutex_lock(&qlock); /*在while循环中保护条件*/ while (workq == NULL) pthread_cond_wait(&qready, &qlock); mp = workq; workq = mp->m_next; pthread_mutex_unlock(&qlock); /* now process the message mp */ } } void enqueue_msg(struct msg *mp) { /*把消息放到工作队列中,需要占有互斥量*/ pthread_mutex_lock(&qlock); mp->m_next = workq; workq = mp; pthread_mutex_unlock(&qlock); /*在给等待线程发信号时,不需要占用互斥量*/ pthread_cond_signal(&qready); }
pthread_cond_wait
放在if
还是while
里面:
需要注意由于有可能同时唤醒多个正在等待该条件的线程。因此,如果pthread_cond_wait
放在if
里面而不是while
里面,那么当被唤起两个线程时(考虑争夺仅有的一个资源时的情况),会发生第二个线程没有资源可以消费的情况,后续操作出现错误!因此,一定要使用while
做进一步循环判断,这样即使两个线程都被唤起,那么第二个线程会再次判断资源是否可用,如果不可用,就再次pthread_cond_wait
。
6.7、自旋锁
- 自旋锁与互斥量类似,但它不通过休眠使进程阻塞,而是在获得锁之前一直处于忙等(自旋)阻塞状态。一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,会特别浪费处理器时间。所以自旋锁不应该被长时间持有。
- 自旋锁可用于以下情况:锁被持有的时间短,而且线程不希望在重新调度上花费太多成本。
- 通过
pthread_spinlock_t
类型表示自旋锁。在使用之前需要初始化,在释放前需要销毁int pthread_spin_init (pthread_spinlock_t *lock, int pshared); int pthread_spin_destroy (pthread_spinlock_t *lock);
pshared
参数表示进程共享属性:PTHREAD_PROCESS_SHARED
:自旋锁可以在不同进程的线程间共享。自旋锁能被可以访问锁底层内存的线程所获取,即使这些线程属于不同的进程。(即,该锁可能位于在多个进程之间共享的共享内存对象中)PTHREAD_PROCESS_PRIVATE
:自旋锁只能被进程内部线程访问.
- 通过以下函数对自旋锁进行加锁解锁
int pthread_spin_lock (pthread_spinlock_t *lock); int pthread_spin_trylock (pthread_spinlock_t *lock); int pthread_spin_unlock (pthread_spinlock_t *lock)
- 注意,
pthread_spin_lock
在获取锁之前一直自旋 - 注意,
pthread_spin_trylock
不会导致自旋,而是直接返回。如果不能获得锁立即返回EBUSY
。
- 注意,
6.8、屏障
-
屏障是用户协调多个线程并行工作的同步机制。屏障允许多个线程等待,直到所有的合作线程都到达某一点,然后从该点继续执行。
-
pthread_barrier_t
表示一个屏障对象,需要进行初始化和销毁int pthread_barrier_init (pthread_barrier_t barrier,const pthread_barrierattr_t *attr, unsigned int count); int pthread_barrier_destroy (pthread_barrier_t *barrier);
- 其中
count
指定在允许所有线程继续运行之前,必须到达屏障的线程数目。 attr
指定屏障对象属性,NULL
表示默认属性。
- 其中
-
通过
pthread_barrier_wait
函数表明,线程已完成工作,准备等所有其他线程赶上来int pthread_barrier_wait (pthread_barrier_t *barrier);
- 调用
pthread_barrier_wait
的线程在屏障计数未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait
的线程,就满足了屏障计数,所有线程被唤醒继续执行。 - 对于一次屏障,
pthread_barrier_wait
会在一个线程中返回PTHREAD_BARRIER_SERIAL
,其他线程返回0。这使得可以把一个线程当做主线程,它工作在其他所有线程已完成的工作结果上。 - 一旦达到屏障计数值,而且线程处于非阻塞状态,屏障对象可以被重用。但是必须先
pthread_barrier_destroy
然后在pthread_barrier_init
对其进行初始化才能被再次使用,否则屏障计数不会改变。
- 调用
-
实例:给出在一个任务上合作的多个线程之间如何使用屏障进行同步。
#include "apue.h" #include <pthread.h> #include <limits.h> #include <sys/time.h> #define NTHR 8 /* number of threads */ #define NUMNUM 8000000L /* number of numbers to sort */ #define TNUM (NUMNUM/NTHR) /* number to sort per thread */ long nums[NUMNUM]; long snums[NUMNUM]; pthread_barrier_t b; #ifdef SOLARIS #define heapsort qsort #else extern int heapsort(void *, size_t, size_t, int (*)(const void *, const void *)); #endif /* * Compare two long integers (helper function for heapsort) */ int complong(const void *arg1, const void *arg2) { long l1 = *(long *)arg1; long l2 = *(long *)arg2; if (l1 == l2) return 0; else if (l1 < l2) return -1; else return 1; } /* * Worker thread to sort a portion of the set of numbers. */ void * thr_fn(void *arg) { long idx = (long)arg; heapsort(&nums[idx], TNUM, sizeof(long), complong); pthread_barrier_wait(&b); /* * Go off and perform more work ... */ return((void *)0); } /* * Merge the results of the individual sorted ranges. */ void merge() { long idx[NTHR]; long i, minidx, sidx, num; for (i = 0; i < NTHR; i++) idx[i] = i * TNUM; for (sidx = 0; sidx < NUMNUM; sidx++) { num = LONG_MAX; for (i = 0; i < NTHR; i++) { if ((idx[i] < (i+1)*TNUM) && (nums[idx[i]] < num)) { num = nums[idx[i]]; minidx = i; } } snums[sidx] = nums[idx[minidx]]; idx[minidx]++; } } int main() { unsigned long i; struct timeval start, end; long long startusec, endusec; double elapsed; int err; pthread_t tid; /* * Create the initial set of numbers to sort. */ srandom(1); for (i = 0; i < NUMNUM; i++) nums[i] = random(); /* * Create 8 threads to sort the numbers. */ gettimeofday(&start, NULL); /*屏蔽计数值设置为工作线程数+1的原因:不需要使用pthread_barrier_wait函数的返回值 PTHREAD_BARRIER_SERIAL来决定那个线程执行结果合并操作,因为我们使用了主线程来完成这个任务*/ pthread_barrier_init(&b, NULL, NTHR+1); for (i = 0; i < NTHR; i++) { err = pthread_create(&tid, NULL, thr_fn, (void *)(i * TNUM)); if (err != 0) err_exit(err, "can't create thread"); } pthread_barrier_wait(&b); merge(); gettimeofday(&end, NULL); /* * Print the sorted list. */ startusec = start.tv_sec * 1000000 + start.tv_usec; endusec = end.tv_sec * 1000000 + end.tv_usec; elapsed = (double)(endusec - startusec) / 1000000.0; printf("sort took %.4f seconds\n", elapsed); for (i = 0; i < NUMNUM; i++) printf("%ld\n", snums[i]); exit(0); }
- 这个例子给出了多个线程只执行一个任务时,使用屏障的简单情况。在更加实际的情况下,工作线程在调用
pthread_barrier_wait
函数返回后会接着执行其他的活动。 - 在这个实例中,使用8个线程分解了800万个数的排序工作。每个线程用堆排序算法对100万个数进行排序。然后主线程调用一个函数对这些结果进行合并。
- 运行结果:在8核处理器系统中,使用8个并行线程和1个合并结果的线程去完成800万个数的堆排序,仅需要
1.91
秒,而如果只用1个线程则需要12.14
秒,速度相差6倍。
- 这个例子给出了多个线程只执行一个任务时,使用屏障的简单情况。在更加实际的情况下,工作线程在调用