多线程
1、什么是线程?
1.1、任务调度的基本概念
- 大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式;
- 任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态。
- 一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。
- 任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发。
- 并发:多个任务同时执行
1.2、进程和线程的基本联系
进程与线程的一个简单解释
计算机的发展,对CPU的要求越来越高,进程之间的**切换开销**较大。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)
- 一个进程中的所有线程不仅共享全局变量,而且共享:进程指令、大多数数据、打开的文件(如描述字)、信号处理程序和信号处置、当前工作目录、用户ID和组ID。
- 每个线程有自己的线程ID、寄存器集合(包括程序计数器和栈指针)、栈(用于存放局部变量和返回地址)、error、信号掩码、优先级。
1.3、多线程与多核的基本关系
1、多核(心)处理器:
- 在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。
- 内核线程(Kernel Thread,KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。
- 一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。
2、超线程技术
利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定
3、用户线程(Lightweight Process,LWP):
- 程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Lightweight Process,LWP),轻量级进程就是我们通常意义上所讲的线程(我们在这称它为用户线程)。
- 由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。用户线程与内核线程的对应关系有三种模型:一对一模型、多对一模型、多对多模型。(参考)
1、一对一模型
2、多对一模型
3、多对多模型
2、为什么使用多线程?
选择多线程的原因就是一个快 字,使用多线程就是在正确的场景下通过设置正确个数的线程来最大化程序的运行速度。
- 充分的利用 CPU 和 I/O 的利用率
- 合理的场景+合理的线程数 得到运行效率的提升。
例如:
多线程用于堆积处理,就像一个大土堆,一个推土机很慢,那么10个推土机一起来处理,当然速度就快了,不过由于位置的限制,如果20个推土机,那么推土机之间会产生相互的避让,相互摩擦,相互拥挤,反而不如10个处理的好,所以,多线程处理,线程数要开的恰当,就可以提高效率。
具体原因集中如下两个层面:
- 1、多线程和进程相比,它是一种非常花销小,切换快,更"节俭"的多任务操作方式。
- 在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。
- 而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,
- 而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
- 2、方便的通信机制。
- 对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。
- 线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
- 当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
2.1、并发场景的应用
- 1、常见的浏览器、Web服务(现在写的web是中间件帮你完成了线程的控制),web处理请求,各种专用服务器(如游戏服务器)
- 2、servlet多线程
- 3、FTP下载,多线程操作文件
- 4、数据库用到的多线程
- 5、分布式计算
- 6、tomcat,tomcat内部采用多线程,上百个客户端访问同一个WEB应用,tomcat接入后就是把后续的处理扔给一个新的线程来处理,这个新的线程最后调用我们的servlet程序,比如doGet或者dpPost方法
- 7、后台任务:如定时向大量(100W以上)的用户发送邮件;定期更新配置文件、任务调度(如quartz),一些监控用于定期信息采集
- 8、自动作业处理:比如定期备份日志、定期备份数据库
- 9、异步处理:如发微博、记录日志
- 10、页面异步处理:比如大批量数据的核对工作(有10万个手机号码,核对哪些是已有用户)
- 11、数据库的数据分析(待分析的数据太多),数据迁移
- 12、多步骤的任务处理,可根据步骤特征选用不同个数和特征的线程来协作处理,多任务的分割,由一个主线程分割给多个线程完成
…
2.1.1、I/O密集型(I/O bound)
1、定义
频繁网络传输、读取硬盘及其他io设备等等
- 涉及到网络、磁盘IO的任务都是IO密集型任务
2、特征
这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。
2.1.1.1、单核型应用
在进行 I/O 操作时,CPU是空闲状态,所以我们要最大化的利用 CPU,不能让其是空闲状态。
从上图中可以看出,每个线程都执行了相同长度的 CPU 耗时和 I/O 耗时,如果你将上面的图多画几个周期,CPU操作耗时固定,将 I/O 操作耗时变为 CPU 耗时的 3 倍,你会发现,CPU又有空闲了,这时你就可以新建线程 4,来继续最大化的利用 CPU。
2.1.2、CPU (计算)密集型程序((CPU-bound)
1、定义
- 程序系统大部分在做计算、逻辑判断、循环导致cpu占用率很高的情况,称之为计算密集型;
- 例如一个计算圆周率至小数点一千位以下的程序,在执行的过程当中绝大部份时间用在三角函数和开根号的计算,便是属于CPU bound的程序。
2、特征
- CPU bound的程序一般而言CPU占用率相当高。这可能是因为任务本身不太需要访问I/O设备,也可能是因为程序是多线程实现因此屏蔽掉了等待I/O的时间。
- 就像你的大脑是CPU,你本来就在一本心思地写作业,多线程这时候就是要你写会作业,然后立刻敲一会代码,然后在P个图,然后在看个视频,然后再切换回作业。过程中你还需要切换(收起来作业,拿出电脑,打开VS…)那你的作业怕是要写到挂科。。。这个时候不太适合使用多线程。
2.1.2.1、单核型
假如我们要计算 1+2+....100亿
的总和,很明显,这就是一个 CPU 密集型程序
在【单核】CPU下,如果我们创建 4 个线程来分段计算,即:
- 线程1计算 [1,25亿)
- … 以此类推
- 线程4计算 [75亿,100亿]
由于是单核 CPU,所有线程都在等待 CPU 时间片。按照理想情况来看,四个线程执行的时间总和与一个线程5独自完成是相等的,实际上我们还忽略了四个线程上下文切换的开销。
2.1.2.2、多核型(并行)
每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。理论情况来看效率提升了 4 倍。
2.2、是不是线程越多越好
2.2.1、CPU 密集型程序创建多少个线程合适?
有些同学早已经发现,对于 CPU 密集型来说,理论上 线程数量 = CPU 核数(逻辑)就可以了,但是实际上,数量一般会设置为 CPU 核数(逻辑)+ 1, 为什么呢?
《Java并发编程实战》这么说:
计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。
所以对于CPU密集型程序, CPU 核数(逻辑)+ 1 个线程数是比较好的经验值的原因了
2.2.2、I/O密集型程序创建多少个线程合适?
最佳线程数 = (1/CPU利用率) = 1 + (I/O耗时/CPU耗时)
这是一个CPU核心的最佳线程数,如果多个核心,那么 I/O 密集型程序的最佳线程数就是:
最佳线程数 = CPU核心数 * (1/CPU利用率) = CPU核心数 * (1 + (I/O耗时/CPU耗时))
要计算 I/O 密集型程序,是要知道 CPU 利用率的,如果我不知道这些,那要怎样给出一个初始值呢?
按照上面公式,假如几乎全是 I/O耗时,所以纯理论你就可以说是 2N(N=CPU核数),当然也有说 2N + 1的,(我猜这个 1 也是backup)
2.2.3、增加 CPU 核数一定能解决问题吗?
算出了理论线程数,但实际CPU核数不够,会带来线程上下文切换的开销,所以下一步就需要增加 CPU 核数,那我们盲目的增加 CPU 核数就一定能解决问题吗?
这个结论告诉我们,假如我们的串行率是 5%,那么我们无论采用什么技术,最高也就只能提高 20 倍的性能。
如何简单粗暴的理解串行百分比(其实都可以通过工具得出这个结果的)呢?来看个小 Tips:
Tips: 临界区都是串行的,非临界区都是并行的,用单线程执行临界区的时间/用单线程执行(临界区+非临界区)的时间就是串行百分比
3、什么是线程池
3.1、背景
传统多线程方案中我们采用的服务器模型则是一旦接受到请求之后,即创建一个新的线程,由该线程执行任务。任务执行完毕后,线程退出,这就是是**“即时创建,即时销毁”**的策略。尽管与创建进程相比,创建线程的时间已经大大的缩短,但是如果提交给线程的任务是执行时间较短,而且执行次数极其频繁,那么服务器将处于不停的创建线程,销毁线程的状态。
我们将传统方案中的线程执行过程分为三个过程:T1、T2、T3
。
- T1:线程创建时间
- T2:线程执行时间,包括线程的同步等时间
- T3:线程销毁时间
那么我们可以看出,线程本身的开销所占的比例为(T1+T3) / (T1+T2+T3)
。如果线程执行的时间很短的话,这比开销可能占到20%-50%左右。如果任务执行时间很频繁的话,这笔开销将是不可忽略的。
因此线程池的出现正是着眼于减少线程池本身带来的开销。
3.2、定义
- 1、线程池采用预创建的技术,在应用程序启动之后,将立即创建一定数量的线程
(N1)
,放入空闲队列中。这些线程都是处于阻塞(Suspended)状态,不消耗CPU,但占用较小的内存空间。 - 2、当任务到来后,缓冲池选择一个空闲线程,把任务传入此线程中运行。
- 3、当
N1
个线程都在处理任务后,缓冲池自动创建一定数量的新线程,用于处理更多的任务。 - 4、在任务执行完毕后线程也不退出,而是继续保持在池中等待下一次的任务。
- 5、当系统比较空闲时,大部分线程都一直处于暂停状态,线程池自动销毁一部分线程,回收系统资源。
3.3、应用场景
- (1) 单位时间内处理任务频繁而且任务处理时间短
- (2) 对实时性要求较高。如果接受到任务后在创建线程,可能满足不了实时要求,因此必须采用线程池进行预创建。
- (3)必须经常面对高突发性事件,比如Web服务器,如果有足球转播,则服务器将产生巨大的冲击。此时如果采取传统方法,则必须不停的大量产生线程,销毁线程。此时采用动态线程池可以避免这种情况的发生。
3.4、简单线程池实现
基于生产者——消费者模型,可以得到,线程池大概由一下几部分组成:
1、生产者:生成任务,添加到任务队列(addTask);
2、任务队列:存放task的容器
3、消费者:从任务队列中取出任务进行消费(threadRoutine)。
4、因为涉及到任务队列,资源竞争,这里采用互斥锁(mutex)和条件变量(condition_variable)完成资源的互斥以及线程的通讯任务
具体执行逻辑如下:
1、线程池结构的封装threadpool.h
#ifndef MYTHREADPOOL_THREADPOOL_H
#define MYTHREADPOOL_THREADPOOL_H
//线程池头文件
#include "condition.h"
//封装线程池中的对象需要执行的任务对象
typedef struct task
{
void *(*run)(void *args); //函数指针,需要执行的任务
void *arg; //参数
struct task *next; //任务队列中下一个任务
}task_t;
//下面是线程池结构体
typedef struct threadpool_data
{
condition_mutex ready; //状态量
task_t *first; //任务队列中第一个任务
task_t *last; //任务队列中最后一个任务
int counter; //线程池中已有线程数
int idle; //线程池中空闲线程数
int max_threads; //线程池最大线程数
int quit; //是否退出标志
}threadpool_t;
class threadpool
{
public:
threadpool(int threads);
~threadpool();
//线程池初始化
void threadpool_init(int threads);
//往线程池中加入任务
void threadpool_add_task(void *(*run)(void *arg), void *arg);
//摧毁线程池
void threadpool_destroy();
private:
threadpool_t workers;
};
#endif //MYTHREADPOOL_THREADPOOL_H
2、线程池实现threadpool.cpp
:
#include "threadpool.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
//创建的线程执行
static void *thread_routine(void *arg)
{
struct timespec abstime;
int timeout;//超时标志位
printf("thread %d is starting\n",pthread_self());
threadpool_t *pool = (threadpool_t *)arg;
while(1)
{
timeout = 0;
//访问线程池之前需要加锁
pool->ready.condition_lock();
//空闲
pool->idle++;
//等待队列有任务到来 或者 收到线程池销毁通知
while(pool->first == NULL && !pool->quit)
{
//否则线程阻塞等待
printf("thread %d is waiting\n",pthread_self());
//获取从当前时间,并加上等待时间, 设置线程的超时睡眠时间
clock_gettime(CLOCK_REALTIME, &abstime);
abstime.tv_sec += 2;
int status;
status = pool->ready.condition_timedwait(&abstime);//该函数会解锁,允许其他线程访问,当被唤醒时,加锁
if(status == ETIMEDOUT)
{
printf("thread %d wait timed out\n",pthread_self());
timeout = 1;
break;
}
}
pool->idle--;
if(pool->first != NULL)
{
//取出等待队列最前的任务,移除任务,并执行任务
task_t *t = pool->first;
pool->first = t->next;
//由于任务执行需要消耗时间,先解锁让其他线程访问线程池
pool->ready.condition_unlock();
//执行任务
t->run(t->arg);
//执行完任务释放内存
free(t);
//重新加锁
pool->ready.condition_lock();
}
//退出线程池
if(pool->quit && pool->first == NULL)
{
pool->counter--;//当前工作的线程数-1
//若线程池中没有线程,通知等待线程(主线程)全部任务已经完成
if(pool->counter == 0)
{
pool->ready.condition_signal();
}
pool->ready.condition_unlock();
break;
}
//超时,跳出销毁线程
if(timeout == 1)
{
pool->counter--;//当前工作的线程数-1
pool->ready.condition_unlock();
break;
}
pool->ready.condition_unlock();
}
printf("thread %d is exiting\n", pthread_self());
return NULL;
}
threadpool::threadpool(int threads)
{
threadpool_init(threads);
}
threadpool::~threadpool()
{
threadpool_destroy();
}
//线程池初始化
void threadpool::threadpool_init(int threads)
{
workers.ready.condition_init();
workers.first = NULL;
workers.last =NULL;
workers.counter =0;
workers.idle =0;
workers.max_threads = threads;
workers.quit =0;
}
//增加一个任务到线程池
void threadpool::threadpool_add_task(void *(*run)(void *arg), void *arg)
{
//产生一个新的任务
task_t *newtask = (task_t *)malloc(sizeof(task_t));
newtask->run = run;
newtask->arg = arg;
newtask->next=NULL;//新加的任务放在队列尾端
//线程池的状态被多个线程共享,操作前需要加锁
workers.ready.condition_lock();
if(workers.first == NULL)//第一个任务加入
{
workers.first = newtask;
}
else
{
workers.last->next = newtask;
}
workers.last = newtask; //队列尾指向新加入的线程
//线程池中有线程空闲,唤醒
if(workers.idle > 0)
{
workers.ready.condition_signal();
}
//当前线程池中线程个数没有达到设定的最大值,创建一个新的线程
else if(workers.counter < workers.max_threads)
{
pthread_t tid;
pthread_create(&tid, NULL, thread_routine, &workers);
workers.counter++;
}
//结束,访问
workers.ready.condition_unlock();
}
//线程池销毁
void threadpool::threadpool_destroy()
{
//如果已经调用销毁,直接返回
if(workers.quit)
{
return;
}
//加锁
workers.ready.condition_lock();
//设置销毁标记为1
workers.quit = 1;
//线程池中线程个数大于0
if(workers.counter > 0)
{
//对于等待的线程,发送信号唤醒
if(workers.idle > 0)
{
workers.ready.condition_broadcast();
}
//正在执行任务的线程,等待他们结束任务
while(workers.counter)
{
workers.ready.condition_wait();
}
}
workers.ready.condition_unlock();
workers.ready.condition_destroy();
}
3、线程同步的类封装condition.h
:互斥和条件变量
#ifndef MYTHREADPOOL_CONDITION_H
#define MYTHREADPOOL_CONDITION_H
#include <pthread.h>
//封装一个互斥量和条件变量作为状态
typedef struct condition
{
pthread_mutex_t pmutex;
pthread_cond_t pcond;
}condition_t;
class condition_mutex
{
public:
condition_mutex();
~condition_mutex();
//对状态的操作函数
int condition_init();//初始化
int condition_lock();//加锁
int condition_unlock();//解锁
int condition_wait();//等待
int condition_timedwait(const struct timespec *abstime);//超时等待
int condition_signal();//唤醒一个睡眠线程
int condition_broadcast();//唤醒所以睡眠线程
int condition_destroy();//销毁
private:
condition_t c_mutex;
};
#endif //MYTHREADPOOL_CONDITION_H
4、具体实现condition.cpp
:
#include "condition.h"
condition_mutex::condition_mutex()
{
condition_init();
}
condition_mutex::~condition_mutex()
{
condition_destroy();
}
//初始化
int condition_mutex::condition_init()
{
int status;
if((status = pthread_mutex_init(&c_mutex.pmutex, NULL)))
return status;
if((status = pthread_cond_init(&c_mutex.pcond, NULL)))
return status;
return 0;
}
//加锁
int condition_mutex::condition_lock()
{
return pthread_mutex_lock(&c_mutex.pmutex);
}
//解锁
int condition_mutex::condition_unlock()
{
return pthread_mutex_unlock(&c_mutex.pmutex);
}
//等待
int condition_mutex::condition_wait()
{
return pthread_cond_wait(&c_mutex.pcond,&c_mutex.pmutex);
}
//固定时间等待
int condition_mutex::condition_timedwait(const struct timespec *abstime)
{
return pthread_cond_timedwait(&c_mutex.pcond,&c_mutex.pmutex, abstime);
}
//唤醒一个睡眠线程
int condition_mutex::condition_signal()
{
return pthread_cond_signal(&c_mutex.pcond);
}
//唤醒所有睡眠线程
int condition_mutex::condition_broadcast()
{
return pthread_cond_broadcast(&c_mutex.pcond);
}
//释放
int condition_mutex::condition_destroy()
{
int status;
if((status = pthread_mutex_destroy(&c_mutex.pmutex)))
return status;
if((status = pthread_cond_destroy(&c_mutex.pcond)))
return status;
return 0;
}
5、实验测试main.cpp
#include "threadpool.h"
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
void* mytask1(void *arg)
{
printf("thread %d is working on task %d\n", pthread_self(), *(int*)arg);
sleep(1);
free(arg);
return NULL;
}
void* mytask2(void *arg)
{
printf("thread %d is working on task %d\n", pthread_self(), *(int*)arg);
sleep(10);
free(arg);
return NULL;
}
//测试代码
int main(void)
{
//初始化线程池,最多三个线程
threadpool pool(3);
int i;
//创建十个任务
for(i=0; i < 10; i++)
{
if(i == 5)
{
sleep(5);//故意停顿5秒钟,测试线程超时
}
int *arg = (int *)malloc(sizeof(int));
*arg = i;
if (i%2 == 0)
{
pool.threadpool_add_task(mytask1, arg);//偶数执行任务1
}
else
{
pool.threadpool_add_task(mytask2, arg);//奇数执行任务2
}
}
return 0;
}
g++ main.cpp condition.cpp threadpool.cpp -o main -lpthread
./main
参考
1、https://kb.cnblogs.com/page/531409/
2、https://www.cnblogs.com/gguozhenqian/archive/2011/11/16/2251521.html
3、https://www.zhihu.com/question/343831397/answer/810660079?utm_source=wechat_session&utm_medium=social&utm_oi=963890703886180352
4、https://www.jianshu.com/p/f30ee2346f9f
5、https://www.cnblogs.com/cpper-kaixuan/p/3640485.html
6、https://www.cnblogs.com/ailumiyana/p/9402761.html
7、https://www.cnblogs.com/yangang92/p/5485868.html