LINUX系统编程--6、线程
六 线程
内容:
1 线程的概念
2 线程的创建、线程的终止、线程的取消选项、栈的清理
3 线程同步
4 线程属性、线程同步的属性
6 重入、线程与信号、线程与fork
1 线程的概念
会话是用来承载进程组的,里面可以有一个或多个进程,一个线程中可以有一个或多个线程。线程的本质就是一个正在运行的函数 ,线程没有主次之分(main函数 也只是一个main线程),多个线程之间共享内存,
posix线程是一套标准,而不是实现,我们主要讨论这套标准
- 线程标识 pthead_t 类型不确定
- pthread_equal()
- pthread_self()
shell查看线程:
- ps ax -L :以linux的模式查看当前进程和线程的关系
重点补充!!:
- 并发主要机制有两个:信号和多线程!!
- 信号和多线程这两个机制不能大范围混着用!!(尽量不要混着用)
- 这是两个大的机制。
- 信号主要是用在进程的并发也就是进程间通信。
- 相比较来说,多线程的机制要比信号的机制简单,因为线程的标准做的比信号(多进程)规范。并且要做到线程安全比做到信号方面的安全要简单的多。
- 对这两种机制之一一定要做到熟练。
- 先有的进程和信号,然后才有的他们的标准;而线程是先有的标准,各家再做的实现。所以线程要比信号机制规范得多。
重点2:
- 线程这块,编译的时候要加,-pthread
2 基本函数
2.1基础函数
-
pthread_equal(),比较线程ID。不清楚线程ID类型,不能直接按照int类型数值的比较方法。(ptread_t是什么类型标准未给出,各家的实现不同,所以要有这么一个函数)
-
pthread_self(),取当前线程的线程ID。
2.2 线程的创建
pthread_create()
线程这一块,包括这个函数,失败时不再像进程那样,返回-1再设置errno,而是当失败时直接返回errno!!这样就不能使用perror进行报错,而是使用strerror进行报错。
例子:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
static void * func(void* p)
{
puts("thread is working!");
return NULL;
}
int main()
{
pthread_t tid;
int err;
puts("begin()");
err=pthread_create(&tid,NULL,func,NULL);
if(err)
{
fprintf(stderr,"pthread_creat():%s\n",strerror(err));
exit(1);
}
puts("ends");
exit(0);
}
注意:
- 线程的调度取决于调度器策略,在我们创建的线程还没来得及调度的时候,当前进程就exit(0)结束了。
2.3 线程的终止
线程的终止3种方式:1)线程从启动例程返回,返回值就是线程的退出码
2)线程可以被同一进程的其他线程取消
3)线程调用pthread_exit()函数
这里先看pthrad_exit。
上面的例子写的是:
static void * func(void* p)
{
puts("thread is working!");
return NULL;
}
以后我们尽量换成:
static void * func(void* p)
{
puts("thread is working!");
return pthread_exit();
}
与return 不同,调用 pthread_exit()结束线程会实现线程栈的清理!(就像进程中调用exit会主动调用我们自己定义的钩子函数一样)
2.4 线程的收尸
pthread_join():
相当于 进程阶段的wait()操作,作用是线程收尸,一直等待线程运行结束再收尸。和wait() 不同的时候 可以指定收尸目标,wait()只有收到了才知道收到的是谁。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
static void* func(void* p)
{
puts("Thread is working!");
pthread_exit(NULL);
//return NULL;
}
int main()
{
pthread_t tid;
int err;
puts("Begin!");
err = pthread_create(&tid,NULL,func,NULL);
if(err)
{
fprintf(stderr,"pthread_crearte():%s\n",strerror(err));
exit(1);
}
pthread_join(tid,NULL);//线程收尸,一直等待线程结束后收尸
puts("End!");
exit(0);
}
可以看到 进程是等待tid线程调度结束后才结束进程,就是因为 pthread_join() 线程收尸函数, 一直在等待目标线程结束,后收尸。执行完收尸动作,进程才会exit(0) 结束进程。
可以理解为,pthread_join是阻塞的。
2.5 栈的清理
pthread_cleanup_push();(相当于挂钩子函数)
pthread_cleanup_pop();(相当于取钩子函数)
这两个函数是宏!,两个宏是组合使用,必须成对出现,否则会有语法错误。
// 将函数挂在钩子上。挂载的函数,挂载的函数的参数
void pthread_cleanup_push(void (*routine)(void *), void *arg);
//决定当前从钩子上面取下来的函数是否被调用。 参数决定是否调用
void pthread_cleanup_pop(int execute);
回顾:钩子函数 atexit(),在进程正常终止的时候,该函数将会被调用,并逆序调用挂在钩子上面的函数,这里的逆序调用挂载钩子上面的函数这个操作,我们无法介入,一定会被执行。
pthread_cleanup_push() 函数 类似于 atexit(),挂载钩子函数,而 pthread_cleanup_pop() 用于取下挂在钩子上面的函数,执不执行看参数,相比于 atexit()钩子函数,这里 我们可以自己决定 执行哪个挂载钩子上面的函数,执行起来同样也是逆序。
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
static void cleanup_func(void* p)
{
puts(p);
}
static void* func(void* p)
{
puts("Thread is working!");
pthread_cleanup_push(cleanup_func,"cleanup1");
pthread_cleanup_push(cleanup_func,"cleanup2");
pthread_cleanup_push(cleanup_func,"cleanup3");
puts("push over!");
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_exit(NULL);
//return NULL;
}
int main()
{
pthread_t tid;
int err;
puts("Begin!");
err = pthread_create(&tid,NULL,func,NULL);
if(err)
{
fprintf(stderr,"pthread_crearte():%s\n",strerror(err));
exit(1);
}
pthread_join(tid,NULL);
puts("End!");
exit(0);
}
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ gcc pthread_create1.c -lpthread
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ ./a.out
Begin!
Thread is working!
push over!
cleanup3
cleanup2
cleanup1
End!
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$
如果是pthread_cleanup_pop(0); 则不执行,即只弹栈,而不执行对应函数。就算只弹栈,不执行,也一定要写上,有几个push,就一定要有对应的几个 pop,否则会有语法问题。
2.6 线程取消
pthread_cancel() : 取消线程
pthread_setcancelstate() :设置 是否允许取消
pthread_setcanceltype(): 设置取消方式,异步 或者 推迟
pthread_testcancel(): 这个函数什么都不做,就是设置一个取消点
pthread_detach() : 线程分离
前面说到的 pthread_join() 线程收尸,是必须要等线程执行结束才能收尸,而有时候不需要目标线程执行结束 就要结束他,那么怎么收尸呢?
pthread_cancel() + pthread_join()
为了防止已经申请的资源不能回收,需要设置取消状态,比如,如果需要取消的线程 刚刚open()了一个文件,此时取消,将会导致错过 close(),造成资源泄露。所以需要设置如下 如下状态:
取消有两种状态:
1、允许取消
1)异步取消
2)推迟取消,默认是推迟取消, 推迟到 cancel点 再响应
cancel点:POSIX定义的cancel点,都是可能引发阻塞的系统调用!!
2、不允许取消
取消状态设置的有关函数以及其他函数:
-
int pthread_setcancelstate(int state, int *oldstate); // 设置是否允许取消
-
int pthread_setcanceltype(int type, int *oldtype);// 设置取消方式,异步或者推迟
-
pthread_testcancel - 设置一个取消点,应用于没有任何可能引发阻塞的系统调用的功能函数,即线程中,用于设置取消点,以便取消该线程。
-
pthread_detach() : 线程分离,一般情况下,对于linux中的资源,我们一直遵循 :谁打开,谁关闭。谁申请,谁释放 这回原则。但是也有特殊情况,假如 创建出某个线程 就不再关心它的生死存亡,就可以分离出去,即pthread_detach()。注意 已经 pthread_detach()分离的线程,是不能进行pthread_join() 收尸的。
3 线程的竞争实例
看下面一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <error.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
static void * thr_prime(void *p);
int main()
{
int i,err;
pthread_t tid[THRNUM];
for(i=LEFT;i<=RIGHT;i++)
{
err=pthread_create((tid+i-LEFT),NULL,thr_prime,&i);
if(err)
{
fprintf(stderr,"phtread_creadted err():%s\n",strerror(err));
exit(1);
}
}
for(i=LEFT;i<=RIGHT;i++)
pthread_join(tid[i-LEFT],NULL);
exit(0);
}
void* thr_prime(void *p)
{
int i,j,mark;
i=*(int*)p;
mark=1;
for(j=2;j<i/2;j++)
{
if(i%j==0)
{
mark==0;
break;
}
}
if(mark)
printf("%d is a primer \n",i);
pthread_exit(NULL);
}
这个例子是错误的,会产生竞争,根源在于:
pthread_create((tid+1-LEFT),NULL,thr_prime,&i);
问题出现在i上,这是因为,这里的&i是同一块地址,当主线程创建了很多子线程的时候,若子线程还没有运行,那么原先想分配给这个子线程的地址块中的i就会变化!
解决为方法之一就是,给每个线程分配一个独立的地址块(放传递给线程的参数的!)
下面给出一种解决方法:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <pthread.h>
#include <unistd.h>
#include <wait.h>
#include <string.h>
#define N 5
#define LEFT 30000000
#define RIGHT 30000200
static void *handler(void *p){
int n = *(int *)p;
int i,j,mark;
for (i = LEFT+n;i <= RIGHT;i+=N){
mark = 1;
for (j = 2;j <= i/2;j++){
if (i%j == 0){
mark = 0;
break;
}
}
if (mark) {
printf("%d is a primer [%d]\n",i,n);
}
}
pthread_exit(p);
}
//交叉算法计算 池类算法涉及到竞争
int main()
{
pthread_t Ptid[N];
void *ptr = NULL;
for (int n = 0;n < N;n++){
int *num = malloc(sizeof(int));
*num = n;
int err = pthread_create(Ptid+n,NULL,handler,num);
if (err){
fprintf(stderr,"%s\n",strerror(err));
exit(1);
}
}
int n;
for (n =0 ;n < N;n++){
pthread_join(Ptid[n],ptr);
free(ptr);
}
exit(0);
}
这个程序需要注意的点是:
- 注意pthread_join的第二个参数
- 注意free的位置和join的结合的
- pthread_creat的最后一个参数的使用!通常是malloc的。这里的malloc和free要配合起来使用。使用的位置要注意。
4 竞争故障
为了引出线程同步,这里再看一下竞争故障。(也就是不引入线程同步出现的后果)
例子1:线程间竞争故障
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#define THRNUM 20
#define FNAME "/home/mhr/Desktop/xitongbiancheng/parallel/thread/posix/out"
#define LINESIZE 1024 //提前写好1
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME,"r+");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
fgets(linebuf,LINESIZE,fp);
fseek(fp,0,SEEK_SET);
sleep(1);
fprintf(fp,"%d\n",atoi(linebuf)+1);
fclose(fp);
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid+i,NULL,thr_add,NULL);
if(err)
{
fprintf(stderr,"pthread_create(): %s\n",strerror(err));
exit(1);
}
}
for(i = 0;i < THRNUM; i++)
{
pthread_join(tid[i],NULL);
}
exit(0);
}
若是虚拟机 单核,模拟不出来多核的效果,故在thr_add() 中 sleep(1) 一秒,比作调度器调度其他线程,放大各个线程之间的竞争,看结果发现,创建出来的20个线程,全部都拿到了 FNAME文件,并拿到的值都是1,sleep()后 开始加一写值,导致重复写了20次2值到文件中。(正确结果应该是21)
出现这个问题的原因就是 线程之间的竞争导致的。
5 线程同步之互斥量
互斥量
锁住的是一段代码而不是一个变量
- pthread_mutex_t
- pthread_mutex_init()
- pthread_mutex_destory()
- pthread_mutex_lock()
- pthread_mutex_trylock()
- pthread_mutex_unlock()
- pthread_once() 动态模块的单词初始化函数
互斥量像是bool,非黒即白,没有共享性。
修改上一小节的代码,使其正常工作
注意:保证 同一个时间,只能一个人做的代码 为临界区。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#define THRNUM 20
#define FNAME "/home/mhr/Desktop/xitongbiancheng/parallel/thread/posix/out"
#define LINESIZE 1024
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static void *thr_add(void *p)
{
FILE *fp;
char linebuf[LINESIZE];
fp = fopen(FNAME,"r+");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
pthread_mutex_lock(&mut); //上锁
fgets(linebuf,LINESIZE,fp);
fseek(fp,0,SEEK_SET);
//sleep(1);
fprintf(fp,"%d\n",atoi(linebuf)+1); // 全缓冲,需要fflush 或者 fclose 刷新 才能写到目标文件
fclose(fp);// fprintf(fp 全缓冲,需要fflush 或者 fclose 刷新 才能写到目标文件,所以也在临界区
pthread_mutex_unlock(&mut); //解锁
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid+i,NULL,thr_add,NULL);
if(err)
{
fprintf(stderr,"pthread_create(): %s\n",strerror(err));
exit(1);
}
}
for(i = 0;i < THRNUM; i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mut);
exit(0);
}
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ gcc add.c -lpthread
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ cat out
1
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ ./a.out
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ cat out
21
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ ./a.out
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ cat out
41
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ ./a.out
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$ cat out
61
mhr@ubuntu:~/Desktop/xitongbiancheng/parallel/thread/posix$
上面程序需要注意的是:
- 互斥量要声明为全局变量,原因是:各个线程有独立的栈空间(用户空间栈)–即在某个线程函数中定义的,这是这个线程独占的,不共享! 所以要使各个线程访问同一个变量,要把它声明为一个全局变量。
- 不严格的说,线程之间共享的只是全局变量
- 各个线程的共享资源是很多的,但是各个线程栈空间是不共享的!栈不共享,即使是主进程的栈也不共享。
- 注意销毁互斥量是在main函数中进行的!
- 实际上锁住的是一段代码,而不是一个变量!!当有两个线程执行同一段代码的时候,若这段代码被锁住,那么另一个线程只能阻塞等待。可以理解为,当另一个线程试图加上一把锁的时候,若这把锁(即这个互斥量)已经被别的线程加上锁,那么这个线程就会被阻塞。
- 但是!!!!即使栈不共享,但是一个进程中的所有线程都是可以访问进程中的所有的地址空间的,也就是说,一个线程可以访问另一个线程的数据,只需要知道数据的地址即可!!(即使变量由于栈的原因不共享,但是地址是共享的,所以可以通过地址访问不同线程中的变量)
- 书上的说的是如果互斥量是动态分配的,才需要调用destory,这里都作统一化,不管互斥量是怎么分配的,结束使用后都调用一下destroy
互斥量例子2:
加深理解:规定时间内 四个线程 按顺序 分别不停的向终端 输出 a,b,c,d 每个线程输出一个字母
创建四个互斥锁,四个线程,临界区代码中,每个线程负责持锁,输出,并释放下一个线程的锁。这样 每个线程就可以轮询持锁输出。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#define THRNUM 4
static pthread_mutex_t mut[THRNUM];
//注意这里的,即使不进行静态初始化,但是这个变量也要设置成全局的,因为线程要使用!!
static int next(int n)
{
if(n+1 == THRNUM)
return 0;
return n+1;
}
static void *thr_add(void *p)
{
int n = (int)p;
int c = 'a' + (int)p;
while(1)
{
pthread_mutex_lock(mut+n);
write(1,&c,1);
pthread_mutex_unlock(mut+next(n));//这里会执行 被阻塞的目标线程,当前线程由于没有释放自己持有的锁,会被再次阻塞。
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i = 0; i < THRNUM; i++)
{
pthread_mutex_init(mut+i,NULL);
pthread_mutex_lock(mut+i);
err = pthread_create(tid+i,NULL,thr_add,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create(): %s\n",strerror(err));
exit(1);
}
}
pthread_mutex_unlock(mut+0);//释放线程1的锁
alarm(5);
for(i = 0;i < THRNUM; i++)
{
pthread_join(tid[i],NULL);
}
exit(0);
}
上面程序的几点说明:
- 当没有互斥量机制的时候的输出:若是单核的处理器,会出现aaaaaacccccccbbbbbbdddddddccccccccc…这样类似的输出,因为单核同时只能调度一个线程。若是双核的处理器,那么会出现acacacacacacbdbdbdbdbdb…这样的输出。
- 线程创建的时候没有处理pthread_create的第四个参数的问题!
6 线程的池类算法
还不是真正的线程池
上面一个筛选质数的例子是用201个线程来确定201个数是不是质数,那种做法过于复杂。下面用4个线程来,结合类池类算法,实现筛选质数。
下面用池类算法来筛选质数。
【注】:下面的很多例子在线程创建的时候没有处理pthread_create的第四个参数的问题。这个点非常重要。。自己写程序的时候不能忘了!
图示:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <error.h>
#include <string.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM 4
static int num =0;
static pthread_mutex_t mut_num=PTHREAD_MUTEX_INITIALIZER;
static void * thr_prime(void *p);
int main()
{
int i,err;
pthread_t tid[THRNUM];
for(i=0;i<=THRNUM;i++)
{
err=pthread_create((tid+i),NULL,thr_prime,(void*)&i);
if(err)
{
fprintf(stderr,"phtread_creadted err():%s\n",strerror(err));
exit(1);
}
}
for(i=LEFT;i<=RIGHT;i++)
{
pthread_mutex_lock(&mut_num);
//任务没有被领取
while (num!=0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
任务已经成功下发
num=i;
pthread_mutex_unlock(&mut_num);
}
pthread_mutex_lock(&mut_num);
//任务没有被领取
while(num!=0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
//任务已经成功下发
num=-1;
pthread_mutex_unlock(&mut_num);
for(i=0;i<=THRNUM;i++)
pthread_join(tid[i],NULL);
pthread_mutex_destroy(&mut_num);
exit(0);
}
void* thr_prime(void *p)
{
int i,j,mark;
while (1)
{
pthread_mutex_lock(&mut_num);
while(num==0)
{
pthread_mutex_unlock(&mut_num);
sched_yield();
pthread_mutex_lock(&mut_num);
}
if(num==-1)
{
pthread_mutex_unlock(&mut_num);//跳出临界区的跳转语句一定要unlock
break;
}
i=num;
num=0;
pthread_mutex_unlock(&mut_num);
mark=1;
for(j=2;j<i/2;j++)
{
if(i%j==0)
{
mark==0;
break;
}
}
if(mark)
printf("[%d]%d is a primer \n",(int) p,i);
}
pthread_exit(NULL);
}
注意:
- sched_yield()导致调用线程放弃CPU。线程被移动到队列的末尾以获得其静态优先级,新线程开始运行。
7 线程令牌桶
信号哪一部分,用信号实现了令牌桶,这里使用线程,而不是信号,来实现令牌桶。(回忆,信号和线程是并发的两大机制,线程相比较信号来说比较简单)
头文件与测试主函数见信号篇的令牌桶,这里要改变的是实现文件
//令牌桶
struct mytbf_st{
int csp;
int burst;
int token;
int pos;//任务列表的下标
pthread_mutex_t mut;
};
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t ptid;
static pthread_once_t pth_once = PTHREAD_ONCE_INIT;
static struct mytbf_st *job[MYTBF_MAX];
static volatile int inited = 0;
static int get_free_pos_unlocked(){
for (int i = 0;i < MYTBF_MAX;i++){
if (job[i] == NULL)
return i;
}
return -1;
}
//线程处理函数
static void *handler(void *p){
struct timespec ts;
ts.tv_sec = 1;
ts.tv_nsec = 0;
while(1){
pthread_mutex_lock(&mutex);
for (int i = 0;i < MYTBF_MAX;i++){
if (job[i] != NULL){
pthread_mutex_lock(&job[i]->mut);
job[i]->token += job[i]->csp;
if (job[i]->token > job[i]->burst){
job[i]->token = job[i]->burst;
}
pthread_mutex_unlock(&job[i]->mut);
}
}
pthread_mutex_unlock(&mutex);
nanosleep(&ts,NULL);
}
pthread_exit(NULL);
}
//卸载线程处理模块
static void mod_unload(){
pthread_cancel(ptid);
pthread_join(ptid,NULL);
for (int i = 0;i < MYTBF_MAX;i++){
if (job[i] != NULL){
mytbf_destroy(job[i]);
}
free(job[i]);
}
pthread_mutex_destroy(&mutex);
}
//装载线程处理模块
static void mod_load(){
int err = pthread_create(&ptid,NULL,handler,NULL);
if (err){
fprintf(stderr,"%s\n",strerror(err));
}
atexit(mod_unload);
}
mytbf_t *mytbf_init(int cps,int burst){
struct mytbf_st *tbf;
pthread_once(&pth_once,mod_load);
tbf = malloc(sizeof(*tbf));
if (tbf == NULL){
return NULL;
}
tbf->token = 0;
tbf->csp = cps;
tbf->burst = burst;
pthread_mutex_init(&tbf->mut,NULL);
pthread_mutex_lock(&mutex);
//将新的tbf装载到任务组中
int pos = get_free_pos_unlocked();
if (pos == -1){
free(tbf);
pthread_mutex_unlock(&mutex);
return NULL;
}
tbf->pos = pos;
job[pos] = tbf;
pthread_mutex_unlock(&mutex);
return tbf;
}
//获取token ptr是一个 void * size是用户想要获取的token数
int mytbf_fetchtoken(mytbf_t *ptr,int size){
struct mytbf_st *tbf = ptr;
if (size <= 0){
return -EINVAL;
}
//有token继续
pthread_mutex_lock(&tbf->mut);
while (tbf->token <= 0){
pthread_mutex_unlock(&tbf->mut);
sched_yield();
pthread_mutex_lock(&tbf->mut);
}
int n =tbf->token<size?tbf->token:size;
tbf->token -= n;
pthread_mutex_unlock(&tbf->mut);
//用户获取了 n 个token
return n;
}
//归还token ptr是一个 void *
int mytbf_returntoken(mytbf_t *ptr,int size){
struct mytbf_st *tbf = ptr;
if (size <= 0){
return -EINVAL;
}
pthread_mutex_lock(&tbf->mut);
tbf->token += size;
if (tbf->token > tbf->burst)
tbf->token = tbf->burst;
pthread_mutex_unlock(&tbf->mut);
return size;
}
int mytbf_destroy(mytbf_t *ptr){
struct mytbf_st *tbf = ptr;
pthread_mutex_lock(&mutex);
job[tbf->pos] = NULL;
pthread_mutex_unlock(&mutex);
pthread_mutex_destroy(&tbf->mut);
free(tbf);
return 0;
}
8 线程同步之条件变量
- pthread_cond_t
- pthread_cond_init()
- pthread_cond_destroy()
- pthread_cond_broadcast() :广播形式的唤醒,唤醒所有因 该条件变量cond而阻塞的线程
- pthread_cond_signal():唤醒 因为该条件变量cond而阻塞的所有线程中的任意一个线程
- pthread_cond_wait():阻塞等待条件变量变为真,参数:条件变量,互斥量
- pthread_cond_timewait():非阻塞等待条件变量变为真,超时等待
为什么要有条件变量:为了弥补互斥锁的不足。
互斥锁的不足:线程只有持锁执行,无锁阻塞(pthread_mutex_lock)两种状态,而如果某个线程是 根据 共享数据的某个变化是否发生 作为依据 来做其他操作。那么如果用互斥锁来实现的话,那么该线程会重复如下操作:
申请持锁成功
判断 (共享数据的某个变化是否发生)
如果有发生,执行其他操作
如果没发生,解锁,(加sleep()会影响效率)并重新申请持锁
申请持锁失败 阻塞(pthread_mutex_lock) 等待持锁
如果 共享数据的某个变化 需要比较久才发生,那么 该线程就会反复执行上面的操作,造成CPU处理时间的浪费。有人会说可以在改伪代码中加 sleep()节省CPU处理时间,但是这样的后果是,不能保证该进程以最快的速度 查到 共享数据的某个变化,影响效率。即类似情况,互斥锁,会有时间和效率的问题。
所以为了解决上述时间和效率问题,引入条件变量.
- 条件变量允许线程阻塞并等待其他线程给自己发送信号唤醒己。兼顾CPU处理时间和效率,这样就弥补了互斥锁的不足。
关于pthread_cond_wait:
-pthread_cond_wait() 用于阻塞线程 并 将线程挂载到 cond条件变量的 阻塞线程队列中,然后解锁,最后等待唤醒。被唤醒之后会抢锁,如果只有自己一个阻塞线程,则被唤醒后 直接持锁。
使用pthread_cond_wait方式如下:
pthread _mutex_lock(&mutex)
while或if(线程执行的条件是否成立)
pthread_cond_wait(&cond, &mutex);
线程执行
pthread_mutex_unlock(&mutex);
问题一 : 为什么要先持锁
答:条件变量是为了弥补互斥锁的不足,两者都是为了 同一时间只能有一个线程访问临界资源,所以必然要加锁。
问题二:pthread_cond_wait() 怎么防止 持锁阻塞,即如果在没有解锁的条件下进入阻塞状态,这一行的话,其他线程将无法访问临界资源。
答:pthread_cond_wait() 内部的实现可以理解为,先将线程挂载到 阻塞队列中,然后解锁。所以在函数内部,已经解锁了。至于函数内部解锁的时机是很重要的,函数是在将 线程挂载到阻塞队列之后 才解锁。这样做的目的是,保证在将线程挂载到阻塞队列之前,其他线程无法改变临界资源。试想一下,如果先解锁,再挂载,会发生什么情况,如果解锁之后,其他线程拿到锁,并改变了临界资源,并发送信号 pthread_cond_signal(), 就会导致 本线程无法响应本次 临界资源的变化。
情景:
线程持锁,并判断条件是否成立,不成立的话,pthread_cond_wait() 阻塞线程 并 将线程挂载到 cond条件变量的 阻塞线程队列中,然后解锁,最后等待唤醒。pthread_cond_wait() 被唤醒之后会抢锁,如果只有自己一个阻塞线程,则被唤醒后 直接持锁, 并开始判断条件是否成立。。。。 成立的话 跳出循环,执行下一步。
实例1:
修改上面的类池类算法的筛质数问题,加上条件变量。
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;;
static int num = 0;
static void *handler(void *p){
int task,mark;
while(1){
pthread_mutex_lock(&mutex);
while(num == 0){
pthread_cond_wait(&cond,&mutex);
}
if (num == -1){
pthread_mutex_unlock(&mutex);
break;
}
task = num;
num = 0;//成功领取任务
//通知所有线程任务被领取走了
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
mark = 1;
for (int j = 2;j <= task/2;j++){
if (task%j == 0){
mark = 0;
break;
}
}
if (mark) {
printf("[%d] %d is a priamer\n",*(int *)p,task);
}
}
pthread_exit(NULL);
}
//池类算法
int main()
{
pthread_t Ptid[THRNUM];
for (int n = 0;n < THRNUM;n++){
int *num = malloc(sizeof(int));
*num = n;
int err = pthread_create(Ptid+n,NULL,handler,num);
if (err){
fprintf(stderr,"%s\n",strerror(err));
exit(1);
}
}
for (int i = LEFT;i <= RIGHT;i++){
pthread_mutex_lock(&mutex);
//任务没有被领取
while(num != 0){
pthread_cond_wait(&cond,&mutex);
}
//任务已经成功下发
num = i;
//叫醒任意一个线程执行任务
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
pthread_mutex_lock(&mutex);
//任务没有被领取
while(num != 0){
pthread_cond_wait(&cond,&mutex);
}
//任务已经成功下发
num = -1;
//广播给所有下游线程 任务结束
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
int n;
for (n =0 ;n < THRNUM;n++){
pthread_join(Ptid[n],NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
条件变量可以解决互斥量进行盲等的问题即实现了通知法(),通知互斥量什么时候上锁。
例二:
用条件变量通知法实现 :规定时间内四个线程按顺序 分别不停的向终端 输出a,b,c,d每个线程输出一个字母。
规范的写法:用条件变量实现 通知法:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <unistd.h>
#define THRNUM 4
static int num = 0;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static int next(int n)
{
if(n+1 == THRNUM)
return 0;
return n+1;
}
static void *thr_add(void *p)
{
int n = (int)p;//0 1 2 3
int c = 'a' + (int)p;
while(1)
{
pthread_mutex_lock(&mut);
while(num != n)//判断是不是目标下一个线程
pthread_cond_wait(&cond,&mut);//被唤醒的所有线程抢锁,抢到锁的线程 判断条件是否成立。。。
write(1,&c,1);
num = next(num);//准备唤醒下一个线程
pthread_cond_broadcast(&cond);//惊群
pthread_mutex_unlock(&mut);
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
int i,err;
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid+i,NULL,thr_add,(void *)i);
if(err)
{
fprintf(stderr,"pthread_create(): %s\n",strerror(err));
exit(1);
}
}
//alarm(5);
for(i = 0;i < THRNUM; i++)
{
pthread_join(tid[i],NULL);
}
pthread_mutex_destroy(&mut);
pthread_cond_destroy(&cond);
exit(0);
}
9 线程同步之信号量
通过互斥量与条件变量的配合我们可以实现信号量信号量像是一个激活函数 当这个变量超过阈值时 将会触发条件变量给互斥量上锁
下面是一个实战:
这个例子有三个文件
//mysem.h
#ifndef MYSEM_H_
#define MYSEM_H_
typedef void mysem_t;
mysem_t *mysem_init(int initval);
int mysem_add(mysem_t *ptr,int n);
int mysem_sub(mysem_t *ptr,int n);
int mysem_destory(mysem_t *ptr);
mysen_init(int initval);
#endif
//mysem.c
#include "mysem.h"
struct mysem_st{
int vaclue;
pthread_mutex_t mutex;
pthread_cond_t cond;
};
mysem_t *mysem_init(int initval){
struct mysem_st *sem;
sem = malloc(sizeof(*sem));
if (sem == NULL){
return NULL;
}
sem->vaclue = initval;
pthread_mutex_init(&sem->mutex,NULL);
pthread_cond_init(&sem->cond,NULL);
return sem;
}
int mysem_add(mysem_t *ptr,int n){
struct mysem_st *sem = ptr;
pthread_mutex_lock(&sem->mutex);
sem->vaclue += n;
pthread_cond_broadcast(&sem->cond);
pthread_mutex_unlock(&sem->mutex);
return n;
}
int mysem_sub(mysem_t *ptr,int n){
struct mysem_st *sem = ptr;
pthread_mutex_lock(&sem->mutex);
while(sem->vaclue < n){
pthread_cond_wait(&sem->cond,&sem->mutex);
}
sem->vaclue -= n;
pthread_mutex_unlock(&sem->mutex);
return n;
}
int mysem_destory(mysem_t *ptr){
struct mysem_st *sem = ptr;
pthread_mutex_destroy(&sem->mutex);
pthread_cond_destroy(&sem->cond);
free(sem);
return 0;
}
//main.c
#define THRNUM 20
#define N 5
#define LEFT 30000000
#define RIGHT 30000200
static mysem_t *sem;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;;
static int num = 0;
static void *handler(void *p){
int task,mark= 0;
pthread_mutex_lock(&mutex);
while(num == 0){
pthread_cond_wait(&cond,&mutex);
}
if (num == -1){
pthread_mutex_unlock(&mutex);
mysem_add(sem,1);
pthread_exit(NULL);
}
task = num;
num = 0;//成功领取任务
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
mark = 1;
for (int j = 2;j <= task/2;j++){
if (task%j == 0){
mark = 0;
//归还计算资源
sleep(1);
mysem_add(sem,1);
pthread_exit(NULL);
}
}
if (mark) {
printf("[%d] %d is a priamer\n",*(int *)p,task);
}
sleep(1);
//归还计算资源
mysem_add(sem,1);
pthread_exit(NULL);
}
//池类算法
int main()
{
pthread_t Ptid[THRNUM];
sem = mysem_init(N);//初始化计算资源
for (int i = LEFT;i <= RIGHT;i++){
mysem_sub(sem,1);//消耗一个计算资源
int *ptid = malloc(sizeof(int));
*ptid = i-LEFT;
int err = pthread_create(Ptid+(i-LEFT)%THRNUM,NULL,handler,ptid);
if (err){
fprintf(stderr,"%s\n",strerror(err));
exit(1);
}
pthread_mutex_lock(&mutex);
//任务没有被领取
while(num != 0){
pthread_cond_wait(&cond,&mutex);
}
//任务已经成功下发
num = i;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
}
pthread_mutex_lock(&mutex);
//任务没有被领取
while(num != 0){
pthread_cond_wait(&cond,&mutex);
}
//任务已经成功下发
num = -1;
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mutex);
int n;
for (n =0 ;n < THRNUM;n++){
pthread_join(Ptid[n],NULL);
}
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
exit(0);
}
10 线程属性、线程同步属性
- 一般而言,百分之八十的情况下是不需要设置线程属性以及线程同步属性的。
- 线程同步属性包括互斥量属性、读写锁属性、条件变量属性等
11 多线程中的重入
这里需要和信号中的异步信号安全相比较。
12 线程和信号
理论上来讲,信号和线程是并发的两种大机制,不能大面积的混用。但是不可避免的会有些场景需要两者同时使用。
13 openmp
POSIX线程标准是一个标准,而不是实现。是先有标准、后有实现。所以大多数的库函数的实现都会遵循这个标准,所以大部分是线程安全的,使用的时候不需要考虑一些零碎的东西。而信号那一块,是先有的实现后有的标准,所以使用的时候要考虑很多东西,如是不是可重入等
openmp也是一个线程标准。是一套跨语言、简单的并发标准。借助语法标记和编译器。