线程的基本操作
147P-线程概念
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
ps -Lf 进程号 查看进程的线程
148P-三级映射
149P-线程共享和非共享
线程共享:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap ---> 共享【全局变量】(errno)
150P-中午复习
151P-创建线程
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
返回值:本线程id
检查出错返回: 线程中。
fprintf(stderr, "xxx error: %s\n", strerror(ret));
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg); 创建子线程。
参1:传出参数,表新创建的子线程 id
参2:线程属性。传NULL表使用默认属性。
参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4:参3的参数。没有的话,传NULL
返回值:成功:0
失败:errno
下面这个例子,创建一个子线程去执行其他任务:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str){
- perror(str);
- exit(1);
- }
- void *tfn(void *arg){
- printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
- return NULL;
- }
- int main(int argc, char *argv[]){
- pthread_t tid;
- printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
- int ret = pthread_create(&tid, NULL, tfn, NULL);
- if (ret != 0) {
- perror("pthread_create error");
- }
- return 0;
- }
编译运行,结果如下:
可以看到,子线程的打印信息并未出现。原因在于,主线程执行完之后,就销毁了整个进程的地址空间,于是子线程就无法打印。简单粗暴的方法就是让主线程睡1秒,等子线程执行。
代码变化如下:
编译执行,如下:
152P-循环创建多个子线程
下面的例子,循环创建多个子线程:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void sys_err(const char *str){
- perror(str);
- exit(1);
- }
- void *tfn(void *arg){
- int i = (int)arg;
- sleep(i);
- printf("--I'm %dth thread: pid = %d, tid = %lu\n",i+1, getpid(), pthread_self());
- return NULL;
- }
- int main(int argc, char *argv[]){
- int i;
- int ret;
- pthread_t tid;
- for(i=0;i<5;i++){
- ret = pthread_create(&tid, NULL, tfn, (void *)i);
- if (ret != 0) {
- sys_err("pthread_create error");
- }
- }
- sleep(i);
- printf("I'm main, pid = %d, tid = %lu\n", getpid(), pthread_self());
- return 0;
- }
编译运行,结果如下:
编译时会出现类型强转的警告,指针4字节转int的8字节,不过不存在精度损失,忽略就行。
153P-错误分析
在152P的代码中,如果将i取地址后再传入线程创建函数里,就是说
当前传的是:(void *)i
改成: (void *)&i
相应的,修改回调函数:int i = *((int *)arg)
运行代码,会出现如下结果:
如果多次运行都只有主线程的输出,将主线程等待时长从i改为大于6的数即可。因为子线程等待时间i是不定的,但都小于等于6秒,由于抢cpu时没抢过主线程,导致没有子线程的输出。
错误原因在于,子线程如果用引用传递i,会去读取主线程里的i值,而主线程里的i是动态变化的,不固定。所以,应该采用值传递,不用引用传递。
154P-线程间全局变量共享
直接看个代码,在子线程里更改全局变量,看主线程里的该变量有啥变化:
编译运行,结果如下:
可以看到,子线程里更改全局变量后,主线程里也跟着发生变化。
155P-pthread_exit退出
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
如果在回调函数里加一段代码:
if(i == 2)
exit(0);
看起来好像是退出了第三个子线程,然而运行时,发现后续的4,5也没了。这是因为,exit是退出进程。
一、修改一下,换成:
if(i == 2)
return NULL;
这样运行一下,发现后续线程不会凉凉,说明return是可以达到退出线程的目的。然而真正意义上,return是返回到函数调用者那里去,线程并没有退出。
二、再修改一下,再定义一个函数func,直接返回那种
void *func(void){
return NULL;
}
if(i == 2)
func();
运行,发现1,2,3,4,5线程都还在,说明没有达到退出目的。
三、再次修改:
void *func(void){
pthread_exit(NULL);
return NULL;
}
if(i == 2)
func();
编译运行,发现3没了,看起来很科学的样子。pthread_exit表示将当前线程退出。放在函数里,还是直接调用,都可以。
回到之前说的一个问题,由于主线程可能先于子线程结束,所以子线程的输出可能不会打印出来,当时是用主线程sleep等待子线程结束来解决的。现在就可以使用pthread_exit来解决了。方法就是将return 0替换为pthread_exit,只退出当先线程,不会对其他线程造成影响。
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
156P-pthread_join
int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值:成功:0
失败:errno
下面这个是回收线程并获取子线程返回值的小例子:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- struct thrd {
- int var;
- char str[256];
- };
- void sys_err(const char *str)
- {
- perror(str);
- exit(1);
- }
- void *tfn(void *arg)
- {
- struct thrd *tval;
- tval = malloc(sizeof(tval));
- tval->var = 100;
- strcpy(tval->str, "hello thread");
- return (void *)tval;
- }
- int main(int argc, char *argv[])
- {
- pthread_t tid;
- struct thrd *retval;
- int ret = pthread_create(&tid, NULL, tfn, NULL);
- if (ret != 0)
- sys_err("pthread_create error");
- //int pthread_join(pthread_t thread, void **retval);
- ret = pthread_join(tid, (void **)&retval);
- if (ret != 0)
- sys_err("pthread_join error");
- printf("child thread exit with var= %d, str= %s\n", retval->var, retval->str);
- pthread_exit(NULL);
- }
编译运行,结果如下:
157P-pthread_join作业
使用pthread_join函数将循环创建的多个子线程回收
这里tid要使用数组来存
158P-pthread_cancel函数
int pthread_cancel(pthread_t thread); 杀死一个线程。 需要到达取消点(保存点)
thread: 待杀死的线程id
返回值:成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
小例子,主线程调用pthread_cancel杀死子线程
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void *tfn(void *arg){
- while (1) {
- printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
- sleep(1);
- }
- return NULL;
- }
- int main(int argc, char *argv[]){
- pthread_t tid;
- int ret = pthread_create(&tid, NULL, tfn, NULL);
- if (ret != 0) {
- fprintf(stderr, "pthread_create error:%s\n", strerror(ret));
- exit(1);
- }
- printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
- sleep(5);
- ret = pthread_cancel(tid); // 终止线程
- if (ret != 0) {
- fprintf(stderr, "pthread_cancel error:%s\n", strerror(ret));
- exit(1);
- }
- while (1);
- pthread_exit((void *)0);
- }
编译运行,如下:
可以看到,主线程确实kill了子线程。
这里要注意一点,pthread_cancel工作的必要条件是进入内核,如果tfn真的奇葩到没有进入内核,则pthread_cancel不能杀死线程,此时需要手动设置取消点,就是pthread_testcancel()
159P-检查出错返回
int pthread_detach(pthread_t thread); 设置线程分离
thread: 待分离的线程id
返回值:成功:0
失败:errno
下面这个例子,使用detach分离线程,照理来说,分离后的线程会自动回收:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void *tfn(void *arg)
- {
- printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
- return NULL;
- }
- int main(int argc, char *argv[])
- {
- pthread_t tid;
- int ret = pthread_create(&tid, NULL, tfn, NULL);
- if (ret != 0) {
- perror("pthread_create error");
- }
- ret = pthread_detach(tid); // 设置线程分离` 线程终止,会自动清理pcb,无需回收
- if (ret != 0) {
- perror("pthread_detach error");
- }
- sleep(1);
- ret = pthread_join(tid, NULL);
- printf("join ret = %d\n", ret);
- if (ret != 0) {
- perror("pthread_join error");
- }
- printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
- pthread_exit((void *)0);
- }
编译运行,结果如下:
这里,问题出现了,join出错了,但是没打印错误原因。之前的perror方法检查线程错误是有问题的。应该使用strerror,修改代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void *tfn(void *arg)
- {
- printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
- return NULL;
- }
- int main(int argc, char *argv[])
- {
- pthread_t tid;
- int ret = pthread_create(&tid, NULL, tfn, NULL);
- if (ret != 0) {
- fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
- exit(1);
- }
- ret = pthread_detach(tid); // 设置线程分离` 线程终止,会自动清理pcb,无需回收
- if (ret != 0) {
- fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
- exit(1);
- }
- sleep(1);
- ret = pthread_join(tid, NULL);
- printf("join ret = %d\n", ret);
- if (ret != 0) {
- fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
- exit(1);
- }
- printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
- pthread_exit((void *)0);
- }
编译运行,结果如下:
160P-线程分离pthread_detach
上一节的出错,是因为线程分离后,系统会自动回收资源,用pthread_join去回收已经被系统回收的线程,那个线程号就是无效参数。
161P-进程和线程控制原语对比
线程控制原语 进程控制原语
pthread_create() fork();
pthread_self() getpid();
pthread_exit() exit(); / return
pthread_join() wait()/waitpid()
pthread_cancel() kill()
pthread_detach()
162P-线程属性设置分离线程
线程属性:
设置分离属性。
pthread_attr_t attr 创建一个线程属性结构体变量
pthread_attr_init(&attr); 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态
pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程
pthread_attr_destroy(&attr); 销毁线程属性
调整线程状态,使线程创建出来就是分离态,代码如下:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void *tfn(void *arg)
- {
- printf("thread: pid = %d, tid = %lu\n", getpid(), pthread_self());
- return NULL;
- }
- int main(int argc, char *argv[])
- {
- pthread_t tid;
- pthread_attr_t attr;
- int ret = pthread_attr_init(&attr);
- if (ret != 0) {
- fprintf(stderr, "attr_init error:%s\n", strerror(ret));
- exit(1);
- }
- ret = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置线程属性为 分离属性
- if (ret != 0) {
- fprintf(stderr, "attr_setdetachstate error:%s\n", strerror(ret));
- exit(1);
- }
- ret = pthread_create(&tid, &attr, tfn, NULL);
- if (ret != 0) {
- perror("pthread_create error");
- }
- ret = pthread_attr_destroy(&attr);
- if (ret != 0) {
- fprintf(stderr, "attr_destroy error:%s\n", strerror(ret));
- exit(1);
- }
- ret = pthread_join(tid, NULL);
- if (ret != 0) {
- fprintf(stderr, "pthread_join error:%s\n", strerror(ret));
- exit(1);
- }
- printf("main: pid = %d, tid = %lu\n", getpid(), pthread_self());
- pthread_exit((void *)0);
- }
编译运行,结果如下:
如图,pthread_join报错,说明线程已经自动回收,设置分离成功。
163P-线程使用注意事项
164P-总结
守护进程:
daemon进程。通常运行与操作系统后台,脱离控制终端。一般不与用户直接交互。周期性的等待某个事件发生或周期性执行某一动作。
不受用户登录注销影响。通常采用以d结尾的命名方式。
守护进程创建步骤:
1. fork子进程,让父进程终止。
2. 子进程调用 setsid() 创建新会话
3. 通常根据需要,改变工作目录位置 chdir(), 防止目录被卸载。
4. 通常根据需要,重设umask文件权限掩码,影响新文件的创建权限。 022 -- 755 0345 --- 432 r---wx-w- 422
5. 通常根据需要,关闭/重定向 文件描述符
6. 守护进程 业务逻辑。while()
=============================================================
线程概念:
进程:有独立的 进程地址空间。有独立的pcb。 分配资源的最小单位。
线程:有独立的pcb。没有独立的进程地址空间。 最小单位的执行。
ps -Lf 进程id ---> 线程号。LWP --》cpu 执行的最小单位。
线程共享:
独享 栈空间(内核栈、用户栈)
共享 ./text./data ./rodataa ./bsss heap ---> 共享【全局变量】(errno)
线程控制原语:
pthread_t pthread_self(void); 获取线程id。 线程id是在进程地址空间内部,用来标识线程身份的id号。
返回值:本线程id
检查出错返回: 线程中。
fprintf(stderr, "xxx error: %s\n", strerror(ret));
int pthread_create(pthread_t *tid, const pthread_attr_t *attr, void *(*start_rountn)(void *), void *arg); 创建子线程。
参1:传出参数,表新创建的子线程 id
参2:线程属性。传NULL表使用默认属性。
参3:子线程回调函数。创建成功,ptherad_create函数返回时,该函数会被自动调用。
参4:参3的参数。没有的话,传NULL
返回值:成功:0
失败:errno
循环创建N个子线程:
for (i = 0; i < 5; i++)
pthread_create(&tid, NULL, tfn, (void *)i); // 将 int 类型 i, 强转成 void *, 传参。
void pthread_exit(void *retval); 退出当前线程。
retval:退出值。 无退出值时,NULL
exit(); 退出当前进程。
return: 返回到调用者那里去。
pthread_exit(): 退出当前线程。
int pthread_join(pthread_t thread, void **retval); 阻塞 回收线程。
thread: 待回收的线程id
retval:传出参数。 回收的那个线程的退出值。
线程异常借助,值为 -1。
返回值:成功:0
失败:errno
int pthread_detach(pthread_t thread); 设置线程分离
thread: 待分离的线程id
返回值:成功:0
失败:errno
int pthread_cancel(pthread_t thread); 杀死一个线程。 需要到达取消点(保存点)
thread: 待杀死的线程id
返回值:成功:0
失败:errno
如果,子线程没有到达取消点, 那么 pthread_cancel 无效。
我们可以在程序中,手动添加一个取消点。使用 pthread_testcancel();
成功被 pthread_cancel() 杀死的线程,返回 -1.使用pthead_join 回收。
线程控制原语 进程控制原语
pthread_create() fork();
pthread_self() getpid();
pthread_exit() exit(); / return
pthread_join() wait()/waitpid()
pthread_cancel() kill()
pthread_detach()
线程属性:
设置分离属性。
pthread_attr_t attr 创建一个线程属性结构体变量
pthread_attr_init(&attr); 初始化线程属性
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 设置线程属性为 分离态
pthread_create(&tid, &attr, tfn, NULL); 借助修改后的 设置线程属性 创建为分离态的新线程
pthread_attr_destroy(&attr); 销毁线程属性
165P-线程同步概念
线程同步:
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
数据混乱的原因:
- 资源共享(独享资源则不会)
- 调度随机(意味着数据访问会出现竞争)
- 线程间缺乏必要同步机制
166P-锁使用的注意事项
锁的使用:
建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
167P-借助互斥锁管理共享数据实现同步
下面一个小例子,数据共享导致的混乱:
- #include <stdio.h>
- #include <string.h>
- #include <pthread.h>
- #include <stdlib.h>
- #include <unistd.h>
- void *tfn(void *arg)
- {
- srand(time(NULL));
- while (1) {
- printf("hello ");
- sleep(rand() % 3); /*模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误*/
- printf("world\n");
- sleep(rand() % 3);
- }
- return NULL;
- }
- int main(void)
- {
- pthread_t tid;
- srand(time(NULL));
- pthread_create(&tid, NULL, tfn, NULL);
- while (1) {
- printf("HELLO ");
- sleep(rand() % 3);
- printf("WORLD\n");
- sleep(rand() % 3);
- }
- pthread_join(tid, NULL);
- return 0;
- }
编译运行,结果如下:
如图,输出结果是主线程和子线程交叉的。
主要应用函数:
pthread_mutex_init 函数
pthread_mutex_destory 函数
pthread_mutex_lock 函数
pthread_mutex_trylock 函数
pthread_mutex_unlock 函数
以上5个函数的返回值都是:成功返回0,失败返回错误号
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待
pthread_mutex_t mutex;变量mutex只有两种取值:0,1
使用mutex(互斥量、互斥锁)一般步骤:
pthread_mutex_t 类型。
1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化 1
3. pthread_mutex_lock;加锁 1-- --> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ --> 1
6. pthead_mutex_destroy;销毁锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr)
这里的restrict关键字,表示指针指向的内容只能通过这个指针进行修改
restrict关键字:
用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
初始化互斥量:
pthread_mutex_t mutex;
1. pthread_mutex_init(&mutex, NULL); 动态初始化。
2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态初始化。
修改上面的代码,使用锁实现互斥访问共享区:
- #include <stdio.h>
- #include <string.h>
- #include <pthread.h>
- #include <stdlib.h>
- #include <unistd.h>
- pthread_mutex_t mutex; // 定义一把互斥锁
- void *tfn(void *arg)
- {
- srand(time(NULL));
- while (1) {
- pthread_mutex_lock(&mutex); // 加锁
- printf("hello ");
- sleep(rand() % 3); // 模拟长时间操作共享资源,导致cpu易主,产生与时间有关的错误
- printf("world\n");
- pthread_mutex_unlock(&mutex); // 解锁
- sleep(rand() % 3);
- }
- return NULL;
- }
- int main(void)
- {
- pthread_t tid;
- srand(time(NULL));
- int ret = pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
- if(ret != 0){
- fprintf(stderr, "mutex init error:%s\n", strerror(ret));
- exit(1);
- }
- pthread_create(&tid, NULL, tfn, NULL);
- while (1) {
- pthread_mutex_lock(&mutex); // 加锁
- printf("HELLO ");
- sleep(rand() % 3);
- printf("WORLD\n");
- pthread_mutex_unlock(&mutex); // 解锁
- sleep(rand() % 3);
- }
- pthread_join(tid, NULL);
- pthread_mutex_destory(&mutex); // 销毁互斥锁
- return 0;
- }
编译运行,结果如下:
可以看到,主线程和子线程在访问共享区时就没有交叉输出的情况了。
168P-互斥锁使用技巧
注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
加锁: --操作, 阻塞线程。
解锁: ++操作, 唤醒阻塞在锁上的线程。
try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY
169P-try锁
try锁:尝试加锁,成功--,加锁失败直接返回错误号(如EBUSY),不阻塞
170P-读写锁操作函数原型
读写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
以上函数都是成功返回0,失败返回错误号。
pthread_rwlock_t 类型 用于定义一个读写锁变量
pthread_rwlock_t rwlock
171P-两种死锁
【死锁】:
是使用锁不恰当导致的现象:
1. 对一个锁反复lock。
2. 两个线程,各自持有一把锁,请求另一把。
172P-读写锁原理
读写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
173P-rwlock
一个读写锁的例子,核心还是
读共享,写独占。
写锁优先级高。
- /* 3个线程不定时 "写" 全局资源,5个线程不定时 "读" 同一全局资源 */
- #include <stdio.h>
- #include <unistd.h>
- #include <pthread.h>
- int counter; //全局资源
- pthread_rwlock_t rwlock;
- void *th_write(void *arg)
- {
- int t;
- int i = (int)arg;
- while (1) {
- t = counter; // 保存写之前的值
- usleep(1000);
- pthread_rwlock_wrlock(&rwlock);
- printf("=======write %d: %lu: counter=%d ++counter=%d\n", i, pthread_self(), t, ++counter);
- pthread_rwlock_unlock(&rwlock);
- usleep(9000); // 给 r 锁提供机会
- }
- return NULL;
- }
- void *th_read(void *arg)
- {
- int i = (int)arg;
- while (1) {
- pthread_rwlock_rdlock(&rwlock);
- printf("----------------------------read %d: %lu: %d\n", i, pthread_self(), counter);
- pthread_rwlock_unlock(&rwlock);
- usleep(2000); // 给写锁提供机会
- }
- return NULL;
- }
- int main(void)
- {
- int i;
- pthread_t tid[8];
- pthread_rwlock_init(&rwlock, NULL);
- for (i = 0; i < 3; i++)
- pthread_create(&tid[i], NULL, th_write, (void *)i);
- for (i = 0; i < 5; i++)
- pthread_create(&tid[i+3], NULL, th_read, (void *)i);
- for (i = 0; i < 8; i++)
- pthread_join(tid[i], NULL);
- pthread_rwlock_destroy(&rwlock); //释放读写琐
- return 0;
- }
编译运行,结果如下:
程序输出飞快,随便截个图,如上图。
由于读共享,写独占。写锁优先级高。前面5个read一定先于后面的write到达的,不然write会抢到锁先进行写操作。
174P-午后复习
175P-静态初始化条件变量和互斥量
条件变量:
本身不是锁! 但是通常结合锁来使用。 mutex
主要应用函数:
pthread_cond_t cond;
初始化条件变量:
1. pthread_cond_init(&cond, NULL); 动态初始化。
2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化。
176P-条件变量和相关函数wait
阻塞等待条件:
pthread_cond_wait(&cond, &mutex);
作用:
1) 阻塞等待条件变量满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex)),12两步为一个原子操作
3) 当条件满足,函数返回时,解除阻塞并重新申请获取互斥锁。重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)
177P-条件变量的生产者消费者模型分析
178P-条件变量生产者消费者代码预览
代码如下:
- /*借助条件变量模拟 生产者-消费者 问题*/
- #include <stdlib.h>
- #include <unistd.h>
- #include <pthread.h>
- #include <stdio.h>
- /*链表作为公享数据,需被互斥量保护*/
- struct msg {
- struct msg *next;
- int num;
- };
- struct msg *head;
- /* 静态初始化 一个条件变量 和 一个互斥量*/
- pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
- pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
- void *consumer(void *p)
- {
- struct msg *mp;
- for (;;) {
- pthread_mutex_lock(&lock);
- while (head == NULL) { //头指针为空,说明没有节点 可以为if吗
- pthread_cond_wait(&has_product, &lock); // 解锁,并阻塞等待
- }
- mp = head;
- head = mp->next; //模拟消费掉一个产品
- pthread_mutex_unlock(&lock);
- printf("-Consume %lu---%d\n", pthread_self(), mp->num);
- free(mp);
- sleep(rand() % 5);
- }
- }
- void *producer(void *p)
- {
- struct msg *mp;
- for (;;) {
- mp = malloc(sizeof(struct msg));
- mp->num = rand() % 1000 + 1; //模拟生产一个产品
- printf("-Produce ---------------------%d\n", mp->num);
- pthread_mutex_lock(&lock);
- mp->next = head;
- head = mp;
- pthread_mutex_unlock(&lock);
- pthread_cond_signal(&has_product); //将等待在该条件变量上的一个线程唤醒
- sleep(rand() % 5);
- }
- }
- int main(int argc, char *argv[])
- {
- pthread_t pid, cid;
- srand(time(NULL));
- pthread_create(&pid, NULL, producer, NULL);
- pthread_create(&cid, NULL, consumer, NULL);
- pthread_join(pid, NULL);
- pthread_join(cid, NULL);
- return 0;
- }
编译运行,结果如下:
可以看到,消费者按序消费,毕竟是链表。
179P-条件变量实现生产者消费者代码
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void err_thread(int ret, char *str)
- {
- if (ret != 0) {
- fprintf(stderr, "%s:%s\n", str, strerror(ret));
- pthread_exit(NULL);
- }
- }
- struct msg {
- int num;
- struct msg *next;
- };
- struct msg *head;
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
- pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量
- void *produser(void *arg)
- {
- while (1) {
- struct msg *mp = malloc(sizeof(struct msg));
- mp->num = rand() % 1000 + 1; // 模拟生产一个数据`
- printf("--produce %d\n", mp->num);
- pthread_mutex_lock(&mutex); // 加锁 互斥量
- mp->next = head; // 写公共区域
- head = mp;
- pthread_mutex_unlock(&mutex); // 解锁 互斥量
- pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量 has_data上的线程.
- sleep(rand() % 3);
- }
- return NULL;
- }
- void *consumer(void *arg)
- {
- while (1) {
- struct msg *mp;
- pthread_mutex_lock(&mutex); // 加锁 互斥量
- if (head == NULL) {
- pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量, 解锁
- } // pthread_cond_wait 返回时, 重写加锁 mutex
- mp = head;
- head = mp->next;
- pthread_mutex_unlock(&mutex); // 解锁 互斥量
- printf("---------consumer:%d\n", mp->num);
- free(mp);
- sleep(rand()%3);
- }
- return NULL;
- }
- int main(int argc, char *argv[])
- {
- int ret;
- pthread_t pid, cid;
- srand(time(NULL));
- ret = pthread_create(&pid, NULL, produser, NULL); // 生产者
- if (ret != 0)
- err_thread(ret, "pthread_create produser error");
- ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
- if (ret != 0)
- err_thread(ret, "pthread_create consuer error");
- pthread_join(pid, NULL);
- pthread_join(cid, NULL);
- return 0;
- }
编译运行,结果如下:
180P-多个消费者使用while做
先看一个手动创建多个消费者的代码:
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <unistd.h>
- #include <errno.h>
- #include <pthread.h>
- void err_thread(int ret, char *str)
- {
- if (ret != 0) {
- fprintf(stderr, "%s:%s\n", str, strerror(ret));
- pthread_exit(NULL);
- }
- }
- struct msg {
- int num;
- struct msg *next;
- };
- struct msg *head;
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义/初始化一个互斥量
- pthread_cond_t has_data = PTHREAD_COND_INITIALIZER; // 定义/初始化一个条件变量
- void *produser(void *arg)
- {
- while (1) {
- struct msg *mp = malloc(sizeof(struct msg));
- mp->num = rand() % 1000 + 1; // 模拟生产一个数据`
- printf("--produce %d\n", mp->num);
- pthread_mutex_lock(&mutex); // 加锁 互斥量
- mp->next = head; // 写公共区域
- head = mp;
- pthread_mutex_unlock(&mutex); // 解锁 互斥量
- pthread_cond_signal(&has_data); // 唤醒阻塞在条件变量 has_data上的线程.
- sleep(rand() % 3);
- }
- return NULL;
- }
- void *consumer(void *arg)
- {
- while (1) {
- struct msg *mp;
- pthread_mutex_lock(&mutex); // 加锁 互斥量
- if (head == NULL) {
- pthread_cond_wait(&has_data, &mutex); // 阻塞等待条件变量, 解锁
- } // pthread_cond_wait 返回时, 重写加锁 mutex
- mp = head;
- head = mp->next;
- pthread_mutex_unlock(&mutex); // 解锁 互斥量
- printf("---------consumer id = %lu :%d\n", pthread_self(), mp->num);
- free(mp);
- sleep(rand()%3);
- }
- return NULL;
- }
- int main(int argc, char *argv[])
- {
- int ret;
- pthread_t pid, cid;
- srand(time(NULL));
- ret = pthread_create(&pid, NULL, produser, NULL); // 生产者
- if (ret != 0)
- err_thread(ret, "pthread_create produser error");
- ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
- if (ret != 0)
- err_thread(ret, "pthread_create consuer error");
- ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
- if (ret != 0)
- err_thread(ret, "pthread_create consuer error");
- ret = pthread_create(&cid, NULL, consumer, NULL); // 消费者
- if (ret != 0)
- err_thread(ret, "pthread_create consuer error");
- pthread_join(pid, NULL);
- pthread_join(cid, NULL);
- return 0;
- }
编译运行,结果如下:
这个代码还是有问题的。比如,两个消费者都阻塞在条件变量上,就是说没有数据可以消费。完事儿都把锁还回去了,生产者此时生产了一个数据,会同时唤醒两个因条件变量阻塞的消费者,完事儿两个消费者去抢锁。结果就是A消费者拿到锁,开始消费数据,B消费者阻塞在锁上。之后A消费完数据,把锁归还,B被唤醒,然而此时已经没有数据供B消费了。所以这里有个逻辑错误,消费者阻塞在条件变量那里应该使用while循环。这样A消费完数据后,B做的第一件事不是去拿锁,而是判定条件变量。
181P-条件变量signal注意事项
pthread_cond_signal(): 唤醒阻塞在条件变量上的 (至少)一个线程。
pthread_cond_broadcast(): 唤醒阻塞在条件变量上的 所有线程。
182P-信号量概念及其相关操作函数
信号量:
应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
函数:
sem_t sem; 定义类型。
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 信号量
pshared: 0: 用于线程间同步
1: 用于进程间同步
value:N值。(指定同时访问的线程数)
sem_destroy();
sem_wait(); 一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock)
sem_post(); 一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock)
183P-信号量实现的生产者消费者
代码如下:
- /*信号量实现 生产者 消费者问题*/
- #include <stdlib.h>
- #include <unistd.h>
- #include <pthread.h>
- #include <stdio.h>
- #include <semaphore.h>
- #define NUM 5
- int queue[NUM]; //全局数组实现环形队列
- sem_t blank_number, product_number; //空格子信号量, 产品信号量
- void *producer(void *arg)
- {
- int i = 0;
- while (1) {
- sem_wait(&blank_number); //生产者将空格子数--,为0则阻塞等待
- queue[i] = rand() % 1000 + 1; //生产一个产品
- printf("----Produce---%d\n", queue[i]);
- sem_post(&product_number); //将产品数++
- i = (i+1) % NUM; //借助下标实现环形
- sleep(rand()%1);
- }
- }
- void *consumer(void *arg)
- {
- int i = 0;
- while (1) {
- sem_wait(&product_number); //消费者将产品数--,为0则阻塞等待
- printf("-Consume---%d\n", queue[i]);
- queue[i] = 0; //消费一个产品
- sem_post(&blank_number); //消费掉以后,将空格子数++
- i = (i+1) % NUM;
- sleep(rand()%3);
- }
- }
- int main(int argc, char *argv[])
- {
- pthread_t pid, cid;
- sem_init(&blank_number, 0, NUM); //初始化空格子信号量为5, 线程间共享 -- 0
- sem_init(&product_number, 0, 0); //产品数为0
- pthread_create(&pid, NULL, producer, NULL);
- pthread_create(&cid, NULL, consumer, NULL);
- pthread_join(pid, NULL);
- pthread_join(cid, NULL);
- sem_destroy(&blank_number);
- sem_destroy(&product_number);
- return 0;
- }
编译运行,结果如下:
184P-总结
线程同步:
协同步调,对公共区域数据按序访问。防止数据混乱,产生与时间有关的错误。
锁的使用:
建议锁!对公共数据进行保护。所有线程【应该】在访问公共数据前先拿锁再访问。但,锁本身不具备强制性。
使用mutex(互斥量、互斥锁)一般步骤:
pthread_mutex_t 类型。
1. pthread_mutex_t lock; 创建锁
2 pthread_mutex_init; 初始化 1
3. pthread_mutex_lock;加锁 1-- --> 0
4. 访问共享数据(stdout)
5. pthrad_mutext_unlock();解锁 0++ --> 1
6. pthead_mutex_destroy;销毁锁
初始化互斥量:
pthread_mutex_t mutex;
1. pthread_mutex_init(&mutex, NULL); 动态初始化。
2. pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 静态初始化。
注意事项:
尽量保证锁的粒度, 越小越好。(访问共享数据前,加锁。访问结束【立即】解锁。)
互斥锁,本质是结构体。 我们可以看成整数。 初值为 1。(pthread_mutex_init() 函数调用成功。)
加锁: --操作, 阻塞线程。
解锁: ++操作, 换醒阻塞在锁上的线程。
try锁:尝试加锁,成功--。失败,返回。同时设置错误号 EBUSY
restrict关键字:
用来限定指针变量。被该关键字限定的指针变量所指向的内存操作,必须由本指针完成。
【死锁】:
是使用锁不恰当导致的现象:
1. 对一个锁反复lock。
2. 两个线程,各自持有一把锁,请求另一把。
读写锁:
锁只有一把。以读方式给数据加锁——读锁。以写方式给数据加锁——写锁。
读共享,写独占。
写锁优先级高。
相较于互斥量而言,当读线程多的时候,提高访问效率
pthread_rwlock_t rwlock;
pthread_rwlock_init(&rwlock, NULL);
pthread_rwlock_rdlock(&rwlock); try
pthread_rwlock_wrlock(&rwlock); try
pthread_rwlock_unlock(&rwlock);
pthread_rwlock_destroy(&rwlock);
条件变量:
本身不是锁! 但是通常结合锁来使用。 mutex
pthread_cond_t cond;
初始化条件变量:
1. pthread_cond_init(&cond, NULL); 动态初始化。
2. pthread_cond_t cond = PTHREAD_COND_INITIALIZER; 静态初始化。
阻塞等待条件:
pthread_cond_wait(&cond, &mutex);
作用: 1) 阻塞等待条件变量满足
2) 解锁已经加锁成功的信号量 (相当于 pthread_mutex_unlock(&mutex))
3) 当条件满足,函数返回时,重新加锁信号量 (相当于, pthread_mutex_lock(&mutex);)
pthread_cond_signal(): 唤醒阻塞在条件变量上的 (至少)一个线程。
pthread_cond_broadcast(): 唤醒阻塞在条件变量上的 所有线程。
【要求,能够借助条件变量,完成生成者消费者】
信号量:
应用于线程、进程间同步。
相当于 初始化值为 N 的互斥量。 N值,表示可以同时访问共享数据区的线程数。
函数:
sem_t sem; 定义类型。
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
sem: 信号量
pshared: 0: 用于线程间同步
1: 用于进程间同步
value:N值。(指定同时访问的线程数)
sem_destroy();
sem_wait(); 一次调用,做一次-- 操作, 当信号量的值为 0 时,再次 -- 就会阻塞。 (对比 pthread_mutex_lock)
sem_post(); 一次调用,做一次++ 操作. 当信号量的值为 N 时, 再次 ++ 就会阻塞。(对比 pthread_mutex_unlock)