Linux 线程池的简单实现

pthread 线程池

最近经在在网上看到别人分享的各种优化策略,其中一个就是线程池。与线程池类似的技术还有数据库连接池、HTTP 连接池等等。
线程在创建销毁进行一系列的系统调用,资源分配与回收,在所以频繁创建销毁线程实际上会带来较大的系统开销,那么我们用线程池来统一管理线程就能够很好的解决这种资源管理问题。
比如因为不需要创建、销毁线程,每次需要用的时候我就去拿,用完了之后再放回去,所以节省了很多资源开销,可以提高系统的运行速度。
而统一的管理和调度,可以合理分配内部资源,根据系统的当前情况调整线程的数量。
那总结来说有以下 3 个好处:

  1. 降低资源消耗:通过重复利用现有的线程来执行任务,避免多次创建和销毁线程。
  2. 提高相应速度:因为省去了创建线程这个步骤,所以在拿到任务时,可以立刻开始执行。
  3. 提供附加功能:线程池的可拓展性使得我们可以自己加入新的功能,比如说定时、延时来执行某些线程。

基于pthread 的线程池简单实现

线程池的关键参数:

  1. 线程池的状态
    线程池需要有多种状态,比如,创建,运行,停止,消亡。添加新任务需要在运行态,其他状态不能接受新任务。
  2. 工作者个数
    就是线程池worker的线程数量。由于线程创建销毁都有资源开销,所以我们允许即使存在一定数量的线程空转也要保证线程池的最小worker数量;
    另外也要考虑限制worker上限,不能创建太多线程; 需要有相应的机制动态调整 worker 的数量,在任务繁忙增加worker,在任务空闲时候销毁worker。
  3. 任务队列
    标识着一系列的任务; 同样任务还需要有超时检测策略,防止任务长时间不被处理导致”饿死“; 在退出或者任务满时候需要拒绝新任务的创建

下述实现参考了 Android RILD 线程池实现技术但是并没有考虑任务的超时情况,以及任务的拒绝策略。
主要实现了以下几点:

  • 线程池的创建,最小工作线程,最大工作线程限制
  • 管理者线程,根据当前任务量动态伸缩(创建,销毁线程)
  • 任务的添加,如果任务过多,将会阻塞

下述实现的不足:

  • 采用互斥锁做同步,可以采用原子变量,无锁实现
  • 没有任务拒绝策略,队列满将阻塞新任务的创建
  • 没有任务超时机制,可能会导致部分任务长时间没执行处于“饥饿”
  • 线程池的伸缩比较简单,可以更细化的实现
  • supervisor 任务比较单一,可以把supervisor的工作交给worker处理
/*
 * @Author: sinpo828
 * @Date: 2021-03-18 14:21:16
 * @LastEditors: sinpo828
 * @LastEditTime: 2021-03-19 11:45:28
 * @Description: file content
 */
#ifndef THREADPOOL_H_
#define THREADPOOL_H_
#include <stdio.h>
#include <pthread.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <stdbool.h>

#define SUPPERVISOR_CHECK_PERIOD 10 // 十秒检查一次
#define WORKER_MIN_INCR_NUM 10      // 如果queue_size > MIN_INCR_TASK_NUM 添加新的线程到线程池
#define WORKER_DESTORY_MIN_NUM 10   // 每次创建销毁线程的个数

/*各子线程任务结构体*/
typedef struct
{
    void (*function)(void *); //函数指针做回调函数
    void *arg;
} threadpool_task_t;

/*线程池结构体*/
typedef struct
{
    pthread_mutex_t lock;      /* 用于锁住本结构体 */
    pthread_mutex_t busy_lock; /* 记录忙状态线程个数 */

    pthread_cond_t task_queue_not_full;  /*当任务队列满时,添加任务的线程阻塞*/
    pthread_cond_t task_queue_not_empty; /*任务队列里不为空时,通知等待任务的线程*/
    pthread_t *workers;                  /*存放线程池中每个线程的tid。数组*/
    pthread_t supervisor;                /* 管理线程tid */
    threadpool_task_t *task_queue;       /* 任务队列(数组首地址) */

    /* 线程数量信息 */
    int min_thr_num;       /* 线程池最小线程数 */
    int max_thr_num;       /* 线程池最大线程数 */
    int live_thr_num;      /* 当前存活线程个数 */
    int busy_thr_num;      /* 忙状态线程个数 */
    int wait_exit_thr_num; /* 要销毁的线程个数 */

    /* 任务队列,模拟一个环形缓冲区 */
    int task_queue_front;    /* task_queue 队头下标 */
    int task_queue_rear;     /* task_queue 队尾下标 */
    int task_queue_size;     /* task_queue 队中实际任务数 */
    int task_queue_max_size; /* task_queue 队列可容纳任务数上限 */
    int quit_flag;           /* 标志位,线程池使用状态, true或false,负责销毁线程池 */

} threadpool_t;

void *worker_thread(void *threadpool);
void *supervisor_thread(void *threadpool);
int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);

//线程池中的线程模拟处理业务
void process(void *arg)
{
    printf("thread %ld working on task %d\n", pthread_self(), *(int *)arg);
    sleep(1);
    printf("task %d is end\n", *(int *)arg);
}

/**
 * pthreadpool_create()
 * 创建线程池的结构体指针
 * 初始化线程池结构体(n个成员变量)
 * 创建n个任务线程
 * 创建一个管理者线程
 * 失败时,销毁所有开辟的空间
 */
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
    threadpool_t *pool = NULL; /*线程池结构体*/

    do
    {
        if ((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL)
        {
            printf("malloc threadpool fail\n");
            break;
        }

        pool->min_thr_num = min_thr_num;
        pool->max_thr_num = max_thr_num;
        pool->busy_thr_num = 0;
        pool->live_thr_num = min_thr_num;
        pool->wait_exit_thr_num = 0;
        pool->task_queue_size = 0;
        pool->task_queue_max_size = queue_max_size;
        pool->task_queue_front = 0;
        pool->task_queue_rear = 0;
        pool->quit_flag = false;

        /*根据最大线程上限数,给工作线程数组开辟空间,并清零*/
        pool->workers = (pthread_t *)malloc(sizeof(pthread_t) * max_thr_num);
        if (pool->workers == NULL)
        {
            printf("malloc workers fail");
            break;
        }

        memset(pool->workers, 0, sizeof(pthread_t) * max_thr_num);

        pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t) * queue_max_size);
        if (pool->task_queue == NULL)
        {
            printf("malloc task_queue fail");
            break;
        }

        //初始化互斥锁,条件变量
        if (pthread_mutex_init(&(pool->lock), NULL) != 0 ||
            pthread_mutex_init(&(pool->busy_lock), NULL) != 0 ||
            pthread_cond_init(&(pool->task_queue_not_empty), NULL) != 0 ||
            pthread_cond_init(&(pool->task_queue_not_full), NULL) != 0)
        {
            perror("init lock or cond error\n");
            break;
        }

        //启动 min_thr_num 个 work thread
        for (int i = 0; i < min_thr_num; i++)
        {
            pthread_create(&(pool->workers[i]), NULL, worker_thread, (void *)pool); //pool指向当前线程池
            printf("start thread %ld...\n", pool->workers[i]);
        }

        pthread_create(&(pool->supervisor), NULL, supervisor_thread, (void *)pool); //创建管理者线程

        return pool;

    } while (0);

    threadpool_free(pool); //前面的代码调用失败,释放poll存储空间

    return NULL;
}

/**
 * worker 工作者线程,每次从任务队列取任务执行
 */
void *worker_thread(void *threadpool)
{
    threadpool_t *pool = (threadpool_t *)threadpool;
    threadpool_task_t task;

    while (true)
    {
        pthread_mutex_lock(&(pool->lock));

        while ((pool->task_queue_size == 0) && (!pool->quit_flag))
        {
            printf("thread %ld is waiting\n", pthread_self());
            pthread_cond_wait(&(pool->task_queue_not_empty), &(pool->lock)); //所有的子线程都阻塞在这里,这部分非常重要

            // 可能是来自 supervisor 的假唤醒,检查是否闲置线程太多,需要销毁
            if (pool->wait_exit_thr_num > 0)
            {
                pool->wait_exit_thr_num--;

                // 如果线程池中的线程个数大于最小值的时候可以结束当前线程
                if (pool->live_thr_num > pool->min_thr_num)
                {
                    printf("thread %ld is exiting\n", pthread_self());
                    pool->live_thr_num--;
                    pthread_mutex_unlock(&(pool->lock));
                    pthread_exit(NULL);
                }
            }
        }

        // 如果指定了true,要关闭线程池里的每个线程,自动退出处理--销毁线程池
        if (pool->quit_flag)
        {
            pthread_mutex_unlock(&(pool->lock));
            printf("thread %ld is exiting\n", pthread_self());
            pthread_exit(NULL);
        }

        /*从任务队列里获取任务,是一个出队操作*/
        task.function = pool->task_queue[pool->task_queue_front].function;
        task.arg = pool->task_queue[pool->task_queue_front].arg;

        pool->task_queue_front = (pool->task_queue_front + 1) % pool->task_queue_max_size; /*出队,模拟环形队列*/
        pool->task_queue_size--;

        /* 通知可以有新的任务添加进来 */
        pthread_cond_broadcast(&(pool->task_queue_not_full));

        /* 任务取出后,立即将线程池琐释放 */
        pthread_mutex_unlock(&(pool->lock));

        /*执行任务*/
        printf("worker %ld start working\n", pthread_self());
        pthread_mutex_lock(&(pool->busy_lock)); /*忙状态线程数变量琐*/
        pool->busy_thr_num++;
        pthread_mutex_unlock(&(pool->busy_lock)); /*忙状态线程数+1*/
        task.function(task.arg);                  /*执行回调函数任务*/

        /*任务结束处理*/
        printf("worker %ld end working\n", pthread_self());
        pthread_mutex_lock(&(pool->busy_lock)); /*处理掉一个任务,忙状态数线程数-1*/
        pool->busy_thr_num--;
        pthread_mutex_unlock(&(pool->busy_lock));
    }

    pthread_exit(NULL);
}

/**
 * supervisor_thread 根据策略新增/销毁工作者线程
 */

void *supervisor_thread(void *threadpool)
{
    threadpool_t *pool = (threadpool_t *)threadpool;

    printf("supervisor %ld start working\n", pthread_self());
    while (true)
    {
        int now = 0;
        do
        {
            sleep(1);
        } while (now++ < SUPPERVISOR_CHECK_PERIOD && !pool->quit_flag);

        if (pool->quit_flag)
            break;

        pthread_mutex_lock(&(pool->lock));      //获取锁进行操作
        int queue_size = pool->task_queue_size; //关注任务数
        int live_thr_num = pool->live_thr_num;  //存活线程数
        pthread_mutex_unlock(&(pool->lock));

        pthread_mutex_lock(&(pool->busy_lock)); //获取锁进行操作
        int busy_thr_num = pool->busy_thr_num;  //忙着的线程数
        pthread_mutex_unlock(&(pool->busy_lock));

        // 扩容条件: 任务数大于最小线程池个数的两倍,且存活的线程数少于最大线程个数
        if (queue_size >= live_thr_num * 2 && live_thr_num < pool->max_thr_num)
        {
            pthread_mutex_lock(&(pool->lock));
            int add = 0;

            // 一次增加 WORKER_MIN_INCR_NUM 个线程
            for (int i = 0; i < pool->max_thr_num &&
                            add < WORKER_MIN_INCR_NUM &&
                            pool->live_thr_num < pool->max_thr_num;
                 i++)
            {
                if (pool->workers[i] == 0 || !is_thread_alive(pool->workers[i]))
                {
                    pthread_create(&(pool->workers[i]), NULL, worker_thread, (void *)pool);
                    add++;
                    pool->live_thr_num++;
                }
            }

            pthread_mutex_unlock(&(pool->lock));
        }

        // 收缩条件: 忙线程*2 小于存活的线程数且存活的线程数大于最小线程数时
        if (busy_thr_num * 2 < live_thr_num && live_thr_num > pool->min_thr_num)
        {
            pthread_mutex_lock(&(pool->lock));
            pool->wait_exit_thr_num = WORKER_DESTORY_MIN_NUM;
            pthread_mutex_unlock(&(pool->lock));

            for (int i = 0; i < WORKER_DESTORY_MIN_NUM; i++)
            {
                // 虚假唤醒,worker 会自动判断是否要退出
                pthread_cond_signal(&(pool->task_queue_not_empty));
            }
        }
    }
    printf("supervisor %ld end working\n", pthread_self());

    return NULL;
}

/**
 * 向线程池中添加一个任务
 */
int threadpool_add(threadpool_t *pool, void (*function)(void *arg), void *arg)
{
    pthread_mutex_lock(&(pool->lock));

    /* 队列已经满,调wait阻塞*/
    while ((pool->task_queue_size == pool->task_queue_max_size) && (!pool->quit_flag))
    {
        pthread_cond_wait(&(pool->task_queue_not_full), &(pool->lock));
    }

    if (pool->quit_flag)
    {
        pthread_cond_broadcast(&(pool->task_queue_not_empty)); //虚假唤醒所有被阻塞的线程。
        pthread_mutex_unlock(&(pool->lock));
        return 0;
    }

    /*添加任务到任务队列里*/
    pool->task_queue[pool->task_queue_rear].function = function;
    pool->task_queue[pool->task_queue_rear].arg = arg;
    pool->task_queue_rear = (pool->task_queue_rear + 1) % pool->task_queue_max_size;
    pool->task_queue_size++;

    //添加完任务后.队列不为空,唤醒线程池中 等待处理任务的线程
    pthread_cond_signal(&(pool->task_queue_not_empty));
    pthread_mutex_unlock(&(pool->lock));
    return 0;
}

/**
 * 销毁线程池,注意要先干掉管理者线程
 */
int threadpool_destroy(threadpool_t *pool)
{
    if (pool == NULL)
        return 0;

    pool->quit_flag = true;

    //先销毁管理线程
    pthread_join(pool->supervisor, NULL);
    pthread_cond_broadcast(&(pool->task_queue_not_empty));

    for (int i = 0; i < pool->live_thr_num; i++)
        pthread_join(pool->workers[i], NULL);

    threadpool_free(pool);
    return 0;
}

int threadpool_free(threadpool_t *pool)
{
    if (pool == NULL)
        return 0;

    if (pool->task_queue)
        free(pool->task_queue);

    if (pool->workers)
    {
        free(pool->workers);
        pthread_mutex_lock(&(pool->lock));
        pthread_mutex_destroy(&(pool->lock));
        pthread_mutex_lock(&(pool->busy_lock));
        pthread_mutex_destroy(&(pool->busy_lock));
        pthread_cond_destroy(&(pool->task_queue_not_full));
        pthread_cond_destroy(&(pool->task_queue_not_empty));
    }
    free(pool);
    pool = NULL;

    return 0;
}

int threadpool_all_threadnum(threadpool_t *pool)
{
    int all_threadnum = -1; //总线程数

    pthread_mutex_lock(&(pool->lock));
    all_threadnum = pool->live_thr_num; //存活线程数
    pthread_mutex_unlock(&(pool->lock));

    return all_threadnum;
}

int threadpool_busy_threadnum(threadpool_t *pool)
{
    int busy_threadnum = -1; //忙线程数

    pthread_mutex_lock(&(pool->busy_lock));
    busy_threadnum = pool->busy_thr_num; //忙线程数
    pthread_mutex_unlock(&(pool->busy_lock));

    return busy_threadnum;
}

int is_thread_alive(pthread_t tid)
{
    /*pthread_kill的返回值:成功(0) 线程不存在(ESRCH) 信号不合法(EINVAL)*/

    int pthread_kill_err;
    pthread_kill_err = pthread_kill(tid, 0);

    if (pthread_kill_err == ESRCH || pthread_kill_err == EINVAL)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

#endif

int main()
{
    //pool init
    threadpool_t *thp = threadpool_create(10, 100, 100); //创建线程池,池中最小10个最多100个,任务队列最大100个

    int *num = (int *)malloc(sizeof(int) * 20);

    for (int i = 0; i < 20; i++)
    {
        num[i] = i;
        printf("新加任务: %d\n", i);
        threadpool_add(thp, process, (void *)&num[i]); //向线程池中添加任务,借助回调处理任务
    }

    sleep(5); //模拟线程处理任务
    threadpool_destroy(thp);
    printf("线程全部摧毁\n");
}
➜  go 
➜  go 
➜  go clang xx.c -lpthread
➜  go ./a.out             
start thread 140544928998976...
start thread 140544920606272...
start thread 140544912213568...
thread 140544928998976 is waiting
start thread 140544903820864...
thread 140544920606272 is waiting
start thread 140544895428160...
start thread 140544887035456...
thread 140544912213568 is waiting
thread 140544903820864 is waiting
start thread 140544878642752...
thread 140544887035456 is waiting
start thread 140544870250048...
start thread 140544861857344...
thread 140544861857344 is waiting
thread 140544878642752 is waiting
start thread 140544853464640...
thread 140544895428160 is waiting
thread 140544853464640 is waiting
新加任务: 0
supervisor 140544845071936 start working
新加任务: 1
worker 140544928998976 start working
worker 140544870250048 start working
thread 140544870250048 working on task 1
新加任务: 2
thread 140544928998976 working on task 0
worker 140544912213568 start working
thread 140544912213568 working on task 2
新加任务: 3
新加任务: 4
worker 140544903820864 start working
worker 140544887035456 start working
thread 140544887035456 working on task 4
新加任务: 5
thread 140544903820864 working on task 3
worker 140544861857344 start working
thread 140544861857344 working on task 5
新加任务: 6
新加任务: 7
worker 140544878642752 start working
thread 140544878642752 working on task 6
worker 140544895428160 start working
thread 140544895428160 working on task 7
新加任务: 8
新加任务: 9
新加任务: 10
新加任务: 11
新加任务: 12
新加任务: 13
新加任务: 14
新加任务: 15
新加任务: 16
新加任务: 17
新加任务: 18
新加任务: 19
worker 140544853464640 start working
thread 140544853464640 working on task 8
worker 140544920606272 start working
thread 140544920606272 working on task 9
task 1 is end
task 2 is end
task 4 is end
worker 140544912213568 end working
worker 140544912213568 start working
thread 140544912213568 working on task 10
worker 140544870250048 end working
worker 140544870250048 start working
thread 140544870250048 working on task 11
task 5 is end
worker 140544861857344 end working
worker 140544861857344 start working
task 6 is end
worker 140544878642752 end working
worker 140544878642752 start working
thread 140544878642752 working on task 13
worker 140544887035456 end working
worker 140544887035456 start working
thread 140544887035456 working on task 14
task 0 is end
worker 140544928998976 end working
task 8 is end
task 7 is end
worker 140544895428160 end working
worker 140544895428160 start working
thread 140544895428160 working on task 16
worker 140544928998976 start working
thread 140544928998976 working on task 15
thread 140544861857344 working on task 12
task 3 is end
worker 140544903820864 end working
worker 140544903820864 start working
thread 140544903820864 working on task 17
task 9 is end
worker 140544920606272 end working
worker 140544920606272 start working
thread 140544920606272 working on task 18
worker 140544853464640 end working
worker 140544853464640 start working
thread 140544853464640 working on task 19
task 10 is end
worker 140544912213568 end working
thread 140544912213568 is waiting
task 11 is end
worker 140544870250048 end working
thread 140544870250048 is waiting
task 13 is end
worker 140544878642752 end working
thread 140544878642752 is waiting
task 14 is end
worker 140544887035456 end working
thread 140544887035456 is waiting
task 16 is end
worker 140544895428160 end working
thread 140544895428160 is waiting
task 15 is end
worker 140544928998976 end working
thread 140544928998976 is waiting
task 12 is end
worker 140544861857344 end working
thread 140544861857344 is waiting
task 17 is end
worker 140544903820864 end working
thread 140544903820864 is waiting
task 18 is end
worker 140544920606272 end working
thread 140544920606272 is waiting
task 19 is end
worker 140544853464640 end working
thread 140544853464640 is waiting
supervisor 140544845071936 end working
thread 140544870250048 is exiting
thread 140544903820864 is exiting
thread 140544853464640 is exiting
thread 140544928998976 is exiting
thread 140544912213568 is exiting
thread 140544878642752 is exiting
thread 140544861857344 is exiting
thread 140544895428160 is exiting
thread 140544887035456 is exiting
thread 140544920606272 is exiting
线程全部摧毁
➜  go clang xx.c -lpthread
➜  go ./a.out             
start thread 139623744407104...
thread 139623744407104 is waiting
start thread 139623736014400...
thread 139623736014400 is waiting
start thread 139623727621696...
thread 139623727621696 is waiting
start thread 139623719228992...
thread 139623719228992 is waiting
start thread 139623710836288...
thread 139623710836288 is waiting
start thread 139623702443584...
thread 139623702443584 is waiting
start thread 139623694050880...
thread 139623694050880 is waiting
start thread 139623685658176...
thread 139623685658176 is waiting
thread 139623677265472 is waiting
start thread 139623677265472...
start thread 139623668872768...
thread 139623668872768 is waiting
新加任务: 0
supervisor 139623660480064 start working
新加任务: 1
新加任务: 2
worker 139623736014400 start working
新加任务: 3
thread 139623736014400 working on task 0
worker 139623719228992 start working
worker 139623727621696 start working
thread 139623727621696 working on task 1
worker 139623744407104 start working
thread 139623744407104 working on task 3
新加任务: 4
thread 139623719228992 working on task 2
worker 139623710836288 start working
thread 139623710836288 working on task 4
新加任务: 5
新加任务: 6
worker 139623694050880 start working
thread 139623694050880 working on task 5
新加任务: 7
worker 139623702443584 start working
thread 139623702443584 working on task 6
worker 139623685658176 start working
thread 139623685658176 working on task 7
新加任务: 8
新加任务: 9
worker 139623677265472 start working
新加任务: 10
新加任务: 11
新加任务: 12
新加任务: 13
新加任务: 14
新加任务: 15
新加任务: 16
新加任务: 17
新加任务: 18
新加任务: 19
worker 139623668872768 start working
thread 139623668872768 working on task 9
thread 139623677265472 working on task 8
task 0 is end
worker 139623736014400 end working
worker 139623736014400 start working
thread 139623736014400 working on task 10
task 1 is end
task 2 is end
task 4 is end
worker 139623710836288 end working
task 3 is end
worker 139623744407104 end working
worker 139623744407104 start working
thread 139623744407104 working on task 12
worker 139623710836288 start working
thread 139623710836288 working on task 11
task 7 is end
worker 139623685658176 end working
worker 139623685658176 start working
thread 139623685658176 working on task 13
task 5 is end
worker 139623694050880 end working
worker 139623694050880 start working
thread 139623694050880 working on task 14
task 6 is end
worker 139623702443584 end working
worker 139623702443584 start working
thread 139623702443584 working on task 15
worker 139623727621696 end working
worker 139623727621696 start working
thread 139623727621696 working on task 16
worker 139623719228992 end working
worker 139623719228992 start working
thread 139623719228992 working on task 17
task 8 is end
worker 139623677265472 end working
worker 139623677265472 start working
thread 139623677265472 working on task 18
task 9 is end
worker 139623668872768 end working
worker 139623668872768 start working
thread 139623668872768 working on task 19
task 10 is end
worker 139623736014400 end working
thread 139623736014400 is waiting
task 11 is end
worker 139623710836288 end working
thread 139623710836288 is waiting
task 12 is end
task 13 is end
worker 139623685658176 end working
thread 139623685658176 is waiting
task 14 is end
task 15 is end
worker 139623702443584 end working
task 16 is end
worker 139623727621696 end working
worker 139623694050880 end working
thread 139623702443584 is waiting
worker 139623744407104 end working
thread 139623744407104 is waiting
task 17 is end
worker 139623719228992 end working
thread 139623694050880 is waiting
task 19 is end
worker 139623668872768 end working
thread 139623727621696 is waiting
task 18 is end
worker 139623677265472 end working
thread 139623677265472 is waiting
thread 139623719228992 is waiting
thread 139623668872768 is waiting
supervisor 139623660480064 end working
thread 139623736014400 is exiting
thread 139623677265472 is exiting
thread 139623685658176 is exiting
thread 139623710836288 is exiting
thread 139623727621696 is exiting
thread 139623668872768 is exiting
thread 139623702443584 is exiting
thread 139623719228992 is exiting
thread 139623744407104 is exiting
thread 139623694050880 is exiting
线程全部摧毁
➜  go 
posted @ 2021-03-19 13:08  sinpo828  阅读(56)  评论(0编辑  收藏  举报