令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶

alarm()

一个信号从发出到收到,有一个不可避免的时延。所以如果使用信号来计时的话,10ms以内的计时不准确。只要超过这个时间,基本上都能准确的用信号来计时。

alarm没有办法直接实现多任务的计时器,因为当程序中有多个alarm时,程序计时可能就会以最后一个alarm为准,所以alarm不能直接用于多任务的计时器。因此,我们需要思考怎么使用alarm这样一个单一的计时器来实现10秒做一个事,5秒做一个事,1秒做一个事的多任务计时器。

pause()

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文件中。
令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶_第1张图片
系统会从按时间那一刻开始计算,会把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命令来统计程序的实际运行时间。
令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶_第2张图片
real 时间= user时间 +sys时间,我们看user的时间。

可以看出,使用alarm控制的时间更精确。

漏桶

流控(流量控制):每秒钟按规定的字符输出。(音、视频播放器中每秒播放多少字节,网络传输中,每秒固定传输多少字节的内容中广泛应用。)

这里要实现的一个具体的功能是:

实现cat的功能,但是将一次性显示改成每秒显示固定个数的字符。

实现效果如下:
令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶_第3张图片
使用sleep延时来实现

#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;
}

令牌桶封装成库

令牌桶的要素:
令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶_第4张图片
1、我们需要指定不同的流速(CPS)
2、每个令牌桶的令牌个数一定有一个上限(BURST)
3、还要有一个变量来存放当前的令牌数。(token)

令牌桶封装成结构体,挂接在数组中。

在函数功能上,原先的
" 一个token一次能输出cps个字符,一次加1个token "
改写成
“一个token一次能输出一个字符,每次加cps个token”
令牌桶 -- alarm信号、漏桶、alarm信号实现的令牌桶、使用alarm信号将令牌桶封装成库、互斥量重新实现令牌桶、使用条件变量修改并实现令牌桶_第5张图片
最关键的问题是,捋着数组走,给每个非空的令牌桶都加上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_broadcastpthread_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)。

你可能感兴趣的:(linux)