微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至[email protected]。
更新:2019/12/13
引子数据
结构和接口实现
反思
今天跟大家分享一个我在实际工作中运用得最多的框架:异步执行队列。
在开发中,我们可能经常会遇到一些执行时间需要很长的任务,如果让程序处理完任务再继续往下走,可能会耽误到程序的主体业务。我们通常的的做法是创建一个FIFO的任务队列,任务来了就将它丢到队列里,会有一个任务处理线程不间断地去队列里取出任务来执行。我们今天讲的这套框架不是处理所有类型的任务,而是处理具有相同数据结构的同一类任务。
一个任务,说白了就是一堆待处理的数据,今天这套框架是针对同一类任务的,所以它的处理函数是固定的。
这样子,我们需要实现一个任务队列,队列的底层可以用链表来实现。我们需要创建一个任务处理线程,不间断地去队列里取出任务来执行。
我们先来定义一个任务的数据结构:
typedef struct task{
xxx;//数据
}TASK_T;
我们需要把这些任务串联起来,所以里面还得增加一个TASK_T指针,指向下一个任务:
typedef struct task{
xxx;//数据
TASK_T *next;
}TASK_T;
实际上我更喜欢采用内核的链表处理方式:
struct list_head {
struct list_head *next, *prev;
};
typedef struct task{
xxx;//数据
struct list_head list;
}TASK_T;
接着我们定义一个全局链表头:
static struct list_head TaskQueue;
INIT_LIST_HEAD(&TaskQueue);//记得初始化链表头:
INIT_LIST_HEAD仅仅只是将链表头的前后指针指向自己,这也作为了后面判断链表是否为空的手段。
#define INIT_LIST_HEAD(ptr) \
do { \
(ptr)->next = (ptr); \
(ptr)->prev = (ptr); \
} while (0)
/*if the list is empty, return ture, else false*/
//判断链表是否为空
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
所有新添加的TASK_T通过成员list(链表结点)链接到链表TaskQueue中。关于内核链表的操作不是我们今天的重点,后面有空再写一个专题重点介绍。
为了保证线程安全,我们还需要定义一把互斥锁:
static pthread_mutex_t TaskQueueMutex = PTHREAD_MUTEX_INITIALIZER;
接收到任务,便是将任务添加到队列里,实际就是一个简单的链表插入操作。
static int g_TaskNum = 0;//统计排队任务数
static int task_enter_queue(TASK_T *task)
{
if(task == httpinfo)
return -1;
//待处理任务太多了,可以限制排队个数,可要可不要
if(g_TaskNum > 15)
{
printf("too many task waiting for process!\n");
return -1;
}
//为了保证该接口是线程安全的,这里需要加锁
pthread_mutex_lock(&TaskQueueMutex);
g_TaskNum++;
list_add_tail(&task->list, &TaskQueue);
pthread_mutex_unlock(&TaskQueueMutex);
return 0;
}
创建任务处理线程
pthread_t taskThreadId;
//创建线程ThreadTaskProcess
if (pthread_create(&taskThreadId, NULL, ThreadTaskProcess,(void *)&taskThreadId) < 0)
{
printf("create ThreadTaskProcess failed!\n");
return -1;
}
任务处理线程的具体框架
void *ThreadTaskProcess(void * arg)
{
prctl(PR_SET_NAME, "ThreadTaskProcess", 0, 0, 0);//设置线程名字
pthread_detach(pthread_self());//分离,用法见下
TASK_T *task;
while (1)
{
usleep(50 * 1000);//睡眠一小段时间,具体根据实际调节
pthread_mutex_lock(&TaskQueueMutex);
if (!list_empty(&TaskQueue))//判断队列是否为空,不空则取出一个来处理
{
list_get_entry(task, &TaskQueue, list);
g_TaskNum--;
list_del(&task->list);//从原有链表中删除
pthread_mutex_unlock(&TaskQueueMutex);
}
else
{
pthread_mutex_unlock(&TaskQueueMutex);
continue;
}
//针对取出来的task的实际处理业务
......
}
}
pthread_detach(threadid)函数的功能是使线程ID为threadid的线程处于分离状态,一旦线程处于分离状态,该线程终止时底层资源立即被回收;否则终止子线程的状态会一直保存(占用系统资源)直到主线程调用pthread_join(threadid,NULL)获取线程的退出状态。
通常是主线程使用pthread_create()创建子线程以后,一般可以调用pthread_detach(threadid)分离刚刚创建的子线程,这里的threadid是指子线程的threadid;如此一来,该子线程终止时底层资源立即被回收;被创建的子线程也可以自己分离自己,子线程调用pthread_detach(pthread_self())就是分离自己,因为pthread_self()这个函数返回的就是自己本身的线程ID。
今天讲的这套框架其实就是经典的生产者消费者模型。
生产者消费者模型
1、什么是生产者消费者模型?
生产者:比喻的是程序中负责产生数据的任务
消费者:比喻的是程序中负责处理数据的任务
生产者>>共享的介质(队列)<<消费者
2、为何用?
实现了生产者与消费者的解耦,生产者可以不停的生产,消费者也可以不停的消费;
从而平衡了生产者的生产能力与消费者的消费能力,提升了程序的整体运行效率。
3、什么时候用?
当我们的程序中存在明显的两类任务,一类负责产生数据,另一类负责处理数据;
此时就应该考虑使用生产者消费者模型来提升程序效率。
到这里就差不多结束了,转念一想还有一点瑕疵。实际上我们还可以对上面的这套框架进行优化。我们在调用static int task_enter_queue(TASK_T *task)入队任务时,并没有讨论指针task的来源,在实际开发中通常是用malloc进行动态分配,这样处理完得进行free,而频繁的malloc和free是我们不愿意见到的。联想到资源池的思想,我们可以一次性分配若干个TASK_T,组成缓存池,需要创建TASK_T时就到缓存池里去取用,用完再归还到缓存池。
缓存池在具体实现上可以用一个队列来表示,归根结底也是一条链表。于是我们整个框架就变成了两个队列,我们对上面的任务队列做个重新定义:
static struct list_head TaskBusyQueue;
static struct list_head TaskFreeQueue;
新任务到达时,从空闲队列TaskFreeQueue里取出任务结构体,组装之后添加到待处理队列TaskBusyQueue。
任务处理线程不间断扫描待处理队列TaskBusyQueue,从中取出任务结构体来执行,执行完再将任务结构体添加回空闲队列TaskFreeQueue。这样子,伴随着任务结构体不断地在两个队列间轮转,一个个的任务在悄然进行。
整套改进后的框架伪代码如下:
//管理结构体
typedef struct _TASK_MANAGER
{
pthread_t taskThreadId;//任务处理线程id
char *task_pool; //任务结构体缓存池
pthread_mutex_t mutex_in;
struct list_head list_idle, list_ready;
}TASK_MANAGER;
TASK_MANAGER task_manager;
//任务结构体缓存池分配
int task_pool_malloc(UINT32 u32Count)
{
TASK_T *ptr;
int i = 0;
//记录下task缓存,后面退出时要销毁
task_manager.task_pool = (char *)malloc(sizeof(TASK_T) * u32Count);
if(task_manager.task_pool == NULL)
return -1;
memset(task_manager.task_pool, 0, sizeof(TASK_T)*u32Count);
//逐个添加到空闲链表
ptr = (TASK_T *)task_manager.task_pool;
for( i = 0; i < u32Count; i++ )
{
list_add_tail(&ptr->list,&task_manager.list_idle);
ptr = ptr + 1;
}
return 0;
}
//任务结构体缓存池销毁
void task_pool_free(void)
{
if(task_manager.task_pool != NULL)
{
free(task_manager.task_pool);
task_manager.task_pool = NULL;
}
}
//队列容量
#define TASK_NUM 30
int task_manager_init()
{
if(pthread_mutex_init(&task_manager.mutex_in,NULL) == -1)
{
printf("init mutex failed!\n");
return -1;
}
INIT_LIST_HEAD(&task_manager.list_idle);
INIT_LIST_HEAD(&task_manager.list_ready);
task_pool_malloc(TASK_NUM);
if (pthread_create(&task_manager.taskThreadId, NULL, ThreadTaskProcess,(void *)&task_manager.taskThreadId) < 0)
{
printf("create ThreadTaskProcess failed!\n");
return -1;
}
return 0;
}
//任务入队
int task_enter_queue(Type *p)
{
TASK_T *task;
pthread_mutex_lock(&task_manager.mutex_in);
if(!list_empty(&task_manager.list_idle))
{
list_get_entry(task, &task_manager.list_idle, list);//从空闲链表取出
list_del(&task->list);//从空闲链表删除
//数据组装
task->xxx = p->xxx;
//添加到待处理链表
list_add_tail(&task->list, &task_manager.list_ready);
}else
{
printf("task list is full!\n");
pthread_mutex_unlock(&task_manager.mutex_in);
return -1;
}
pthread_mutex_unlock(&task_manager.mutex_in);
return 0;
}
//任务处理线程的改动不大,在此不贴出来了。
二进制人生公众号