1.补充点:
上述这些函数成功返回0,失败则返回错误码;
pthread_mutex_destroy函数用于销毁互斥锁,以释放其占用的内核资源.
销毁一个已经加锁的互斥锁将导致不可预期的后果.
2.线程安全引例
先来看这样一段代码的执行结果是什么?
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <assert.h>#include <pthread.h>void*thread_fun(void *arg){char buff[128]={"a b c d e f g h w q"};char *s=strtok(buff," ");while(s!=NULL){printf("thread:s=%s\n",s);sleep(1);s=strtok(NULL," ");}}int main(){pthread_t id;pthread_create(&id,NULL,thread_fun,NULL);char str[128]={"1 2 3 4 5 6 7 8 9 10"};char *s=strtok(str," ");while(s!=NULL){printf("main:%s\n",s);sleep(1);s=strtok(NULL," ");}pthread_join(id,NULL);exit(0);}
为什么是这样的结果呢?
因为strtok不是线程安全的.内部使用了全局变量(静态变量); 也就是说,只要使用了全局变量或者静态变量的函数都不能在多线程中使用;这些函数都不是线程安全的.
不可重入:当程序被多个线程反复调用,产生的结果会出错.
3线程安全
线程安全即就是在多线程运行的时候,不论线程的调度顺序怎样,最终的结果都是一样的、正确的。那么就说这些线程是安全的。
要保证线程安全需要做到:
1) 对线程同步,保证同一时刻只有一个线程访问临界资源.
2) 在多线程中使用线程安全的函数(可重入函数).
所谓线程安全的函数指的是:如果一个函数能被多个线程同时调用且不发生竟态条件,则我们程它是线程安全的。
4.使用线程安全的函数
strtok_r
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>void *thread_fun(void *arg)
{char buff[128]={"a b c d e f g h w q"};char *ptr=NULL;char *s=strtok_r(buff," ",&ptr);while(s!=NULL){printf("thread:s=%s\n",s);sleep(1);s=strtok_r(NULL," ",&ptr);}
}
int main()
{pthread_t id;pthread_create(&id,NULL,thread_fun,NULL);char str[128]={"1 2 3 4 5 6 7 8 9 10"};char *ptr=NULL;char *s=strtok_r(str," ",&ptr);while(s!=NULL){printf("main:%s\n",s);sleep(1);s=strtok_r(NULL," ",&ptr);}pthread_join(id,NULL);exit(0);
}
5.多线程中执行fork
思考下面两个问题:
(1)多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?
(2)父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?
1.多线程中执行fork,两个面试题
解析1: 代码验证:
线程主要是做一个输出,验证线程的个数;
#include <stdio.h>#include <stdlib.h>#include<unistd.h>#include <string.h>#include <assert.h>#include <pthread.h>void *fun(void *arg){int i=0;for(;i<5;i++){printf("fun run pid=%d\n",getpid());//打印当前进程的PID;sleep(1);}}int main(){pthread_t id;pthread_create(&id,NULL,fun,NULL);fork();int i=0;for(;i<5;i++){printf("main run pid=%d\n",getpid());sleep(1);}}
main run pid=4670 有5个
fun run pid=4670 有5个
main run pid=4672 有5个
再把fork挪动到子线程中观察一下.
结论(1):
fork以后,不管父进程有多少条执行路径,子进程只有一条执行路径.这条执行路径就是fork所在的那条执行路径;
解析2:
关于第二个问题,我们先来看下面一段代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <sys/wait.h>pthread_mutex_t mutex;//在这个线程中,我们模拟持有锁5秒中
void* fun(void *arg)
{pthread_mutex_lock(&mutex);printf("fun lock\n");sleep(5);pthread_mutex_unlock(&mutex);printf("fun unlock\n");
}int main()
{pthread_t id;pthread_mutex_init(&mutex,NULL);pthread_create(&id,NULL,fun,NULL);sleep(1);//保证子线程一定已经加锁了;pid_t pid=fork();//创建一个子进程;if(pid==-1){exit(1);//如果fork失败了,那么我们就退出进程,也就是说主线程和子线程都退出;}if(pid==0){printf("child lock start\n");pthread_mutex_lock(&mutex);printf("child lock success\n");pthread_mutex_unlock(&mutex);exit(0);}wait(NULL);pthread_join(id,NULL);printf("main over\n"); exit(0);
}
为什么呢?
原因如下:
其实就是:fork之后锁的状态也一并被复制了.但是.
因为多进程并发运行,你也不知道某一刻锁的状态到底是什么;也就是锁的状态在子进程中是不清晰的;也就是子进程中锁的状态你也不清楚,那么我们怎么在子进程中使用锁呢?
虽然你可以直接解锁,但是这么做意义就不对了,如果本来是在保护资源,你一来就解锁,那么程序就出现问题了;
结论(2):
父进程有锁,子进程也被复制了锁;锁的状态取决于fork的那一刻父进程的锁的状态.也就是说锁的状态也会被复进去子进程 ;
2.如何解决呢?(拓展了解)
延迟fork的复制.
没有人用锁的时候我们再去fork;那么如何判断有没有人用锁呢?我们去加锁一下,如果没有成功,就是有人用锁.如果加锁成功,就是没有人用锁.这个时候再去fork;
而这个方法(在fork前后去加锁),它是有一个线程的方法可以完成的:pthread_atfork;
man pthread_atfork
int pthread_atfork(void (*prepare)(void), void (*parent)(void),void (*child)(void));
三个参数:每个参数都是一个函数指针;指针指向参数为void,返回值也为void的函数;
//先准备两个函数
void prepare(void)
{pthread_mutex_lock(&mutex);
}
void after(void)
{pthread_mutex_unlock(&mutex);
}
//然后再主函数中对fork进行如下的设置:(放在锁的初始化后面,fork之前即可)
pthread_atfork(prepare,after,after);
6.死锁(重点)
死锁主要发生在有多个依赖锁存在时,会在一个线程试图以另一个线程相反顺序锁住互斥量时发生.
死锁使得一个或多个线程被挂起而无法继续执行,最糟糕的是,这种情况还不容易被发现;
在一个线程中对一个已经加锁的普通锁再次加锁,将导致死锁.
这种情况可能出现在设计得不够仔细地递归函数中.
另外,如果两个线程按照不同的顺序来申请两个互斥锁,也容易发生死锁.
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdio.h>int a=0;
int b=0;
pthread_mutex_t mutex_a;
pthread_mutex_t mutex_b;void *another(void *arg)
{pthread_mutex_lock(&mutex_b);printf("in child thread,got mutex b,waiting for mutex a\n");sleep(5);++b;pthread_mutex_lock(&mutex_a);b+=a++;pthread_mutex_unlock(&mutex_a);pthread_mutex_unlock(&mutex_b);pthread_exit(NULL);
}int main()
{pthread_t id;pthread_mutex_init(&mutex_a,NULL);pthread_mutex_init(&mutex_b,NULL);pthread_create(&id,NULL,another,NULL);pthread_mutex_lock(&mutex_a);printf("in parent thread,got mutex a,waiting for mutex b\n");sleep(5);++a;pthread_mutex_lock(&mutex_b);a+=b++;pthread_mutex_unlock(&mutex_b);pthread_mutex_unlock(&mutex_a);pthread_join(id,NULL);pthread_mutex_destroy(&mutex_a);pthread_mutex_destroy(&mutex_b);exit(0);
}
主线程试图先占有互斥锁mutex_a,然后操作被该锁保护的变量a,但操作完毕之后,主线程并没有立即释放互斥锁mutex_a,而是又申请互斥锁mutex_b,并在两个互斥锁的保护下,操作变量a和b,最后才一起释放这两个互斥锁;
于此同时,子线程则按照相反的顺序来申请互斥锁mutex_a和mutex_b,并在两个锁的保护下操作变量a和b.
我们用sleep函数来模拟连续两次调用pthread_mutex_lock之间的时间差,以确保代码中的两个线程各自先占用一个互斥锁(主线程占有mutex_a,子线程占有mutex_b),然后等待另外一个互斥锁(主线程等待mutex_b,子线程等待mutex_a).这样,两个线程就僵持住了,谁也不能继续往下执行,从而形成死锁.
如果代码中不加入sleep函数,则这段代码或许总能成功地运行,从而为程序留下了一个潜在的bug.
如何避免死锁是使用互斥量应该格外注意的东西. 总体来讲,有几个不成文的基本规则: 对共享资源操作前一定要获得锁. 完成操作以后一定要释放锁. 尽量短时间地占用锁. 如果有多个锁,如获得顺序是ABC连环扣.释放顺序也应该是ABC. 线程错误返回时应该释放它所获得的锁.
死锁相关面试题目(重点):
问题:如何避免死锁?
解答: 死锁的概念:多个进程或线程访问一组竟态资源的时候,出现的永久阻塞的问题。也可以这么说:指两个或两个以上的线程或进程在执行程序的过程中,因争夺资源或者程序推进顺序不当而相互等待的一个现象。 产生的原因主要有三个:系统资源不足,程序运行推进的顺序不当,资源分配不当。或者说:死锁产生的必要条件是:互斥条件、请求和保持条件、不剥夺条件、环路等待条件. 避免死锁就是打破这四个条件中的某一个即可。如某个进程申请多个资源,只要有一个资源不满足暂时就不要分配任何资源。等所有资源能满足时一起分配。
处理死锁的基本方法?
预防死锁、避免死锁(银行家算法)、检测死锁(资源分配)、解除死锁:剥夺资源、撤销进程.
7.拓展部分(仅作了解:线程属性)
如你所见,可以使用的线程属性非常多.但幸运的是,你通常不需要设置太多属性就可以让程序正常工作.
我们一般是用不到线程的属性的;
先看前面我们讲过的例子:
#include <stdio.h>#include <stdlib.h>#include <pthread.h>#include <unistd.h>#include <assert.h>void *fun(void *arg){int i=0;for(;i<10;i++){printf("fun run\n");sleep(1);}pthread_exit(NULL);}int main(){pthread_t id;pthread_create(&id,NULL,fun,NULL);int i=0;for(;i<5;i++){printf("main run\n");sleep(1);}// pthread_join(id,NULL);//注意,一定要屏蔽这一行; exit(0);}
detachedstate:这个属性允许我们无需对线程进行重新合并.
例如:
我们对线程做如下设置:
pthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);pthread_create(&id,&attr,fun,NULL);