非常精简的Linux线程池实现(一)——使用互斥锁和条件变量
线程池的含义跟它的名字一样,就是一个由许多线程组成的池子。
有了线程池,在程序中使用多线程变得简单。我们不用再自己去操心线程的创建、撤销、管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里就好了,然后我们的主程序(主线程)可以继续干自己的事去,线程池里面的线程会自动去执行这些任务。
另一方面,线程池提升了多线程程序的性能。我们不需要在大量任务需要执行时现创建大量线程,然后在任务结束时又销毁大量线程,因为线程池里面的线程都是现成的而且能够重复使用。一个理想的线程池能够合理地动态调节池内线程数量,既不会因为线程过少而导致大量任务堆积,也不会因为线程过多了而增加额外的系统开销。
线程池看上去很神奇的样子,那它是怎么实现的呢?线程这么虚渺在的东西也能像有形的物品一样圈在一个池子里?在只知道线程池这个名字的时候,我心里的疑惑就是这样的。
其实线程池的原理非常简单,它就是一个非常典型的生产者消费者同步问题。如果不知道我说的这个XXX问题也不要紧,我下面就解释。
根据刚才描述的线程池的功能,可以看出线程池至少有两个主要动作,一个是主程序不定时地向线程池添加任务,另一个是线程池里的线程领取任务去执行。且不论任务和执行任务是个什么概念,但是一个任务肯定只能分配给一个线程执行。
这样就可以简单猜想线程池的一种可能的架构了:主程序执行入队操作,把任务添加到一个队列里面;池子里的多个工作线程共同对这个队列试图执行出队操作,这里要保证同一时刻只有一个线程出队成功,抢夺到这个任务,其他线程继续共同试图出队抢夺下一个任务。所以在实现线程池之前,我们需要一个队列,我为这个线程池配备的队列单独放到了另一篇博客一个通用纯C队列的实现中。
这里的生产者就是主程序,生产任务(增加任务),消费者就是工作线程,消费任务(执行、减少任务)。因为这里涉及到多个线程同时访问一个队列的问题,所以我们需要互斥锁来保护队列,同时还需要条件变量来处理主线程通知任务到达、工作线程抢夺任务的问题。如果不熟悉条件变量,我在另一篇博客Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解中作了详细说明。
准备工作都差不多了,可以开始设计线程池了。一个最简单线程池应该有什么功能呢?对于使用者来说,除了创建和销毁线程池,最简单的情况下只需要一个功能——添加任务。对于线程池自己来说,最简单的情况下不需要动态调节线程数量,不需要考虑线程同步、线程死锁等等一大堆麻烦的问题。所以最后的线程池API定义为:
- //thread_pool.h
- #ifndef THREAD_POOL_H_INCLUDED
- #define THREAD_POOL_H_INCLUDED
- typedef struct thread_pool *thread_pool_t;
- thread_pool_t thread_pool_create(unsigned int thread_count);
- void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg);
- void thread_pool_destroy(thread_pool_t pool);
- #endif //THREAD_POOL_H_INCLUDED
创建线程池时指定线程池中应该固定包含多少工作线程,添加任务就是向线程池添加一个任务函数指针和任务函数需要的参数——这跟Pthread线程库中的普通线程创建函数pthread_create是一样的。根据这套线程池API,我们使用线程池的应用程序应该是这个套路:
- //test.c
- #include "thread_pool.h"
- #include <stdio.h>
- #include <unistd.h>
- #include <pthread.h>
- void* test(void *arg) {
- int i;
- for(i=0; i<5; i++) {
- printf("tid:%ld task:%ld\n", pthread_self(), (long)arg);
- fflush(stdout);
- sleep(2);
- }
- return NULL;
- }
- int main() {
- long i=0;
- thread_pool_t pool;
- pool=thread_pool_create(2);
- for(i=0; i<5; i++) {
- thread_pool_add_task(pool, test, (void*)i);
- }
- puts("press enter to terminate ...");
- getchar();
- thread_pool_destroy(pool);
- return 0;
- }
上面这个测试程序向线程池添加了5个相同的任务,每个任务耗时10秒,但是线程池中只有2个工作线程,所以程序的运行结果是两个工作线程轮流把5个任务挨个做完。显示到屏幕上就是:前10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号0和1,各输出5次;第二个10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号2和3……
在这期间,主程序输出“press enter to terminate ...”并等待用户输入,任何时候都可以按回车让主程序继续往下,这样会强制终止所有工作线程并销毁线程池,最后程序退出。test程序运行效果截图如下:
最后就是线程池真正的实现了:
- //thread_pool.c
- #include "thread_pool.h"
- #include "queue.h"
- #include <stdlib.h>
- #include <pthread.h>
- struct thread_pool {
- unsigned int thread_count;
- pthread_t *threads;
- queue_t tasks;
- pthread_mutex_t lock;
- pthread_cond_t task_ready;
- };
- struct task {
- void* (*routine)(void *arg);
- void *arg;
- };
- static void cleanup(pthread_mutex_t* lock) {
- pthread_mutex_unlock(lock);
- }
- static void * worker(thread_pool_t pool) {
- struct task *t;
- while(1) {
- pthread_mutex_lock(&pool->lock);
- pthread_cleanup_push((void(*)(void*))cleanup, &pool->lock);
- while(queue_isempty(pool->tasks)) {
- pthread_cond_wait(&pool->task_ready, &pool->lock);
- /*A condition wait (whether timed or not) is a cancellation point ... a side-effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler.*/
- }
- t=(struct task*)queue_dequeue(pool->tasks);
- pthread_cleanup_pop(0);
- pthread_mutex_unlock(&pool->lock);
- t->routine(t->arg);/*todo: report returned value*/
- free(t);
- }
- return NULL;
- }
- thread_pool_t thread_pool_create(unsigned int thread_count) {
- unsigned int i;
- thread_pool_t pool=NULL;
- pool=(thread_pool_t)malloc(sizeof(struct thread_pool));
- pool->thread_count=thread_count;
- pool->threads=(pthread_t*)malloc(sizeof(pthread_t)*thread_count);
- pool->tasks=queue_create();
- pthread_mutex_init(&pool->lock, NULL);
- pthread_cond_init(&pool->task_ready, NULL);
- for(i=0; i<thread_count; i++) {
- pthread_create(pool->threads+i, NULL, (void*(*)(void*))worker, pool);
- }
- return pool;
- }
- void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg) {
- struct task *t;
- pthread_mutex_lock(&pool->lock);
- t=(struct task*)queue_enqueue(pool->tasks, sizeof(struct task));
- t->routine=routine;
- t->arg=arg;
- pthread_cond_signal(&pool->task_ready);
- pthread_mutex_unlock(&pool->lock);
- }
- void thread_pool_destroy(thread_pool_t pool) {
- unsigned int i;
- for(i=0; i<pool->thread_count; i++) {
- pthread_cancel(pool->threads[i]);
- }
- for(i=0; i<pool->thread_count; i++) {
- pthread_join(pool->threads[i], NULL);
- }
- pthread_mutex_destroy(&pool->lock);
- pthread_cond_destroy(&pool->task_ready);
- queue_destroy(pool->tasks);
- free(pool->threads);
- free(pool);
- }
上面的worker函数就是工作线程函数,所有的工作线程都在执行着这个函数。它首先在互斥锁和条件变量的保护下从任务队列中取出一个任务,这个任务实际上是一个函数指针和调用函数所需的参数,所以执行任务就很简单了——用任务参数调用任务函数。函数返回以后,工作线程继续去抢任务。
这里没有处理任务函数的返回值问题,理论上任务函数返回以后线程池应该用某种机制通知主程序,然后主程序获取通过某种手段获取返回值,但这明显不是一个最简单的线程池需要操心的事。实际上,应用程序可以通过全局变量或传入的参数指针,加上额外的线程同步代码解决返回值的通知和获取问题。
还有一点需要注意,最后线程池销毁时会强制终止所有处于撤销点(cacellation
points)的工作线程,如果工作线程正在任务函数中没返回而且任务函数中有非手动创建的撤销点,那么任务函数就会在跑到撤销点时戛然而止,这可能导致意外结果。而如果任务函数中没有任何线程撤销点,那么线程池销毁函数会一直阻塞等待直到任务函数完成后才能终止对应的工作线程并返回。
要正确处理这个问题,线程池使用者必须通过自己的线程同步代码保证调用thread_pool_destroy之前所有任务都已经完成、终止或者取消。
非常精简的Linux线程池实现(一)——使用互斥锁和条件变量的更多相关文章
- linux 线程的同步 二 (互斥锁和条件变量)
互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...
- 进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...
- node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】
本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...
- linux c 线程间同步(通信)的几种方法--互斥锁,条件变量,信号量,读写锁
Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量.信号量和读写锁. 下面是思维导图: 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1 . ...
- [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...
- Linux互斥锁、条件变量和信号量
Linux互斥锁.条件变量和信号量 来自http://kongweile.iteye.com/blog/1155490 http://www.cnblogs.com/qingxia/archive/ ...
- linux 互斥锁和条件变量
为什么有条件变量? 请参看一个线程等待某种事件发生 注意:本文是linux c版本的条件变量和互斥锁(mutex),不是C++的. mutex : mutual exclusion(相互排斥) 1,互 ...
- 线程私有数据TSD——一键多值技术,线程同步中的互斥锁和条件变量
一:线程私有数据: 线程是轻量级进程,进程在fork()之后,子进程不继承父进程的锁和警告,别的基本上都会继承,而vfork()与fork()不同的地方在于vfork()之后的进程会共享父进程的地址空 ...
- 【Linux C 多线程编程】互斥锁与条件变量
一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...
随机推荐
- BZOJ4556 HEOI2016字符串
没错,又是这题,使用后缀自动机,反向建树,主席树维护right集合. By:大奕哥 #include<bits/stdc++.h> using namespace std; ; ]; ch ...
- Western Subregional of NEERC, Minsk, Wednesday, November 4, 2015 Problem G. k-palindrome dp
Problem G. k-palindrome 题目连接: http://opentrains.snarknews.info/~ejudge/team.cgi?SID=c75360ed7f2c7022 ...
- 有强大的cURL,忘掉httpclient的吧!
这段时间想做一个网页采集的程序,由于一网站采用了防采集的办法,我的httpclient总是在登录后无法获取到我想要过去的链接.在无数次的跟踪过后发现原来人家给返回的是javascript拼成的页面,而 ...
- LPC43xx SGPIO Slice 输入输出连接表
- sqlserver 2012 IDE中 Windows身份验证连接服务器报错 ,Login failed for user 'xxx\Administrator'. 原因: 找不到与提供的名称匹配的登录名。
问题描述: 本地装了两个实例,一个是SQLEXPRESS,可以正常操作.但是另一个开发常用的实例MSSQLSERVER却连Windows身份验证都报错,报的错误也是很奇葩,怎么会找不到Administ ...
- SpringMVC介绍之视图解析器ViewResolver
在前一篇博客中讲了SpringMVC的Controller控制器,在这篇博客中将接着介绍一下SpringMVC视图解析器.当我们对SpringMVC控制的资源发起请求时,这些请求都会被SpringMV ...
- EF Core数据迁移操作
摘要 在开发中,使用EF code first方式开发,那么如果涉及到数据表的变更,该如何做呢?当然如果是新项目,删除数据库,然后重新生成就行了,那么如果是线上的项目,数据库中已经有数据了,那么删除数 ...
- JavaScript学习总结(十五)——Function类
在JavaScript中,函数其实是对象,每个函数都是Function类的实例,既然函数对象,那么就具有自己的属性和方法,因此,函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定. 一.函数的 ...
- jeffy-vim-v3.2
jeffy-vim-v3.2 增加了vim-gutentags 插件,支持tags自动生成.
- C#编程(二十七)----------创建泛型类
创建泛型类 首先介绍一个一般的,非泛型的简化链表类,可以包含任意类型的对象,以后再把这个类转化为泛型类. 在立案表中,一个元素引用下一个元素.所以必须创建一个类,他将对象封装在链表中,并引用下一个对象 ...