一个信号从发出到收到,有一个不可避免的时延。所以如果使用信号来计时的话,10ms以内的计时不准确。只要超过这个时间,基本上都能准确的用信号来计时。
alarm没有办法直接实现多任务的计时器,因为当程序中有多个alarm时,程序计时可能就会以最后一个alarm为准,所以alarm不能直接用于多任务的计时器。因此,我们需要思考怎么使用alarm这样一个单一的计时器来实现10秒做一个事,5秒做一个事,1秒做一个事的多任务计时器。
NAME
pause - wait for signal
SYNOPSIS
#include <unistd.h>
int pause(void);
DESCRIPTION
pause() causes the calling process (or thread) to sleep until a signal is delivered that either terminates the process or causes the invocation of asignal-catching function.
Pause()导致调用(当前正在运行的)进程(或线程)进入休眠状态,直到发送了终止进程或调用信号捕获函数的信号。
我们需要强调的是,有时我们使用sleep延时虽然能实现我们想要的效果,但是sleep只能用来调试,不能用到发布的源码当中,原因是sleep对于不同的平台并不通用,在有的平台上面sleep是由alarm和pause两者封装在一起实现的,假如在你当前程序中还有一个alarm,那么是不是就修改了我当前的定时时间,程序功能是不是就有问题了。也就是说sleep不具有兼容性。
下面,我们来使用alarm做一个定时循环的实验(一个程序疯狂的跑5s)。
第一个版本,使用time函数来实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
time_t end;
int64_t count=0;//为了防止溢出,使用64位的整型数来计时
//当前的时间戳 + 5
end =time(NULL)+5;
//也就是计时5s
while(time(NULL)<=end)
{
count++;
}
printf("%ld\n",count);
return 0;
}
运行时,我们使用time命令来统计程序的实际运行时间,并把结果输出到当前目录下的log文件中。
系统会从按时间那一刻开始计算,会把5.001s – 5.9999s都看作是5s的时间。这样一看,这个误差比例还是挺高的,近乎相差1s。但如果计时时间越长,比如计时10000s,误差也是相差1s,从这方面来看,误差比例就相对小很多了。
第二个版本,使用信号alarm函数来实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
int loop = 1;
void alarm_handler(int s)
{
loop = 0;
}
int main()
{
int64_t count=0;//为了防止溢出,使用64位的整型数来计时
signal(SIGALRM,alarm_handler);
alarm(5);
//也是计时5s
while(loop)
{
count++;
}
printf("%ld\n",count);
return 0;
}
程序中的语句
signal(SIGALRM,alarm_handler);
一定要放在
alarm(5);
之前。
同样,运行时,我们使用time命令来统计程序的实际运行时间。
real 时间= user时间 +sys时间,我们看user的时间。
可以看出,使用alarm控制的时间更精确。
流控(流量控制):每秒钟按规定的字符输出。(音、视频播放器中每秒播放多少字节,网络传输中,每秒固定传输多少字节的内容中广泛应用。)
这里要实现的一个具体的功能是:
实现cat的功能,但是将一次性显示改成每秒显示固定个数的字符。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//每秒传输的字符的个数
#define CPS 10
#define BUFSIZE CPS
int main(int argc,char* argv[])
{
int sfd,dfd=1;
char buf[BUFSIZE];
int len,ret,pos;
if(argc < 2)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
do
{
sfd = open(argv[1],O_RDONLY);
if(sfd < 0)
{
if(errno!=EINTR)
{
perror("open()");
exit(1);
}
}
}while(sfd<0);
while(1)
{
len = read(sfd,buf,BUFSIZE);
if(len < 0)
{
if(errno == EINTR)
continue;
perror("read()");
break;
}
if(len == 0)
{
break;
}
pos = 0;
while(len > 0)
{
ret = write(dfd,buf+pos,len);
if(ret < 0)
{
if(errno == EINTR)
continue;
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
sleep(1);//等待1s
}
close(sfd);
return 0;
}
需要说明的是,使用sleep延时虽然能实现想要的效果,但是sleep只能用作调试来用,不能用到发布的源码当中,原因是sleep对于不同的平台并不通用,不具有兼容性。
因此,下面我们用alarm()函数来实现
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//每秒传输的字符的个数
#define CPS 10
#define BUFSIZE CPS
static volatile int loop = 1;
static void alarm_handler(int s)
{
alarm(1);
loop = 1;
}
int main(int argc,char* argv[])
{
int sfd,dfd=1;
char buf[BUFSIZE];
int len,ret,pos;
if(argc < 2)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
signal(SIGALRM,alarm_handler);
alarm(1);
do
{
sfd = open(argv[1],O_RDONLY);
if(sfd < 0)
{
if(errno!=EINTR)
{
perror("open()");
exit(1);
}
}
}while(sfd<0);
while(1)
{
while(!loop)
{
//等待SIGALRM信号的到来,程序才继续执行。有点类似于sleep。
//可避免忙等(什么都不做)。
pause();
}
loop = 0;
while((len = read(sfd,buf,BUFSIZE))<0)
{
//如果是假错,比如遇到中断
if(errno == EINTR)
//就跳出当前循环体,重新执行当前层的while循环
continue;
//如果是真错,就退出循环体
perror("read()");
break;
}
if(len == 0)
{
break;
}
pos = 0;
while(len > 0)
{
ret = write(dfd,buf+pos,len);
if(ret < 0)
{
if(errno == EINTR)
continue;
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
}
close(sfd);
return 0;
}
从上面的实现来看,我们可以发现我们所实现的这个漏桶功能的特点是,假设我们从一个(设备)文件中读数据,不管数据量来的很小还是数据量特别大的时候,程序都是一秒钟不紧不慢的最多输出10个字节大小的数据,这样就显得不够灵活。
令牌桶的功能相比于漏桶更加灵活,程序可以在漏桶的基础上进行修改,我们漏桶的权限是每间隔1秒最多能输出10个字节大小的数据,我们想改成当读数据时,没有读到数据的时候就攒权限,当数据量大的时候,就可以根据积攒的权限多输出一些数据。这样也可以避免像漏桶那样一直在忙等。
比如还是一秒钟能输出10个字节的数据,现在,没读到数据,我们就开始积攒权限,假设积攒三秒,就积攒到了连续三次输出10个字节的数据量。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
//每秒传输的字符的个数
#define CPS 10
#define BUFSIZE CPS
//规定一个上限,权限(令牌)不能一直积攒
#define BURST 100
//token -- 令牌
static volatile int token = 0;
//如果程序一开始就输出数据,就让token=1,
//如果一开始就让它积攒权限,1秒后再输出,就让token=0
//如果模拟等待一段时间,token积攒到某个数值时,突然有
//大量数据来的的输出效果,就给token>1的初始值
static void alarm_handler(int s)
{
alarm(1);
token++;
if(token > BURST)//如果token的值大于上限
token = BURST;//让token的值保持在上限不动
}
int main(int argc,char* argv[])
{
int sfd,dfd=1;
char buf[BUFSIZE];
int len,ret,pos;
if(argc < 2)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
signal(SIGALRM,alarm_handler);
alarm(1);
do
{
sfd = open(argv[1],O_RDONLY);
if(sfd < 0)
{
if(errno!=EINTR)
{
perror("open()");
exit(1);
}
}
}while(sfd<0);
while(1)
{
while(token <= 0)
pause();
//读写一圈消耗一个token
token-=1;
//假设这里读打印机数据,一开始没读到数据,会阻塞到这,恰好1s时间到了,
//被信号打断,程序会执行alarm信号指定的函数,token++,执行完之后,程序返回到这里,
//就会被判断为假错,然后重新执行while循环,当仍然没有读到数据,循环上述步骤。
//假设在第三秒后,数据来了,此时token = 3。
while((len = read(sfd,buf,BUFSIZE))<0)
{
//如果是假错
if(errno == EINTR)
//就跳出当前循环体,重新执行while循环
continue;
//如果是真错,就退出循环体
perror("read()");
break;
}
if(len == 0)
{
break;
}
//接上面的分析
//程序开始写数据,每次仍然写10个字节大小的数据,因为有充足的token,所以可以
//连续的写,而不需要靠执行信号捕获函数来积攒token,这样就给人一种一
//次读写多个字节大小的数据的感觉。
pos = 0;
while(len > 0)
{
ret = write(dfd,buf+pos,len);
if(ret < 0)
{
if(errno == EINTR)
continue;
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
}
close(sfd);
return 0;
}
令牌桶的要素:
1、我们需要指定不同的流速(CPS)
2、每个令牌桶的令牌个数一定有一个上限(BURST)
3、还要有一个变量来存放当前的令牌数。(token)
令牌桶封装成结构体,挂接在数组中。
在函数功能上,原先的
" 一个token一次能输出cps个字符,一次加1个token "
改写成
“一个token一次能输出一个字符,每次加cps个token”
最关键的问题是,捋着数组走,给每个非空的令牌桶都加上token。初始的alarm只能执行一次。
token.h
封装的很彻底
#ifndef _TOKEN_H_
#define _TOKEN_H_
//最多能支持1024个不同速率的令牌桶
#define MYTBF_MAX 1024
typedef void mytbf_t;
//void* 赋值给任何类型的指针都天经地义
//任何类型的指针赋值给void*也都天经地义
//用户指定令牌桶的每秒传输速率和令牌上限
//实际上应该是struct mytbf_st *mytbf_init(int cps,int burst);
mytbf_t *mytbf_init(int cps,int burst);//一定会申请空间
//使用令牌桶,无非两个动作
//1、取令牌,取了多少通过返回值返回
int mytbf_fetchtoken(mytbf_t*, int);
//2、取多了,要还回去,还回去多少,也要以返回值形式返回
int mytbf_returntoken(mytbf_t*,int);
int mytbf_destory(mytbf_t*);//释放空间
#endif
token.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include "token.h"
typedef void (*sighandler_t)(int);
static int inited = 0;
static sighandler_t alarm_handler_save;
struct mytbf_st
{
int cps;
int burst;
int token;
int pos;
};
//结构体指针数组
static struct mytbf_st* job[MYTBF_MAX];
static void alarm_handler(int s)
{
int i;
alarm(1);
for(i=0;i<MYTBF_MAX;i++)
{
if(job[i] != NULL)
{
job[i]->token +=job[i]->cps;
if(job[i]->token >= job[i]->burst)
job[i]->token = job[i]->burst;
}
}
}
static void module_unload(void)
{
//从全局考虑,程序作为一个模块被加载到一个工程中
//别人出去不再使用alarm时钟,所以我们在退出后,
//还原信号原有的行为,要将alarm关闭。
int i;
//恢复原来的闹钟行为
signal(SIGALRM,alarm_handler_save);
alarm(0);//关闭时钟
//程序正常终止,将没有释放的内容给释放掉
for(i=0;i<MYTBF_MAX;i++)
{
free(job[i]);
}
}
static void module_load(void)
{
//保存信号初始行为,并且定义对应于alarm_handler的新行为
alarm_handler_save = signal(SIGALRM,alarm_handler);
alarm(1);
//钩子函数-程序正常终止,调用module_unload
atexit(module_unload);
}
static int get_free_pos(void)
{
int i;
for(i=0;i<MYTBF_MAX;i++)
{
if(job[i]==NULL)
{
return i;
}
}
return -1;
}
mytbf_t *mytbf_init(int cps,int burst)
{
struct mytbf_st* mytbf;
int pos=0;
if(!inited)
{
module_load();
inited = 1;
}
//找出结构体指针数组中的空位,如果找到了返回下标
pos = get_free_pos();
if(pos < 0)
{
return NULL;
}
mytbf = malloc(sizeof(*mytbf));
if(mytbf == NULL)
return NULL;
mytbf->token = 0;
mytbf->cps = cps;
mytbf->burst = burst;
mytbf->pos =pos;
job[pos]=mytbf;
return mytbf;
}
static int min(int a,int b)
{
if(a<b)
{
return a;
}
return b;
}
int mytbf_fetchtoken(mytbf_t* ptr, int size)
{
int n;
struct mytbf_st *mytbf = ptr;
if(size<=0)
{
//参数非法
return -EINVAL;
}
//我当前也没有token
while(mytbf->token <= 0)
{
pause();
}
n = min(mytbf->token,size);
mytbf->token-=n;
return n;
}
//2、多了,要还回去,还回去多少,也要以返回值形式返回
int mytbf_returntoken(mytbf_t* ptr,int size)
{
struct mytbf_st *mytbf = ptr;
if(size <=0)
{
//参数非法
return -EINVAL;
}
mytbf->token+=size;
if(mytbf->token > mytbf->burst)
mytbf->token = mytbf->burst;
return size;
}
//ptr是void*类型
int mytbf_destory(mytbf_t* ptr)//释放空间
{
struct mytbf_st *mytbf = ptr;
job[mytbf->pos]=NULL;
if (ptr != NULL)
{
free(ptr);
ptr = NULL;
}
mytbf = NULL;
}
main.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include "token.h"
//每个令牌对应的能传输的字符的个数
#define CPS 10
//想要申请的令牌数
#define BUFSIZE 1024
//规定一个上限,权限(令牌)不能一直积攒
#define BURST 1024
int main(int argc,char* argv[])
{
int sfd,dfd=1;
char buf[BUFSIZE];
int len,ret,pos;
int size;
mytbf_t *tbf = NULL;
if(argc < 2)
{
fprintf(stderr,"Usage...\n");
exit(1);
}
tbf = mytbf_init(CPS,BURST);
if(tbf == NULL)
{
fprintf(stderr,"mytbf_init(),failed!\n");
exit(1);
}
do
{
sfd = open(argv[1],O_RDONLY);
if(sfd < 0)
{
if(errno!=EINTR)
{
perror("open()");
exit(1);
}
}
}while(sfd<0);
while(1)
{
//我想要BUFSIZE个令牌
size = mytbf_fetchtoken(tbf,BUFSIZE);
if(size < 0)
{
fprintf(stderr,"mytbf_fetchtoken():%s.\n",strerror(-size));
exit(1);
}
//能获得多少个令牌,我就先读多少个字节
//len是实际读到的字节数
while((len = read(sfd,buf,size))<0)
{
//如果是假错
if(errno == EINTR)
//就跳出当前循环体,重新执行while循环
continue;
//如果是真错,就退出循环体
perror("read()");
break;
}
//读到文件的末尾就直接退出
if(len == 0)
{
break;
}
//如果还剩token,就返还回去。
if(size-len >0)
{
mytbf_returntoken(tbf,size-len);
}
pos = 0;
while(len > 0)
{
ret = write(dfd,buf+pos,len);
if(ret < 0)
{
if(errno == EINTR)
continue;
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
}
close(sfd);
mytbf_destory(tbf);
tbf = NULL;
return 0;
}
makefile
token:main.o token.o
gcc -o token $^
%.o: %.c
gcc -c -o $@ $<
clean:
rm *.o token
.PHONY:clean
为什么说对job数组操作应该使用互斥量(加锁)呢?
想象一下,当前有多个线程几乎同时在操作mytbf_init这个函数,那么他们都会去找出结构体指针数组job中的空位,那么势必会产生竞争。因此,对job数组操作应该使用互斥量(加锁)。
应该定义成什么性质的互斥量呢?
应该定义成一个(全局)互斥量,相当于1024个令牌桶在用同一把锁。
对于job数组来说,使用同一把锁是合适的。
但对于同一个令牌桶来说,创建出来的线程每过一秒会加token,而main线程也会执行加token或减token,这势必会产生竞争,因此也需要使用互斥量。
那这个互斥量应该定义成全局的吗?
这个不合适的,因为对于不同的令牌桶来说,加token和减token是不相关的,因此,再用同一把锁就不合适了。
NAME
pthread_once - once-only initialization
对某个模块执行单次初始化
SYNOPSIS
#include <pthread.h>
pthread_once_t once_control = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *once_control, void (*init_routine) (void));
即使在mian函数中有两个线程来调 mytbf_init 或 执行两次 mytbf_init,mytbf_init 也只被调一次。操作的都是同一个job数组。
上面的mian.c和token.h都不用改动。只需要修改token.c文件和makefile文件。
token.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
#include <string.h>
#include "token.h"
pthread_t tid_alarm;
static pthread_once_t once_control = PTHREAD_ONCE_INIT;
struct mytbf_st
{
int cps;
int burst;
int token;
int pos;
pthread_mutex_t mut_token;
};
//应该做为一个独占的资源被使用
//结构体指针数组
static struct mytbf_st* job[MYTBF_MAX];
//定义一个互斥量,要通过名字来体现是对谁的限制和约束
static pthread_mutex_t mut_job = PTHREAD_MUTEX_INITIALIZER;
//把时间到了,加token这个函数用一个线程来完成
static void* thr_alarm(void* p)
{
int i;
while(1)
{
//进入楼层后,先锁整栋楼的门,再锁每个房间的门
//离开楼层后,先开每个房间的门,再开整栋楼的门
pthread_mutex_lock(&mut_job);
for(i=0;i<MYTBF_MAX;i++)
{
if(job[i] != NULL)
{
pthread_mutex_lock(&job[i]->mut_token);
job[i]->token +=job[i]->cps;
if(job[i]->token >= job[i]->burst)
job[i]->token = job[i]->burst;
pthread_mutex_unlock(&job[i]->mut_token);
}
}
pthread_mutex_unlock(&mut_job);
sleep(1);//1秒钟做一次,调试时可用,发布时应使用更安全的延时1s的函数.
}
}
static void module_unload(void)
{
int i;
//因为线程在执行一个死循环,不会返回,所以要先给该线程给取消
pthread_cancel(tid_alarm);
//再回收,不关心退出线程退出时的状态.
pthread_join(tid_alarm,NULL);
//程序正常终止,将没有释放的内容给释放掉
for(i=0;i<MYTBF_MAX;i++)
{
if(job[i]!=NULL)
{
mytbf_destory(job[i]);
}
}
//互斥量mut_job一定是在模块卸载时销毁
pthread_mutex_destroy(&mut_job);
}
static void module_load(void)
{
int err;
//创建线程
err = pthread_create(&tid_alarm,NULL,thr_alarm,NULL);
if(err)
{
fprintf(stderr,"pthread_creat() %s.\n",strerror(err));
exit(1);
}
//钩子函数---程序正常终止,调用module_unload
atexit(module_unload);
}
//两个人同时调用get_free_pos,有可能找到同一个空闲的下标,从而发生冲突
//从名字上体现出未加锁版本,但你要使用它,请加锁之后再调用.
static int get_free_pos_unlocked(void)
{
int i;
//理论上,你在查看job[i]数组,是不是应该加锁
//但是,别忘了,你是怎么到这个函数这的,你是
//加了锁之后,然后来到这儿的,如果在这要加锁,
//就形成了二次加锁,虽然,互斥量有一个属性可以让我们
//二次加锁成功,但是这里是不符合的,另外,程序中没有人在别的地方
//调用这个函数。所以加锁是没有必要的,但我们要在函数名称上
//给出提示,表明这个函数是没有加锁的,但你如果要使用这个函数,
//请先加锁再使用这个函数。
for(i=0;i<MYTBF_MAX;i++)
{
if(job[i]==NULL)
{
return i;
}
}
return -1;
}
mytbf_t *mytbf_init(int cps,int burst)
{
struct mytbf_st* mytbf;
int pos=0;
//假设有两个线程前后脚来调用mytbf_init这个函数
//几乎同时来判断inited这个全局变量的值,结果两个
//线程的inited都为零,结果都能执行module_load()
//这显然是不可以的。
/*
if(!inited)
{
module_load();
inited = 1;
}
*/
//那有解决的方法吗?
//1、再次加锁
//2、使用模块的单次初始化函数pthread_once
pthread_once(&once_control, module_load);
//都跟job数组没有关系,因此可以放在临界区外面来实现
mytbf = malloc(sizeof(*mytbf));
if(mytbf == NULL)
{
return NULL;
}
mytbf->token = 0;
mytbf->cps = cps;
mytbf->burst = burst;
//初始化结构体的成员互斥量mut_token
pthread_mutex_init(&mytbf->mut_token,NULL);
//找出结构体指针数组中的空位,如果找到了返回下标
pthread_mutex_lock(&mut_job);
//尝试对临界区进行优化,临界区应该越短越好
//尝试对临界区进行优化,临界区应该越短越好
//临界区内的函数跳转也是一个问题,比如下面的get_free_pos永远也回不来,程序也将死锁
pos = get_free_pos_unlocked();
if(pos < 0)
{
pthread_mutex_unlock(&mut_job);
//临界区内的任何一个跳转语句一定要记得解锁之后再跳转,
//否则你跳转了,一直没解锁,别人也就一直进不去
free(mytbf);
mytbf = NULL;
return NULL;
}
mytbf->pos =pos;
job[pos]=mytbf;
pthread_mutex_unlock(&mut_job);
return mytbf;
}
static int min(int a,int b)
{
if(a<b)
{
return a;
}
return b;
}
int mytbf_fetchtoken(mytbf_t* ptr, int size)
{
int n;
struct mytbf_st *mytbf = ptr;
if(size<=0)
{
//参数非法
return -EINVAL;
}
//查看我当前有没有token
//要加锁查看
pthread_mutex_lock(&mytbf->mut_token);
while(mytbf->token <= 0)
{
//不满足条件,就先解锁
pthread_mutex_unlock(&mytbf->mut_token);
//等待--短暂出让调度器
sched_yield();
//再抢锁,此时发现大于0,退出循环体,向下执行
pthread_mutex_lock(&mytbf->mut_token);
}
n = min(mytbf->token,size);
//当程序执行到这想加token,但另一个线程恰好想要减token,
//就会引起竞争和冲突,因此仍然需要使用到互斥量,但是这个互斥量
//的定义需要来分析下.能否和上面定义的一样使用全局互斥量呢?
mytbf->token-=n;
//对token的操作到此为止,将其解锁
pthread_mutex_unlock(&mytbf->mut_token);
return n;
}
//2、多了,要还回去,还回去多少,也要以返回值形式返回
int mytbf_returntoken(mytbf_t* ptr,int size)
{
struct mytbf_st *mytbf = ptr;
if(size <=0)
{
//参数非法
return -EINVAL;
}
pthread_mutex_lock(&mytbf->mut_token);
mytbf->token+=size;
if(mytbf->token > mytbf->burst)
mytbf->token = mytbf->burst;
pthread_mutex_unlock(&mytbf->mut_token);
return size;
}
//ptr是void*类型
int mytbf_destory(mytbf_t* ptr)//释放空间
{
struct mytbf_st *mytbf = ptr;
pthread_mutex_lock(&mut_job);
job[mytbf->pos]=NULL;
pthread_mutex_unlock(&mut_job);
//因为这个互斥量是结构体成员变量,
//所以要在释放内存区域之前回收。
pthread_mutex_destroy(&mytbf->mut_token);
if (mytbf != NULL)
{
//释放令牌桶空间
free(mytbf);
mytbf = NULL;
}
}
makefile
CFLAGS+=-pthread
LDFLAGS+=-pthread
all:token
token:main.o token.o
gcc -o token $^ $(LDFLAGS)
%.o: %.c
gcc $(CFLAGS) -c -o $@ $<
clean:
rm *.o token
.PHONY:clean
我们上面写的是忙等版本,忙在那呢?
忙在函数 mytbf_fetchtoken 中的这一段
//查看我当前有没有token
//要加锁查看
pthread_mutex_lock(&mytbf->mut_token);
while(mytbf->token <= 0)
{
//不满足条件,就先解锁
pthread_mutex_unlock(&mytbf->mut_token);
//等待--短暂出让调度器
sched_yield();
//再抢锁,此时发现大于0,退出循环体,向下执行
pthread_mutex_lock(&mytbf->mut_token);
}
我一秒钟会在这个地方执行上亿次,不停的加锁查看我当前有没有token,,解锁等待。
那我们能不能利用条件变量将其改成通知机制呢?
可以的,我们可以在while循环中使用函数pthread_cond_wait来等待当前token的值发生变化。但要和线程消息函数pthread_cond_broadcast或pthread_cond_signal相互配合,具体使用pthread_cond_broadcast还是使用
pthread_cond_signal,稍后分析。此外,我们还要分析pthread_cond_wait这个函数为什么能够实现这一目的。
但在做这些之前,我们需要使用条件变量的动态定义方式来初始化条件变量。因为,我们要控制的是每个令牌桶(结构体)中的token值,所以,我们也要在结构体中注册这一变量。
struct mytbf_st
{
int cps;
int burst;
int token;
int pos;
pthread_mutex_t mut_token;
pthread_cond_t mytbf_cond;
};
接着,我们要在init函数中进行初始化。
mytbf_t *mytbf_init(int cps,int burst)
{
......
......
mytbf->burst = burst;
pthread_mutex_init(&mytbf->mut_token,NULL);
//采用默认属性,填NULL
pthread_cond_init(&mytbf->cond, NULL;
......
......
}
之后,还要在destory函数中销毁,代码如下。
//ptr是void*类型
int mytbf_destory(mytbf_t* ptr)//释放空间
{
......
pthread_mutex_destroy(&mytbf->mut_token);
pthread_cond_destroy(&mytbf->cond);
......
}
我们再回到mytbf_fetchtoken函数。
int mytbf_fetchtoken(mytbf_t* ptr, int size)
{
......
......
//查看我当前有没有token
//要加锁查看
pthread_mutex_lock(&mytbf->mut_token);
while(mytbf->token <= 0)
{
/*
//不满足条件,就先解锁
pthread_mutex_unlock(&mytbf->mut_token);
//等待--短暂出让调度器
sched_yield();
//再抢锁,此时发现大于0,退出循环体,向下执行
pthread_mutex_lock(&mytbf->mut_token);
*/
//使用pthread_cond_wait函数就相当于上面的三个步骤
//为什么呢?注意该函数的两个参数.
pthread_cond_wait(&mytbf->cond,&mytbf->mut_token);
//pthread_cond_wait在这里的功能是发现循环条件不成立,就解锁等待,等到什么时候,
//等到有pthread_cond_broadcast或pthread_cond_signal发来消息.
//broadcast和signal的区别是叫醒全部等待和叫醒任意一个等待.
//所以pthread_cond_wait一定是和一个或多个broadcast或signal相对应的.
//注意:等待是解锁等待.就相当于上面代码中的先解锁后短暂让出调度器,但不是sched_yield,
//而是一个等broadcast或signal.(是在临界区外等待),当有broadcast或signal叫醒它后,它起来的
//第一反应是抢锁,然后再来查看条件是否成立.而不是直接向下运行的.如果没抢到锁,锁被别人抢走
//了,当前这个wait原语就会被堵塞在抢锁阶段,直到别人让出锁,它再抢到锁.
......
......
}
那么谁会来打断呢,我们要的是改变token,token在哪些地方会改变呢?
在线程函数thr_alarm这,每过一秒钟,给所有非空的令牌桶加token。
还有返还token函数mytbf_returntoken这,当使用完之后,还剩余token的话,会还token。
首先来看线程函数thr_alarm这
我们在该函数中添加pthread_cond_broadcast或pthread_cond_signal,在哪添加呢?添加哪一个呢?
使用pthread_cond_broadcast可以叫醒所有正在等待的线程。
使用pthread_cond_signal叫醒哪一个等待的线程是随机的,是不确定的。
我们可能会这样想。
我们在给第i个令牌桶加token,那么使用signal给这个等待给唤醒不就好了吗?但是,这是不对的,因为会有这么一种情况,假设有多个令牌桶,他们恰好都停留在取token的阶段(多个线程同时都在等待),都在等待着信号给他们给叫醒,但是使用signal叫醒又是随机的,不确定它会叫醒哪一个,假设第三个令牌桶需要等待信号,但你signal却偏偏独爱其他几个,每次都不给第三个发。
所以,我们使用pthread_cond_broadcast来叫醒所有正在等待的线程。
代码如下:
//把时间到了,加token这个函数用一个线程来完成
static void* thr_alarm(void* p)
{
......
while(1)
{
.......
job[i]->token = job[i]->burst;
//下面这两句代码的顺序关系不大(除非是在非常特定情况下),都有道理.
//比如解锁之前就发通知,可以先唤醒正在等待的进程,
//让他们做好抢锁的准备(等这边一解锁之后,就立刻去抢锁)
//这样就可以在有很多种线程都在抢锁的时候,他们抢到锁的机会更大些。
//当然,也可以先解锁,因为加锁的目的是控制token的值,
//而且临界区的代码要越少越好。
//因为这里只有一种线程(只有多个同类型的令牌桶)在抢锁,所以我更倾向于先解锁,再发通知.
pthread_mutex_unlock(&job[i]->mut_token);
pthread_cond_broadcast(&job[i]->mytbf_cond);
......
}
}
最后,还需要考虑的是更全面的一种情况。
假如说,我现在正在和别人共用同一个令牌桶。
比如,我们的目的是把A文件复制到B文件的位置,我的这一个流控能实现每秒钟复制10个字节,另外还有一个人干活,它可以实现每秒钟复制5个字节(我们协调操作同一个令牌桶),这样总共就是每秒钟能复制15个字节。
那这样的话,就有可能出现下面这种情况。
我针对这个令牌桶来取token,可是token被你提前一步给取走了。那我就会因为没有token而等待。
int mytbf_fetchtoken(mytbf_t* ptr, int size)
{
......
......
//查看我当前有没有token
//要加锁查看
pthread_mutex_lock(&mytbf->mut_token);
while(mytbf->token <= 0)
{
pthread_cond_wait(&mytbf->cond,&mytbf->mut_token);//阻塞到这
......
而你在使用完token后,发现取多了。就来返还token,也许返还回来的token就足够我使用了。所以当你返还回来之后,你就发消息来通知我一下。
从而在mytbf_returntoken中,我们加上这么一句代码。
int mytbf_returntoken(mytbf_t* ptr,int size)
{
......
pthread_mutex_unlock(&mytbf->mut_token);
pthread_cond_broadcast(&mytbf->cond);
......
}
总结:发通知就像过马路,当红灯变成绿灯,你就可以过马路了。
所以通知机制也一定要有监听(wait)。