如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?


1、进程、线程、线程池的概念

进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。

线程,程序执行流的最小执行单位,是进程中的实际运作单位。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗,那为什么我们要使用线程池,主要解决如下几个问题:

创建/销毁线程伴随着系统开销,过于频繁的创建/销毁线程,会很大程度上影响处理效率

线程并发数量过多,抢占系统资源,从而导致系统阻塞

能够容易的管理线程,比如:线程延迟执行、执行策略等

2、线程的 生命周期

线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第1张图片

首先使用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。而我们在图解中可以看出,线程还具有一个则色的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。


文末超强干货分享


3、单线程和多线程概念

单线程,顾名思义即是只有一个线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解

多线程,创建多个线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。

并行和并发:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?

并发:从宏观方面来说,并发就是同时进行多种时间,实际上,这几种时间,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情

并行:则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础上完成。

还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,共享资源还来不及发生变化,就同时有两个线程使用了同一条资源,会造成数据不一致性,导致线程执行错误发生。

有造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程占用了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。

4、JAVA中线程池的实现

在Java中,线程池的概念是Executor这个接口,具体实现为ThreadPoolExecutor类,学习Java中的线程池,就可以直接学习它。对线程池的配置,就是对ThreadPoolExecutor构造函数的参数的配置,既然这些参数这么重要,就来看看构造函数的各个参数吧

ThreadPoolExecutor提供了四个构造函数

//五个参数的构造函数public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue workQueue) //六个参数的构造函数-1public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue workQueue,                          ThreadFactory threadFactory) //六个参数的构造函数-2public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue workQueue,                          RejectedExecutionHandler handler) //七个参数的构造函数public ThreadPoolExecutor(int corePoolSize,                          int maximumPoolSize,                          long keepAliveTime,                          TimeUnit unit,                          BlockingQueue workQueue,                          ThreadFactory threadFactory,                          RejectedExecutionHandler handler)

其它这四个构造函数,一共牵涉到7个参数类型,下面主要讲解七个参数。

  • int corePoolSize => 该线程池中核心线程数最大值

核心线程:

线程池新建线程的时候,如果当前线程总数小于corePoolSize,则新建的是核心线程,如果超过corePoolSize,则新建的是非核心线程

核心线程默认情况下会一直存活在线程池中,即使这个核心线程啥也不干(闲置状态)。

如果指定ThreadPoolExecutor的allowCoreThreadTimeOut这个属性为true,那么核心线程如果不干活(闲置状态)的话,超过一定时间(时长下面参数决定),就会被销毁掉

很好理解吧,正常情况下你不干活我也养你,因为我总有用到你的时候,但有时候特殊情况(比如我自己都养不起了),那你不干活我就要把你干掉了

int maximumPoolSize

该线程池中线程总数最大值

线程总数 = 核心线程数 + 非核心线程数。核心线程在上面解释过了,这里说下非核心线程:

不是核心线程的线程(别激动,把刀放下…),其实在上面解释过了

long keepAliveTime

该线程池中非核心线程闲置超时时长

一个非核心线程,如果不干活(闲置状态)的时长超过这个参数所设定的时长,就会被销毁掉

如果设置allowCoreThreadTimeOut = true,则会作用于核心线程

TimeUnit unit

keepAliveTime的单位,TimeUnit是一个枚举类型,其包括:

NANOSECONDS : 1微毫秒 = 1微秒 / 1000

MICROSECONDS : 1微秒 = 1毫秒 / 1000

MILLISECONDS : 1毫秒 = 1秒 /1000

SECONDS : 秒

MINUTES : 分

HOURS : 小时

DAYS : 天

BlockingQueue workQueue

该线程池中的任务队列:维护着等待执行的Runnable对象

当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

常用的workQueue类型:

SynchronousQueue:这个队列接收到任务的时候,会直接提交给线程处理,而不保留它,如果所有线程都在工作怎么办?那就新建一个线程来处理这个任务!所以为了保证不出现的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大

LinkedBlockingQueue:这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

ArrayBlockingQueue:可以限定队列的长度,接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

DelayQueue:队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

ThreadFactory threadFactory

创建线程的方式,这是一个接口,你new他的时候需要实现他的Thread newThread(Runnable r)方法,一般用不上。

小伙伴应该知道AsyncTask是对线程池的封装吧?那就直接放一个AsyncTask新建线程池的threadFactory参数源码吧:

new ThreadFactory() {    private final AtomicInteger mCount = new AtomicInteger(1);     public Thread new Thread(Runnable r) {        return new Thread(r,"AsyncTask #" + mCount.getAndIncrement());    }}

RejectedExecutionHandler handler

这玩意儿就是抛出异常专用的,比如上面提到的两个错误发生了,就会由这个handler抛出异常,你不指定他也有个默认的抛异常能抛出什么花样来?一般情况下根本用不上。

新建一个线程池的时候,一般只用5个参数的构造函数。

向ThreadPoolExecutor添加任务

那说了这么多,你可能有疑惑,我知道new一个ThreadPoolExecutor,大概知道各个参数是干嘛的,可是我new完了,怎么向线程池提交一个要执行的任务啊?

通过ThreadPoolExecutor.execute(Runnable command)方法即可向线程池内添加一个任务

ThreadPoolExecutor的策略

上面介绍参数的时候其实已经说到了ThreadPoolExecutor执行的策略,这里给总结一下,当一个任务被添加进线程池时:

线程数量未达到corePoolSize,则新建一个线程(核心线程)执行任务

线程数量达到了corePools,则将任务移入队列等待

队列已满,新建线程(非核心线程)执行任务

队列已满,总线程数又达到了maximumPoolSize,就会由上面那位星期天(RejectedExecutionHandler)抛出异常

5、常见四种线程池

如果你不想自己写一个线程池,那么你可以从下面看看有没有符合你要求的(一般都够用了),如果有,那么很好你直接用就行了,如果没有,那你就老老实实自己去写一个吧

Java通过Executors提供了四种线程池,这四种线程池都是直接或间接配置ThreadPoolExecutor的参数实现的,下面我都会贴出这四种线程池构造函数的源码,各位大佬们一看便知!

来,走起:

CachedThreadPool()

可缓存线程池:

线程数无限制(没有核心线程,全部是非核心线程)

有空闲线程则复用空闲线程,若无空闲线程则新建线程

一定程序减少频繁创建/销毁线程,减少系统开销

适用场景:适用于耗时少,任务量大的情况

创建方法:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

源码:

public static ExecutorService newCachedThreadPool() {    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                  60L, TimeUnit.SECONDS,                                  new SynchronousQueue());}

FixedThreadPool()

定长线程池:

  1. 有核心线程,核心线程数就是线程的最大数量(没有非核心线程)
  2. 可控制线程最大并发数(同时执行的线程数)
  3. 超出的线程会在队列中等待

创建方法:

//nThreads => 最大线程数即maximumPoolSizeExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads); //threadFactory => 创建线程的方法!ExecutorService fixedThreadPool = Executors.newFixedThreadPool(int nThreads, ThreadFactory threadFactory);

源码:

public static ExecutorService newFixedThreadPool(int nThreads) {    return new ThreadPoolExecutor(nThreads, nThreads,                                  0L, TimeUnit.MILLISECONDS,                                  new LinkedBlockingQueue());}

2个参数的构造方法源码,不用我贴你也知道他把星期六放在了哪个位置!所以我就不贴了,省下篇幅给我扯皮

ScheduledThreadPool()

定长线程池:

支持定时及周期性任务执行。

有核心线程,也有非核心线程

非核心线程数量为无限大

适用场景:适用于执行周期性任务

创建方法:

//nThreads => 最大线程数即maximumPoolSizeExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(int corePoolSize);

源码:

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {    return new ScheduledThreadPoolExecutor(corePoolSize);} //ScheduledThreadPoolExecutor():public ScheduledThreadPoolExecutor(int corePoolSize) {    super(corePoolSize, Integer.MAX_VALUE,          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,          new DelayedWorkQueue());}

SingleThreadExecutor()

单线程化的线程池:

有且仅有一个工作线程执行任务

所有任务按照指定顺序执行,即遵循队列的入队出队规则

适用场景:适用于有顺序的任务应用场景

创建方法:

ExecutorService singleThreadPool = Executors.newSingleThreadPool();

源码:

public static ExecutorService newSingleThreadExecutor() {    return new FinalizableDelegatedExecutorService        (new ThreadPoolExecutor(1, 1,                                0L, TimeUnit.MILLISECONDS,                                new LinkedBlockingQueue()));}

还有一个Executors.newSingleThreadScheduledExecutor()结合了3和4,就不介绍了,基本不用。

6、什么是协程?

问题:协程存在的原因?协程能够解决哪些问题?

在我们现在CS,BS开发模式下,服务器的吞吐量是一个很重要的参数。其实吞吐量是IO处理时间加上业务处理。为了简单起见,比如,客户端与服务器之间是长连接的,客户端定期给服务器发送心跳包数据。客户端发送一次心跳包到服务器,服务器更新该新客户端状态的。心跳包发送的过程,业务处理时长等于IO读取(RECV系统调用)加上业务处理(更新客户状态)。吞吐量等于1s业务处理次数。

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第2张图片

业务处理(更新客户端状态)时间,业务不一样的,处理时间不一样,我们就不做讨论。

那如何提升recv的性能。若只有一个客户端,recv的性能也没有必要提升,也不能提升。若在有百万计的客户端长连接的情况,我们该如何提升。以Linux为例,在这里需要介绍一个“网红”就是epoll。服务器使用epoll管理百万计的客户端长连接,代码框架如下:

hile (1) {    int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);    for (i = 0;i < nready;i ++) {        int sockfd = events[i].data.fd;        if (sockfd == listenfd) {            int connfd = accept(listenfd, xxx, xxxx);                        setnonblock(connfd);            ev.events = EPOLLIN | EPOLLET;            ev.data.fd = connfd;            epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);        } else {            handle(sockfd);        }    }}

对于响应式服务器,所有的客户端的操作驱动都是来源于这个大循环。来源于epoll_wait的反馈结果。

对于服务器处理百万计的IO。Handle(sockfd)实现方式有两种。

第一种,handle(sockfd)函数内部对sockfd进行读写动作。代码如下

int handle(int sockfd) {    recv(sockfd, rbuffer, length, 0);        parser_proto(rbuffer, length);    send(sockfd, sbuffer, length, 0);    }

handle的io操作(send,recv)与epoll_wait是在同一个处理流程里面的。这就是IO同步操作。

优点:

1. sockfd管理方便。

2. 操作逻辑清晰。

缺点:

1. 服务器程序依赖epoll_wait的循环响应速度慢。

2. 程序性能差

第二种,handle(sockfd)函数内部将sockfd的操作,push到线程池中,代码如下:

int thread_cb(int sockfd) {    // 此函数是在线程池创建的线程中运行。    // 与handle不在一个线程上下文中运行    recv(sockfd, rbuffer, length, 0);    parser_proto(rbuffer, length);    send(sockfd, sbuffer, length, 0);}int handle(int sockfd) {    //此函数在主线程 main_thread 中运行    //在此处之前,确保线程池已经启动。    push_thread(sockfd, thread_cb); //将sockfd放到其他线程中运行。}

Handle函数是将sockfd处理方式放到另一个已经其他的线程中运行,如此做法,将io操作(recv,send)与epoll_wait 不在一个处理流程里面,使得io操作(recv,send)与epoll_wait实现解耦。这就叫做IO异步操作。

优点:

1. 子模块好规划。

2. 程序性能高。

缺点:

正因为子模块好规划,使得模块之间的sockfd的管理异常麻烦。每一个子线程都需要管理好sockfd,避免在IO操作的时候,sockfd出现关闭或其他异常。

上文有提到IO同步操作,程序响应慢,IO异步操作,程序响应快。

下面来对比一下IO同步操作与IO异步操作。

代码如下:

https://github.com/wangbojing/c1000k_test/blob/master/server_mulport_epoll.c

在这份代码的486行,#if 1, 打开的时候,为IO异步操作。关闭的时候,为IO同步操作。

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第3张图片

接下来把我测试接入量的结果粘贴出来。

IO异步操作,每1000个连接接入的服务器响应时间(900ms左右)。

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第4张图片

IO同步操作,每1000个连接接入的服务器响应时间(6500ms左右)。

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第5张图片

IO异步操作与IO同步操作

如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?_第6张图片

有没有一种方式,有异步性能,同步的代码逻辑。来方便编程人员对IO操作的组件呢? 有,采用一种轻量级的协程来实现。在每次send或者recv之前进行切换,再由调度器来处理epoll_wait的流程。

就是采用了基于这样的思考,写了NtyCo,实现了一个IO异步操作与协程结合的组件。https://github.com/wangbojing/NtyCo

线程、进程了解懂的人应该不少,但是什么是协程,纯C写的协程框架有了解过吗?

不急,关注我,后台私信【协程】,为你解密协程以下的内容:

协程框架实现,调度器模式实现,底层原理,多核模式,性能分析,ntyco作者亲讲

协程起源 — 存在的原因?协程能够解决哪些问题?

协程案例 — 如何使用?与线程使用有何区别?

协程实现之工作流程 — 内部是如何工作的?

协程实现之原语操作 — 原语操作有哪些?分别如何实现?

协程实现之切换 — 上下文如何切换?代码如何实现?

协程实现之定义 — 运行体如何定义?调度器如何定义?

协程实现之调度器 — 协程如何被调度?

协程多核模式 — 多核实现

协程性能测试 — 实战性能测试


你可能感兴趣的:(如何解决recv-q一直处于阻塞状态_进程、线程、线程池和协程如何理解?)