一、基础概念
在讲线程池技术之前,我们先对操作系统中的一些基础概念,比如进程、线程、线程的创建与销毁等进行说明。
进程和线程
进程
一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。
线程
进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。
与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。
public class MultiThread {
public static void main(String[] args) {
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
for (ThreadInfo threadInfo : threadInfos) {
System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
}
}
}
上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):
[6] Monitor Ctrl-Break //监听线程转储或“线程堆栈跟踪”的线程
[5] Attach Listener //负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者
[4] Signal Dispatcher // 分发处理给 JVM 信号的线程
[3] Finalizer //在垃圾收集前,调用对象 finalize 方法的线程
[2] Reference Handler //用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收的线程
[1] main //main 线程,程序入口
从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行。
进程与线程的区别总结
线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位
资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的
影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行
堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
多进程和多线程区别
多进程:操作系统中同时运行的多个程序
多线程:在同一个进程中同时运行的多个任务
举个例子,多线程下载软件,可以同时运行多个线程,但是通过程序运行的结果发现,每一次结果都不一致。 因为多线程存在一个特性:随机性。造成的原因:CPU在瞬间不断切换去处理各个线程而导致的,可以理解成多个线程在抢CPU资源。
多线程提高CPU使用率
多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。
二、线程池技术
线程池(thread pool)技术是指能够保证所创建的任一线程都处于繁忙状态,而不需要频繁地为了某一任务而创建和销毁线程,因为系统在创建和销毁线程时所耗费的cpu资源很大。如果任务很多,频率很高,为了单一一个任务而起线程而后销线程,那么这种情况效率相当低下的。线程池技术就是用于解决这样一种应用场景而应运而生的。
什么时候需要创建线程池呢?简单的说,如果一个应用需要频繁的创建和销毁线程,而任务执行的时间又非常短,这样线程创建和销毁的带来的开销就不容忽视,这时也是线程池该出场的机会了。如果线程创建T1和销毁时间T3相比任务执行时间T2可以忽略不计,则没有必要使用线程池了。反之如果T1+T3>T2,那就很有必要使用线程池。
线程池技术的工作原理:在起先就创建一定数量的线程以队列形式存在,并为其分配一个工作队列,当工作队列为空时,表示没有任务,此时所有线程挂起等待新的工作到来。当新的工作到来时,线程队列头开始执行这个任务,然后依次是第二、第三个线程执行新到来的任务,当其中某个线程处理完任务后,那么该线程立马开始接受任务分派,从而让所有线程都处于忙碌的状态,提高并行处理效率。
线程池技术是一种典型的生产者-消费者模型。因此,无论用哪种语言实现,只要遵循其原理本身就能够很好的工作了。那么实现线程池技术我们需要考虑到哪些技术性的问题?
C语言线程池技术的实现:
需要考虑的技术问题一,线程池应该包含哪些成员变量。
既然要开一定数量的线程,那么这个“一定数量(max_thread_num)”必定是线程池的一个成员。
如何表示一个线程池是否已经关闭?如果关闭那么必需要立马释放资源。所以“是否关闭(shutdown)”也是一个成员。
创建线程需要有id,必需要为每个线程准备一个id,所以需要一个id数组,其长度就是max_thread_num。
线程锁,用以保证对线程操作时的互斥性。所以需要一个锁,queue_lock。
条件变量(condition_variable),这里使用条件变量主要是为了广播任务到来的消息给所有线程。当有处于空闲的线程,则由此线程
接受任务分派。所以需要一个条件变量queue_ready。
最为重要的就是任务本身,也就是工作。那么工作本身又需要哪几个成员变量?首先肯定是任务入口,routine函数;
其次是routine函数的参数args;再次任务是以队列存在着的,所以任务本身应该包含一个next。
需要考虑的技术问题二,线程池应该包含哪些api。
一、创建线程池,create_tpool
二、销毁线程池,destroy_tpool
三、分派任务,add_task_2_tpool
基于上述分析,我们可以先构造头文件。
tpool.h
#ifndef T_POOL
#define T_POOL
#include <pthread.h>
#include <ctype.h>
typedef struct tpool_work{
void* (*work_routine)(void*);
void* args;
struct tool_work* next;
}tpool_work_t;
typedef struct tpool{
size_t shutdown;
size_t maxnum_thread;
pthread_t *thread_id;
tpool_work_t* tpool_head;
pthread_cond_t queue_ready;
pthread_mutex_t queue_lock;
}tpool_t;
int create_tpool(tpool_t** pool,size_t max_thread_num);
void destroy_tpool(tpool_t* pool);
int add_task_2_tpool(tpool_t* pool,void* (*routine)(void*),void* args);
#endif
需要考虑的技术问题三,线程池的所有权应该交予谁。
这里我们需要考虑到,将线程池封装成一个so库是比较好的想法,那么,线程池的所有权就应该交予调用它的函数。所以我这里采取的就是这个方法。
tpool.c
#include "tpool.h"
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
static void* work_routine(void* args)
{
tpool_t* pool = (tpool_t*)args;
tpool_work_t* work = NULL;
while(1){
pthread_mutex_lock(&pool->queue_lock);
while(!pool->tpool_head && !pool->shutdown){
pthread_cond_wait(&pool->queue_ready,&pool->queue_lock);
}
if(pool->shutdown){
pthread_mutex_unlock(&pool->queue_lock);
pthread_exit(NULL);
}
work = pool->tpool_head;
pool->tpool_head = (tpool_work_t*)pool->tpool_head->next;
pthread_mutex_unlock(&pool->queue_lock);
work->work_routine(work->args);
free(work);
}
return NULL;
}
int create_tpool(tpool_t** pool,size_t max_thread_num)
{
(*pool) = (tpool_t*)malloc(sizeof(tpool_t));
if(NULL == *pool){
printf("in %s,malloc tpool_t failed!,errno = %d,explain:%s\n",__func__,errno,strerror(errno));
exit(-1);
}
(*pool)->shutdown = 0;
(*pool)->maxnum_thread = max_thread_num;
(*pool)->thread_id = (pthread_t*)malloc(sizeof(pthread_t)*max_thread_num);
if((*pool)->thread_id == NULL){
printf("in %s,init thread id failed,errno = %d,explain:%s",__func__,errno,strerror(errno));
exit(-1);
}
(*pool)->tpool_head = NULL;
if(pthread_mutex_init(&((*pool)->queue_lock),NULL) != 0){
printf("in %s,initial mutex failed,errno = %d,explain:%s",__func__,errno,strerror(errno));
exit(-1);
}
if(pthread_cond_init(&((*pool)->queue_ready),NULL) != 0){
printf("in %s,initial condition variable failed,errno = %d,explain:%s",__func__,errno,strerror(errno));
exit(-1);
}
for(int i = 0; i < max_thread_num; i++){
if(pthread_create(&((*pool)->thread_id[i]),NULL,work_routine,(void*)(*pool)) != 0){
printf("pthread_create failed!\n");
exit(-1);
}
}
return 0;
}
void destroy_tpool(tpool_t* pool)
{
tpool_work_t* tmp_work;
if(pool->shutdown){
return;
}
pool->shutdown = 1;
pthread_mutex_lock(&pool->queue_lock);
pthread_cond_broadcast(&pool->queue_ready);
pthread_mutex_unlock(&pool->queue_lock);
for(int i = 0; i < pool->maxnum_thread; i++){
pthread_join(pool->thread_id[i],NULL);
}
free(pool->thread_id);
while(pool->tpool_head){
tmp_work = pool->tpool_head;
pool->tpool_head = (tpool_work_t*)pool->tpool_head->next;
free(tmp_work);
}
pthread_mutex_destroy(&pool->queue_lock);
pthread_cond_destroy(&pool->queue_ready);
free(pool);
}
int add_task_2_tpool(tpool_t* pool,void* (*routine)(void*),void* args)
{
tpool_work_t* work,*member;
if(!routine){
printf("rontine is null!\n");
return -1;
}
work = (tpool_work_t*)malloc(sizeof(tpool_work_t));
if(!work){
printf("in %s,malloc work error!,errno = %d,explain:%s\n",__func__,errno,strerror(errno));
return -1;
}
work->work_routine = routine;
work->args = args;
work->next = NULL;
pthread_mutex_lock(&pool->queue_lock);
member = pool->tpool_head;
if(!member){
pool->tpool_head = work;
}
else{
while(member->next){
member = (tpool_work_t*)member->next;
}
member->next = work;
}
pthread_cond_signal(&pool->queue_ready);
pthread_mutex_unlock(&pool->queue_lock);
return 0;
}
demo.c
#include "tpool.h"
#include <stdio.h>
#include <unistd.h>
#include <time.h>
void* fun(void* args)
{
int thread = (int)args;
printf("running the thread of %d\n",thread);
return NULL;
}
int main(int argc, char* args[])
{
tpool_t* pool = NULL;
if(0 != create_tpool(&pool,5)){
printf("create_tpool failed!\n");
return -1;
}
for(int i = 0; i < 1000; i++){
add_task_2_tpool(pool,fun,(void*)i);
}
sleep(2);
destroy_tpool(pool);
return 0;
}
Makefile
pool: tpool.c demo.c
gcc tpool.c demo.c -o pool -lpthread -std=c99
clean:
rm ./pool
其他的一些C语言实现可以参考下面这些文章:
C语言实现线程池技术
C语言实现线程池
线程池原理及C语言实现线程池
C语言实现线程池功能
进程和线程的区别(超详细)
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)