【嵌入式Linux】<总览> 多线程

文章目录

前言

一、多线程

1. 概述

2. 创建线程

3. 线程退出

4. 线程回收

5. 线程分离

6. 线程取消

7. 线程的ID比较

8. 注册线程清理函数

9. 线程属性

10. 可重入函数

二、线程同步

1. 概述

2. 互斥锁

3. 死锁

4. 自旋锁

5. 读写锁

6. 条件变量

7. 信号量

三、线程池

1. 作用

2. 实现原理

3. 手撕线程池


前言

记录学习多线程的知识重点与难点,若涉及版权问题请联系本人删除!


一、多线程

1. 概述

  • 线程是轻量级的进程,一个进程可以涵盖多个线程。
  • 线程是操作系统调度的最小单位,进程是资源分配的最小单位。
  • 多个线程有不同的“地位”:进程的虚拟地址空间的生命周期默认和主线程一样,与创建的子线程无关。当主线程执行完毕,虚拟地址空间就会被释放。
  • 每个线程都有唯一的线程ID(无符号的长整型数),类型为pthread_t。调用pthread_self函数就能得到当前线程的ID。
#include <pthread.h>

pthread_t pthread_self(void);

//使用pthread库,链接时需要加上选项-lpthread来指定动态库

进程与线程的区别:

  • 进程有独立的地址空间,线程共用同一个地址空间。每个线程都有自己的栈区和寄存器,多个线程共享代码段、堆区、全局数据区和文件描述符表。
  • 线程的上下文切换比进程快得多。

上下文切换:进程/线程分时复用CPU时间片,在切换前会保存当前状态,下次执行该任务时加载保存的状态。

控制多个线程个数:

  • 针对文件IO:线程个数=2*CPU核心数。
  • 针对复杂算法:线程个数=CPU核心数。

2. 创建线程

调用pthread_create函数:

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

【3】参数说明:

  • thread:传出的参数,保存创建的线程ID。
  • attr:线程的属性,一般为NULL表示默认的属性。
  • start_routine:函数指针,线程的执行函数。
  • arg:作为实参传递到start_routine指向的函数内部。

【4】返回值:成功返回0,失败返回错误码。

程序实例:创建子线程,并在主线程内打印子线程和主线程的tid。同时,在子线程执行函数中打印信息。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* doing(void *arg)
{
    printf("我是子线程, ID: %ld\n", pthread_self());
    return NULL;
}

int main(int argc, char ** argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (!pthread_create(&tid, NULL, doing, NULL)) {
        printf("子线程创建成功,其ID为:%ld\n", tid);
    }

    /* 主线程才会执行 */
    printf("主线程的ID:%ld\n", pthread_self());
    while (1) {
        sleep(1);
    }
    return 0;
}

3. 线程退出

调用pthread_exit函数,调用后线程就退出了。只有当所有线程都退出了,虚拟地址空间才会释放。

【1】头文件:#include <pthread.h>

【2】函数原型:void pthread_exit(void *retval);

【3】参数说明:retval表示子线程退出后返回主线程的数据。不需要时使用NULL。

程序实例:创建子线程,在子线程的执行函数中睡眠3秒后打印信息,主进程创建完子线程后调用pthread_exit函数退出。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

/* 子线程执行函数 */
void* doing(void *arg)
{
        sleep(3);//延迟三秒,确定主线程是否会退出
        for (int i = 0; i < 9; ++i) {
                printf("我是子线程!\n");
                if (i == 3) {
                        pthread_exit(NULL);
                }
        }
        return NULL;
}

int main(int argc, char **argv)
{
        /* 创建子线程 */
        pthread_t tid;
        if (!pthread_create(&tid, NULL, doing, NULL)) {
                printf("子线程创建成功,ID: %ld\n", tid);
        }

        /* 主线程:调用pthread_exit不会释放虚拟地址空间 */
        pthread_exit(NULL);
        return 0;
}

4. 线程回收

主线程调用pthread_join函数来阻塞式回收子线程。若子线程还在运行,那么该函数就会阻塞。该函数只能回收一个子线程,若想回收多个可以考虑采用循环。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_join(pthread_t thread, void **retval);

【3】参数说明:

  • thread:要回收的子线程ID。
  • retval:接收子线程通过pthread_exit传出的数据。如果目标线程被 pthread_cancel函数取消, 则将PTHREAD_CANCELED(就是(void*)-1)放在*retval 中。

【4】返回值:回收成功返回0,失败返回错误号。

【5】注意:

  • 若子线程返回的数据位于子线程的栈区中,那么当子线程退出后其栈区就会被释放,主线程获取的数据就是无效的。
  • 由于多个线程共用堆区和全局数据区,可以将子线程的数据保存于全局变量中。
  • 由于主线程要回收子线程,一般都是最后退出。因此,可以将主线程的栈区变量传入子线程,在子线程中进行修改。

程序实例:借助主线程的栈区变量,将其传入子线程中,子线程将数据保存至该变量中,最后通过pthread_exit函数返回主线程,主线程调用pthread_join函数回收子线程并获取子线程返回的数据。

#include <stdio.h>
#include <pthread.h>

void* doing(void *arg)
{
    int *i = (int *)arg;
    *i = 666666;
    printf("子线程将参数修改为: %d\n", *i);
    pthread_exit(i);
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    int variable = 0;//主线程栈区变量
    pthread_create(&tid, NULL, doing, &variable);

    /* 主线程获取子线程退出时的数据 */
    void *ret = NULL;
    pthread_join(tid, &ret);//回收子线程
    printf("主线程获取子线程数据: %d\n", *(int*)ret);
    return 0;
}

5. 线程分离

如果总是让主线程来回收子线程,那么可能会出现子线程一直运行,而主线程阻塞在pthread_join函数,无法执行其他任务。因此,可以调用pthread_detach函数将子线程分离。当子线程退出时,其内核资源就会被系统其他进程接管并回收。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_detach(pthread_t thread);

【3】参数说明:thread是要分离的线程ID。

【4】返回值:成功返回0,失败返回错误号。

程序实例:主线程与子线程分离,主线程执行完任务后就退出。

#include <stdio.h>
#include <pthread.h>

void* doing(void *arg)
{
    for (int i = 0; i < 9; ++i) {
        printf("Hello, Can! %d\n", i);
        if (i == 4) {
            pthread_exit(NULL);
        }
    }
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程  */
    pthread_t tid;
    int retCreate = pthread_create(&tid, NULL, doing, NULL);
    if (retCreate != 0) {
        perror("pthread_create error!");
        return -1;
    }

    /* 主线程与子线程分离 */
    int retDetach = pthread_detach(tid);
    if (retDetach != 0) {
        perror("pthread_detach error!");
        pthread_join(tid, NULL);
    }

    /* 主线程执行其他任务 */
    for (int i = 0; i < 5; i++) {
        printf("我是主线程, %d\n", i);
    }
    pthread_exit(NULL);

    return 0;
}

6. 线程取消

线程取消就是一个线程杀死另一个线程。线程A杀死线程B需要两个条件:①线程A调用pthread_cancel函数;②线程B进行一次系统调用(取消点),从用户态切换回内核态。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_cancel(pthread_t thread);

【3】参数说明:thread是要杀死的线程ID。

【4】返回值:成功返回0,失败返回错误号。

设置线程不能被取消:int pthread_setcancelstate(int state, int *oldstate);

  • state:①PTHREAD_CANCEL_ENABLE能被取消(默认);②PTHREAD_CANCEL_DISABLE
    不能被取消。
  • oldstate:线程之前的取消性状态。不关心则为NULL。

设置线程的取消类型:int pthread_setcanceltype(int type, int *oldtype);

  • type:①PTHREAD_CANCEL_DEFERRED线程继续运行,直到到达某个取消点(默认);②PTHREAD_CANCEL_ASYNCHRONOUS(应用场景很少)。

产生取消点:void pthread_testcancel(void);若某个线程已有被取下的需求,那么在执行该函数时,线程就会终止。

程序实例:主线程杀死子线程。当子线程执行pthread_self函数时就会被杀死,因为该函数间接调用了系统调用函数。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* doing(void *arg)
{
    printf("子线程: 我要调用pthread_self了!\n");
    printf("子线程: %ld\n", pthread_self());
    sleep(2);//确保杀死子线程时间足够
    printf("子线程: 我还活着吗?\n");

    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (pthread_create(&tid, NULL, doing, NULL) != 0) {
        perror("pthread_create error!");
        return -1;
    }

    /* 主线程杀死子线程 */
    if (pthread_cancel(tid) != 0) {
        perror("pthread_cancel error!");
    }

    /* 主线程退出  */
    pthread_exit(NULL);
    return 0;
}

7. 线程的ID比较

调用pthread_equal函数来比较两个线程的ID是否相等。

【1】头文件:#include <pthread.h>

【2】函数原型:int pthread_equal(pthread_t t1, pthread_t t2);

【3】参数说明:t1和t2就是两个线程的ID。

【4】返回值:相同返回非0值,不同返回0.

程序实例:主线程创建子线程,然后判断主线程和子线程的ID是否相同。

#include <stdio.h>
#include <pthread.h>

void* doing(void *arg)
{
    printf("我是子线程\n");
    return NULL;
}

int main(int argc, char **argv)
{
    /* 创建子线程 */
    pthread_t tid;
    if (pthread_create(&tid, NULL, doing, NULL) != 0) {
        perror("create error!");
        return -1;
    }

    /* 比较线程ID是否相等*/
    pthread_t tidMain = pthread_self();
    if (pthread_equal(tidMain, tid) > 0) {
        printf("主线程ID: %ld, 子线程ID: %ld, 二者相等\n", tidMain, tid);
    } else {
        printf("主线程ID: %ld, 子线程ID: %ld, 二者不相等\n", tidMain, tid);
    }

    return 0;
}

8. 注册线程清理函数

当线程退出时,可以去执行相关的处理函数,这类函数被称为线程清理函数。与进程的atexit函数类似,可以登记/注册多个,当执行时也是按照栈的方式来执行。

【1】通过pthread_cleanup_push和pthread_cleanup_pop函数分别负责向调用线程的清理函数栈中添加和移除清理函数:

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

【2】注意:

  • pthread_cleanup_pop函数中的execute参数可以为0也可以为非0。若为0,只是移除最顶层的清理函数,而不会调用;若不为0,除了移除外还会调用。
  • 只有执行以下动作才会执行清理函数:①调用pthread_exit函数;②线程响应取消请求;③用非0参数调用pthread_cleanup_pop函数。
  • pthread_cleanup_push和 pthread_cleanup_pop通过宏来实现的,因此必须一一对应,否则编译出错。

程序实例:

#include <stdio.h>
#include <pthread.h>

/* 线程清理函数 */
void cleanup(void* arg) 
{
    printf("cleanup: %s\n", (char*)arg);
}

/* 线程执行函数 */
void* doing(void* arg)
{
    //注册线程清理函数
    pthread_cleanup_push(cleanup, "First");
    pthread_cleanup_push(cleanup, "Sencond");
    pthread_cleanup_push(cleanup, "Third");

    //手动执行清理函数
    pthread_cleanup_pop(-1);

    //执行其它代码
    printf("-------------------------\n");
    pthread_exit((void*)0);

    //为了与pthread_cleanup_push对应
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
    return NULL;
}

/* 主函数 */
int main(int argc, char** argv)
{
    //创建子线程
    pthread_t tid;
    if (pthread_create(&tid, NULL, doing, NULL) != 0) {
        perror("pthread_create error");
        return -1;
    }
    //阻塞回收子线程
    void* retval = NULL;
    int ret = pthread_join(tid, &retval);
    if (ret) {
        perror("pthread_join error");
        return -1;
    }
    printf("thread exit: %d\n", (int)retval);
    return 0; 
}

9. 线程属性

在通过pthread_create函数来创建线程时,需要指定线程的属性,之前都是采用NULL来指定为默认属性,当然可以自定义线程的属性。

注意:以下函数的返回值都是:成功返回0,失败返回错误号。

【1】初始化和销毁线程属性变量:

#include <pthread.h>

pthread_attr_t attr;//创建线程属性变量
int pthread_attr_init(pthread_attr_t *attr);//初始化为默认值
int pthread_attr_destroy(pthread_attr_t *attr);//销毁

【2】获取/设置线程栈的属性:(系统默认的栈大小为8MB)当设置的栈大小<2MB时,系统还是给8MB的栈空间大小。

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);

//单独设置与获取
int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);

【3】获取/设置线程分离的属性:设置该属性后,线程一开始运行就是分离的,后续无需再调用pthread_detach函数。

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//detachstate: 有两个取值
    //PTHREAD_CREATE_DETACHED: 分离启动,线程最后由操作系统回收
    //PTHREAD_CREATE_JOINABLE: (默认)正常启动线程

程序实例1:创建新线程,将线程的栈大小设置为3MB(24576bytes)。

#include <stdio.h>
#include <pthread.h>

/* 线程执行函数 */
void* doing(void* arg)
{
    printf("Hello, Can!\n");
    return NULL;
}

/* 主函数 */
int main(int argc, char** argv)
{
    //初始化线程属性
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    //设置线程栈大小为3MB
    pthread_attr_setstacksize(&attr, 24576);

    //查看线程属性中栈的大小
    size_t attr_size;
    pthread_attr_getstacksize(&attr, &attr_size);
    printf("stack size: %lu\n", attr_size);

    //创建子线程
    pthread_t tid;
    if (pthread_create(&tid, &attr, doing, NULL)) {
        perror("pthread_create error");
        return -1;
    }

    //阻塞回收子线程
    if (pthread_join(tid, NULL)) {
        perror("pthread_join error");
        return -1;
    }

    //销毁线程属性
    pthread_attr_destroy(&attr);
    return 0; 
}

程序实例2:设置线程为分离启动。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

/* 线程执行函数 */
void* doing(void* arg)
{
    printf("Hello, Can!\n");
    return NULL;
}

/* 主函数 */
int main(int argc, char** argv)
{
    //初始化线程属性
    pthread_attr_t attr;
    pthread_attr_init(&attr);

    //设置线程为分离启动
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

    //创建子线程
    pthread_t tid;
    if (pthread_create(&tid, &attr, doing, NULL)) {
        perror("pthread_create error");
        return -1;
    }
    sleep(2);

    //销毁线程属性
    pthread_attr_destroy(&attr);
    return 0; 
}

10. 可重入函数

【1】概念:多个执行流执行同一个函数,并且能得到预期效果,那么该函数就是可重入函数。

【2】场景:

  • 多线程执行同一个函数。
  • 程序中涉及信号处理,信号处理函数也被其它线程调用。

【3】可重入函数举例:由于i都是位于线程栈内部的,多个线程之间互不影响,其执行效果是能预期获得的。

int add()
{
    int i = 0;
    return i;
}

【4】不可重入函数举例:由于涉及临界区资源count,多线程来执行该函数可能会造成数据混乱,无法得到预期效果。

int count = 0;
void add()
{
    int tmp = count;
    tmp++;
    count = tmp;
}

二、线程同步

1. 概述

  • 当有多个线程访问同一个共享资源(临界资源)时,且不允许同时访问,那么就需要线程同步。
  • 常见的线程同步方式:互斥锁、读写锁、条件变量、信号量。

2. 互斥锁

互斥锁的方式可以简单概括为:锁定操作临界资源的代码片段,锁定后每次只能由一个线程来进行操作。这样能够解决多个线程同时访问临界资源造成的数据混乱,但是降低了执行效率(因为并行操作变成了串行操作)。

【1】互斥锁类型:pthread_mutex_t。创建一个该类型的变量就得到一把互斥锁。该变量中保存了锁的状态(打开还是锁定),若为锁定则保存了加锁的线程ID。锁定时其他线程将会阻塞,直到这个互斥锁被打开。

以下函数的返回值:成功返回0,失败返回错误号。

【2】初始化互斥锁:

//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                        const pthread_mutexattr_t *restrict attr);
//mutex: 互斥锁地址
//attr: 互斥锁属性,一般为NULL默认属性

【3】释放互斥锁资源:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

【4】加锁(阻塞):

int pthread_mutex_lock(pthread_mutex_t *mutex);
//若锁是打开的,那么加锁成功,锁会记录线程ID。
//若锁是锁定的,那么加锁失败,线程阻塞在此,直到上一个线程解锁。

【5】加锁(非阻塞):

int pthread_mutex_trylock(pthread_mutex_t *mutex);
//若加锁失败,则不会阻塞,而是直接返回错误号。
//锁被占用时,返回的是EBUSY(整数16)

【6】解锁:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
//解锁还需加锁人,哪个线程加的锁就得哪个线程来解锁。

【7】互斥锁属性:pthread_mutexattr_t类型

①创建互斥锁属性变量:pthread_mutexattr_t attr;

②初始化互斥锁属性变量:int pthread_mutexattr_init(pthread_mutexattr_t *attr);

③销毁互斥锁属性变量:int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

互斥锁类型如下:

  • PTHREAD_MUTEX_NORMAL:不做任何的错误检测。
  • PTHREAD_MUTEX_ERRORCHECK:会做错误检测,效率不高,可以用于调试。
  • PTHREAD_MUTEX_RECURSIVE:允许多次加锁,维护加锁次数,解锁次数和加锁次数相同时才会解锁。
  • PTHREAD_MUTEX_DEFAULT:默认属性,与PTHREAD_MUTEX_NORMAL类似。

④获取互斥锁的类型:int pthread_mutexattr_gettype(const pthread_mutexattr_t *attr, int *type);

⑤修改互斥锁的类型:int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);

程序实例:创建两个子线程对全局变量number进行+1操作。若不使用互斥锁,就会造成数据混乱。使用了互斥锁,运行正常。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t mutex;  //全局的互斥锁
int number = 0;         //全局变量

//线程t1执行函数
void* funA(void *arg)
{
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        usleep(10);
        number = cur;
        printf("线程1的ID: %ld, number: %d\n", pthread_self(), number);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

//线程t2执行函数
void* funB(void *arg)
{
    for (int i = 0; i < 50; i++) {
        pthread_mutex_lock(&mutex);
        int cur = number;
        cur++;
        number = cur;
        printf("线程2的ID: %ld, number: %d\n", pthread_self(), number);
        usleep(5);
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

//主函数
int main(int argc, char **argv)
{
    /* 初始化互斥锁 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建两个子线程 */
    pthread_t t1, t2;
    pthread_create(&t1, NULL, funA, NULL);
    pthread_create(&t2, NULL, funB, NULL);

    /* 阻塞回收两个子线程*/
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);
    return 0;
}

3. 死锁

  • 互斥锁若使用不当,就会造成死锁。一旦多线程造成死锁,就会使得所有线程处于阻塞状态,且无法解除。
  • 常见的死锁场景:①加锁后忘记解锁;②重复加锁;③存在多个共享资源,随意加锁。
  • 避免死锁:①多检查代码;②对共享资源访问完毕后解锁,或者在加锁时使用trylock非阻塞;③引入一些专门用于死锁检测的模块;④如果程序中有多把锁, 可以控制对锁的访问顺序。另外,在加其它锁之前先释放拥有的互斥锁。

4. 自旋锁

【1】介绍:

  • 自旋锁与互斥锁类似,但是自旋锁在试图上锁时是不断“自旋”查看锁是否被释放,CPU资源占用多。因此,使用自旋锁的效率不高,在用户程序中很少使用自旋锁,更多的是在内核中使用针对执行时间少的代码
  • 互斥锁是基于自旋锁实现的,所以自旋锁更加底层。

【2】自旋锁类型:pthread_spinlock_t。创建一个该类型的变量就得到一把自旋锁。

以下函数的返回值:成功返回0,失败返回错误号。

【3】初始化/销毁自旋锁:

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);//初始化
//lock: 自旋锁的地址
//pshared: 表示自旋锁的进程共享属性
    //PTHREAD_PROCESS_SHARED: 共享自旋锁。该自旋锁可以在多个进程中的线程之间共享
    //PTHREAD_PROCESS_PRIVATE: 私有自旋锁。只有本进程内的线程才能够使用该自旋锁

int pthread_spin_destroy(pthread_spinlock_t *lock);//销毁

【4】自旋锁的加锁和解锁:

int pthread_spin_lock(pthread_spinlock_t *lock);     //加锁(一直自旋)
int pthread_spin_trylock(pthread_spinlock_t *lock);  //加锁(返回错误,错误码EBUSY)
int pthread_spin_unlock(pthread_spinlock_t *lock);   //解锁

程序实例:创建两个子线程来对全局变量+1(每个线程循环10000次)。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>

int count = 0;//全局变量(临界区资源)
pthread_spinlock_t locker;//自旋锁

/* 线程执行函数 */
void* doing(void* arg)
{
    for (int i = 0; i < 10000; ++i) {
        //自旋锁加锁
        pthread_spin_lock(&locker);
        //操作临界区资源
        int tmp = count;
        tmp++;
        count = tmp;
        //自旋锁解锁
        pthread_spin_unlock(&locker);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char** argv)
{
    //初始化自旋锁
    pthread_spin_init(&locker, PTHREAD_PROCESS_PRIVATE);

    //创建两个子线程
    pthread_t tid[2];
    for (int i = 0; i < 2; ++i) {
        if (pthread_create(&tid[i], NULL, doing, NULL)) {
            perror("pthread_create error");
            exit(-1);
        }
    }

    //阻塞回收子线程
    for (int i = 0; i < 2; ++i) {
        if (pthread_join(tid[i], NULL)) {
            perror("pthread_join error");
            exit(-1);
        }
    }

    //打印全局变量
    printf("count = %d\n", count);

    //销毁自旋锁
    pthread_spin_destroy(&locker);
    return 0; 
}

5. 读写锁

读写锁可以视为互斥锁的升级版,可以指定锁定的是读操作还是写操作,且同一时间内只能锁定其中一个操作。读写锁的使用方式与互斥锁相同。读写锁非常适合于对共享数据读的次数远大于写的次数的情况。

【1】读写锁类型:pthread_rwlock_t。创建一个该类型的变量就得到一把读写锁。读写锁中保存了以下信息:①锁的状态(打开还是锁定);②锁的是哪个操作(读/写);③哪个线程锁定了这把锁。

【2】读写锁特点:

  • 使用读写锁的读锁锁定了临界区,那么读操作是并行的。
  • 使用读写锁的写锁锁定了临界区,那么写操作是串行的。
  • 使用读写锁的读锁和写锁分别锁定了两个临界区,那么访问写锁的线程优先进入。因为写锁的优先级高于读锁。

以下函数的返回值:成功返回0,失败返回错误号。

【3】初始化读写锁:

//restrict:是一个修饰指针的关键字,该关键字修饰的指针可以访问指向的内存地址,其他指针不行
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           const pthread_rwlockattr_t *restrict attr);
//rwlock: 读写锁地址
//attr: 读写锁属性,一般为NULL默认属性

【3】释放读写锁资源:

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

【4】锁定读操作(阻塞):

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会阻塞。

【5】锁定读操作(非阻塞):

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,那么锁定读操作成功。
//若读写锁中读锁锁定,那么锁定读操作成功。因为读锁是共享的。
//若读写锁中写锁锁定,那么会返回错误号,而不会阻塞。

【6】锁定写操作(阻塞):

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么就会阻塞。

【7】锁定写操作(非阻塞):

int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
//若读写锁均打开,锁定写操作成功。
//若读写锁中读锁或写锁锁定了,那么返回错误号,而不会阻塞。

【8】解锁:

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
//不管锁定的是读锁还是写锁,都能解锁。

程序实例:创建3个子线程进行写操作,创建5个子线程进行读操作。它们都针对一个全局变量。


#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

int number = 0;             //全局变量(临界资源)
pthread_rwlock_t rwlocker;  //全局的读写锁

/* 写线程执行函数 */
void* writeNum(void *arg)
{
    while (1) {
        pthread_rwlock_wrlock(&rwlocker);
        int cur = number;
        cur++;
        number = cur;
        printf("写操作完毕!number = %d, tid: %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlocker);
        usleep(rand() % 100);//让子线程交替写
    }
    return NULL;
}

/* 读线程执行函数 */
void* readNum(void *arg)
{
    while (1) {
        pthread_rwlock_rdlock(&rwlocker);
        printf("读操作完毕!number = %d, tid: %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlocker);
        usleep(rand() % 100);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化读写锁
    pthread_rwlock_init(&rwlocker, NULL);

    //创建8个线程,3个为写线程,5个为读线程
    pthread_t wtid[3];
    pthread_t rtid[5];
    for (int i = 0; i < 3; i++) {
        pthread_create(&wtid[i], NULL, writeNum, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&rtid[i], NULL, readNum, NULL);
    }

    //主线程回收8个线程
    for (int i = 0; i < 3; i++) {
        pthread_join(wtid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(rtid[i], NULL);
    }

    //释放读写锁
    pthread_rwlock_destroy(&rwlocker);
    return 0;
}

6. 条件变量

条件变量的作用是进行线程的阻塞,而不是线程同步。当满足某个特定条件时才会阻塞线程。一般用于生产者-消费者模型,且和互斥锁相互配合。条件检测需要在互斥锁的保护内进行。

【1】条件变量类型:pthread_cond_t。被条件变量阻塞的线程的信息会被记录到该类型的变量中,以便在解除阻塞时使用。

【2】初始化条件变量:

int pthread_cond_init(pthread_cond_t *restrict cond,
      const pthread_condattr_t *restrict attr);
//cond:条件变量的地址
//attr:条件变量的属性,一般为NULL。

【3】释放条件变量资源:调用pthread_cond_destroy销毁条件变量之后,可以调用pthread_cond_init再次初始化。

int pthread_cond_destroy(pthread_cond_t *cond);

【4】阻塞线程:

// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

//mutex是互斥锁,用于线程同步。
//当阻塞线程时,若线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样做是为了避免死锁
//当线程解除阻塞时,函数内部会帮助这个线程再次将这个mutex锁上,继续向下访问临界区

【5】阻塞线程(时间到解除):

// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

// 以下结构体表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};

//使用示例
time_t mytim = time(NULL);	// 1970.1.1 0:0:0 到当前的总秒数
struct timespec tmsp;
tmsp.tv_nsec = 0;
tmsp.tv_sec = time(NULL) + 100;	// 线程阻塞100s

【6】解除阻塞(至少一个线程):只有一个处于等待状态的线程,使用 pthread_cond_signal更好

int pthread_cond_signal(pthread_cond_t *cond);

【7】解除阻塞(全部线程):

int pthread_cond_broadcast(pthread_cond_t *cond);

生产者-消费者模型:

①若干个生产者线程:生产商品放入任务队列中,若任务队列满则阻塞,可以使用一个生产者条件变量来控制是否阻塞。

②若干个消费者线程:消费者从任务队列中拿走商品,若任务队列空则阻塞,可以使用一个消费者条件变量来控制是否阻塞。

③任务队列:可以是数组、链表、stl容器等等。

程序实例:使用条件变量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

/* 单链表定义 */
typedef struct LNode {
    int number;
    struct LNode *next;
} LNode;

/* 全局变量及锁 */
pthread_mutex_t mutex;  //互斥锁
pthread_cond_t cond;    //条件变量
LNode *head = NULL;     //临界资源,单链表

/* 生产者执行函数 */
void* produce(void *arg)
{
    while (1) {
        //生产商品,往链表头部添加节点
        pthread_mutex_lock(&mutex);
        LNode *tmp = (LNode*)malloc(sizeof(LNode));//创建新节点
        tmp->number = rand() % 100;
        tmp->next = head;
        head = tmp;
        printf("生产完毕! 新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        pthread_mutex_unlock(&mutex);
        //通知消费者拿走商品
        pthread_cond_broadcast(&cond);
        sleep(rand() % 3);//生产慢一点
    }
    return NULL;
}

/* 消费者执行函数 */
void* consume(void *arg)
{
    while (1) {
        pthread_mutex_lock(&mutex);
        //无商品则等待
        //这里不能用if,可能出现段错误。
        //通过while循环判断,当阻塞解除后也会再次判断。
        while (head == NULL) {
            pthread_cond_wait(&cond, &mutex);
        }
        //拿走商品,删除链表头部节点
        LNode *tmp = head;
        printf("消费完毕! 节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        head = head->next;
        free(tmp);
        pthread_mutex_unlock(&mutex);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);

    //创建5个生产者线程和5个消费者线程
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; i++) {
        pthread_create(&ptid[i], NULL, produce, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&ctid[i], NULL, consume, NULL);
    }

    //主线程回收10个线程
    for (int i = 0; i < 5; i++) {
        pthread_join(ptid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(ctid[i], NULL);
    }

    //释放互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

7. 信号量

信号量(信号灯)用在多线程的多任务同步中,一个线程完成了某个任务就通过信号量告诉其它线程,其它线程再进行相关操作。信号量也是用于阻塞线程,要保证线程安全,需要信号量与互斥锁一起使用。

【1】信号量类型:sem_t。需要添加头文件<semaphore.h>.

【2】初始化信号量:

int sem_init(sem_t *sem, int pshared, unsigned int value);
//sem: 信号量地址
//pshared: 0表示线程同步,非0表示进程同步
//value: 初始化当前信号量拥有的资源数>=0, 若资源数为0则阻塞

【3】释放信号量资源:

int sem_destroy(sem_t *sem);
//sem: 信号量地址

【4】消耗资源函数(阻塞):

//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会阻塞
int sem_wait(sem_t *sem);

【5】消耗资源函数(非阻塞):

//sem: 信号量地址
//函数被调用,sem中的资源就会被消耗1个
//当资源数为0时,线程就会返回错误号,而不会阻塞
int sem_trywait(sem_t *sem);

【6】时间到解除阻塞:

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
//sem: 信号量地址
//abs_timeout: 与pthread_cond_timedwait中的参数一样

【7】增加资源函数:

//给sem中的资源数+1
//若资源数从0加到1,那么阻塞的线程就会解除阻塞
int sem_post(sem_t *sem);

【8】查看资源数:

//查看信号量sem中的整形数值, 这个值会被写到sval指针对应的内存中
int sem_getvalue(sem_t *sem, int *sval);

程序实例:使用信号量实现生产者-消费者模型,生产者线程有5个,往链表头部添加节点;消费者线程也有5个,删除链表头部节点。同时,限定了平台容纳商品的最大容量为6个。我们可以设置两个信号量,分别来代表生产者线程生产的商品数量、平台中现有的商品数量(用于给消费者线程拿走)。

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <stdlib.h>
#include <unistd.h>

/* 单链表节点声明 */
typedef struct LNode{
    int number;
    struct LNode *next;
} LNode;

/* 全局变量 */
sem_t psem; //生产者生产商品的信号量
sem_t csem; //消费者拿走时的信号量
pthread_mutex_t mutex;  //互斥锁
LNode *head = NULL;     //单链表(临界区资源)

/* 生产者线程执行函数 */
void* produce(void *arg)
{
    while (1) {
        //生产者信号量-1
        sem_wait(&psem);
        //生产者生产商品
        pthread_mutex_lock(&mutex);
        LNode *tmp = (LNode*)malloc(sizeof(LNode));
        tmp->number = rand() % 100;
        tmp->next = head;
        head = tmp;
        printf("生产完毕!新节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        pthread_mutex_unlock(&mutex);
        //消费者信号量+1
        sem_post(&csem);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 消费者线程执行函数 */
void* consume(void *arg)
{
    while (1) {
        //消费者信号量-1
        sem_wait(&csem);
        //消费者拿走商品
        pthread_mutex_lock(&mutex);
        LNode *tmp = head;
        head = head->next;
        printf("消费完毕!节点number: %d, 线程ID: %ld\n", tmp->number, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);
        //生产者信号量+1
        sem_post(&psem);
        sleep(rand() % 3);
    }
    return NULL;
}

/* 主函数 */
int main(int argc, char **argv)
{
    //初始化信号量和互斥锁
    sem_init(&psem, 0, 6);//平台最多容纳的商品数
    sem_init(&csem, 0, 0);//平台最初没有商品
    pthread_mutex_init(&mutex, NULL);

    //创建5个生产者线程,5个消费者线程
    pthread_t ptid[5];
    pthread_t ctid[5];
    for (int i = 0; i < 5; i++) {
        pthread_create(&ptid[i], NULL, produce, NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_create(&ctid[i], NULL, consume, NULL);
    }

    //释放线程资源
    for (int i = 0; i < 5; i++) {
        pthread_join(ptid[i], NULL);
    }
    for (int i = 0; i < 5; i++) {
        pthread_join(ctid[i], NULL);
    }

    //释放其它资源
    sem_destroy(&psem);
    sem_destroy(&csem);
    pthread_mutex_destroy(&mutex);
    return 0;
}

三、线程池

1. 作用

线程池内部维护了多个工作线程,每个工作线程都会去任务队列中拿取任务并执行,当执行完一个任务后不是马上销毁,而是继续保留执行其它任务。显然,线程池提高了多线程的复用率,减少了创建和销毁线程的时间。

2. 实现原理

线程池内部由任务队列、工作线程和管理者线程组成。

任务队列:存储需要处理的任务。每个任务其实就是具体的函数,在任务队列中存储函数指针和对应的实参。当工作线程获取任务后,就能根据函数指针来调用指定的函数。其实现可以是数组、链表、STL容器等。

工作线程:有N个工作线程,每个工作线程会去任务队列中拿取任务,然后执行具体的任务。当任务被处理后,任务队列中就不再有该任务了。当任务队列中没有任务时,工作线程就会阻塞。

管理者线程:周期性检测忙碌的工作线程数量和任务数量。当任务较多线程不够用时,管理者线程就会多创建几个工作线程来加快处理(不会超过工作线程数量的上限)。当任务较少线程空闲多时,管理者线程就会销毁几个工作线程来减少内存占用(不会低于工作线程数量的下限)。

注意:线程池中没有维护“生产者线程”,所谓的“生产者线程”就是往任务队列中添加任务的线程。

3. 手撕线程池

参考来源:爱编程的大丙。

【1】threadpool.c:

#include "threadpool.h"
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define NUMBER	2//增加或销毁的工作线程数量

//任务结构体
typedef struct Task
{
	void (*fun)(void* arg);
	void* arg;
} Task;


//线程池结构体
struct ThreadPool
{
	//任务队列(视为循环队列)
	Task* taskQ;
	int queueCapacity;	//最大容量
	int queueSize;		//当前任务数量
	int queueFront;		//队头 -> 取任务
	int queueRear;		//队尾 -> 加任务
	//线程相关
	pthread_t managerID;	//管理者线程
	pthread_t* workerIDs;	//工作线程
	int minNum;				//工作线程最小数量
	int maxNum;				//工作线程最大数量
	int liveNum;			//工作线程存活数量
	int busyNum;			//工作线程繁忙数量
	int exitNum;			//工作线程退出数量
	//互斥锁和互斥量
	pthread_mutex_t mutexPool;//锁整个线程池
	pthread_mutex_t mutexBusy;//锁busyNum(提升效率)
	pthread_cond_t condWorker;//阻塞/唤醒工作线程
	pthread_cond_t condProducer;//阻塞/唤醒生产者线程
	//线程池销毁标志
	int shutFlag;	//1表示销毁,0表示非销毁
};

/***************************************************************
 * 函  数: threadPoolCreate
 * 功  能: 创建线程池并初始化
 * 参  数: min---工作线程的最小数量
 *         max---工作线程的最大数量
 *		   capacity---任务队列的最大容量
 * 返回值: 创建的线程池的地址
 **************************************************************/
ThreadPool* threadPoolCreate(int min, int max, int capacity)
{
	//创建线程池
	ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
	if (!pool)
		return NULL;
	//初始化任务队列
	pool->taskQ = (Task*)malloc(sizeof(Task) * capacity);
	if (!pool->taskQ) {
		free(pool);
		return NULL;
	}
	pool->queueCapacity = capacity;
	pool->queueSize = 0;
	pool->queueFront = 0;
	pool->queueRear = 0;
	//初始化互斥锁和条件变量
	if (pthread_mutex_init(&pool->mutexBusy, NULL) != 0 &&
		pthread_mutex_init(&pool->mutexPool, NULL) != 0 &&
		pthread_cond_init(&pool->condProducer, NULL) != 0 &&
		pthread_cond_init(&pool->condWorker, NULL) != 0)
	{
		perror("mutex or cond init error");
		free(pool->taskQ);
		free(pool);
		return NULL;
	}
	//初始化销毁标志
	pool->shutFlag = 0;
	//初始化线程
	pool->minNum = min;
	pool->maxNum = max;
	pool->liveNum = min;//设置默认存活的工作线程数为最小
	pool->busyNum = 0;
	pool->exitNum = 0;
	pool->workerIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
	if (!pool->workerIDs) {
		free(pool->taskQ);
		free(pool);
		return NULL;
	}
	memset(pool->workerIDs, 0, sizeof(pthread_t) * max);//初始化所有工作线程ID为0
	pthread_create(&pool->managerID, NULL, manager, pool);
	for (int i = 0; i < min; ++i) {
		pthread_create(&pool->workerIDs[i], NULL, worker, pool);
	}
	return pool;
}

/***************************************************************
 * 函  数: threadPoolDestroy
 * 功  能: 销毁线程池
 * 参  数: pool---要销毁的线程池
 * 返回值: 0表示销毁成功,-1表示销毁失败
 **************************************************************/
int threadPoolDestroy(ThreadPool* pool)
{
	if (!pool)
		return -1;
	//修改线程池销毁标志
	pool->shutFlag = 1;
	//阻塞回收管理者线程
	if (pthread_join(pool->managerID, NULL)) {
		printf("manager thread destroy success!\n");
	}
	//唤醒所有工作线程,使其销毁
	for (int i = 0; i < pool->liveNum; i++) {
		pthread_cond_signal(&pool->condWorker);
	}
	sleep(3);//保证工作线程全部销毁
	//销毁互斥锁和条件变量
	pthread_mutex_destroy(&pool->mutexBusy);
	pthread_mutex_destroy(&pool->mutexPool);
	pthread_cond_destroy(&pool->condProducer);
	pthread_cond_destroy(&pool->condWorker);
	//回收堆空间资源
	if (pool->taskQ) {
		free(pool->taskQ);
		pool->taskQ = NULL;
	}
	if (pool->workerIDs) {
		free(pool->workerIDs);
		pool->workerIDs = NULL;
	}
	free(pool);
	pool = NULL;
	return 0;
}

/***************************************************************
 * 函  数: threadPoolAdd
 * 功  能: 生产者往线程池的任务队列中添加任务
 * 参  数: pool---线程池
 *		   task---函数指针,要执行的任务地址
 *		   arg---func指向的函数的实参地址
 * 返回值: 无
 **************************************************************/
void threadPoolAdd(ThreadPool* pool, void (*task)(void* arg), void* arg)
{
	if (!pool) return;
	pthread_mutex_lock(&pool->mutexPool);
	while (pool->queueSize == pool->queueCapacity && !pool->shutFlag) {
		pthread_cond_wait(&pool->condProducer, &pool->mutexPool);
	}
	if (pool->shutFlag) {
		pthread_mutex_unlock(&pool->mutexPool);
		return;
	}
	pool->taskQ[pool->queueRear].fun = task;
	pool->taskQ[pool->queueRear].arg = arg;
	pool->queueSize++;
	pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
	pthread_cond_signal(&pool->condWorker);
	pthread_mutex_unlock(&pool->mutexPool);
}

/***************************************************************
 * 函  数: worker
 * 功  能: 工作线程的执行函数
 * 参  数: arg---实参传入,这里传入的是线程池
 * 返回值: 空指针(实则不会返回,而是调用threadExit退出)
 **************************************************************/
void* worker(void* arg)
{
	ThreadPool* pool = (ThreadPool*)arg;
	while (1) {
		/* 1.获取任务队列中的任务 */
		pthread_mutex_lock(&pool->mutexPool);
		//任务队列无任务则阻塞等待
		while (pool->queueSize == 0 && !pool->shutFlag) {
			pthread_cond_wait(&pool->condWorker, &pool->mutexPool);
			//被唤醒情况:管理者线程让工作线程销毁
			if (pool->exitNum > 0) {
				pool->exitNum--;
				if (pool->liveNum > pool->minNum) {//存活>最小时才销毁线程
					pthread_mutex_unlock(&pool->mutexPool);
					printf("manager inform destroy: ");
					threadExit(pool);
				}
			}
		}
		//被唤醒情况:线程池关闭
		if (pool->shutFlag) {
			pthread_mutex_unlock(&pool->mutexPool);
			threadExit(pool);
		}
		//被唤醒情况:任务队列有任务
		Task task;
		task.fun = pool->taskQ[pool->queueFront].fun;
		task.arg = pool->taskQ[pool->queueFront].arg;
		pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
		pool->queueSize--;
		pthread_cond_signal(&pool->condProducer);//通知生产者添加任务
		pthread_mutex_unlock(&pool->mutexPool);

		/* 2.执行获取的任务 */
		pthread_mutex_lock(&pool->mutexBusy);
		pool->busyNum++;
		pthread_mutex_unlock(&pool->mutexBusy);

		printf("thread %ld start working...\n", pthread_self());
		task.fun(task.arg);
		free(task.arg);//由于arg是堆中的资源,执行完释放
		task.arg = NULL;
		printf("thread %ld end working...\n", pthread_self());

		pthread_mutex_lock(&pool->mutexBusy);
		pool->busyNum--;
		pthread_mutex_unlock(&pool->mutexBusy);
	}
	return NULL;
}

/***************************************************************
 * 函  数: manager
 * 功  能: 管理者线程的执行函数
 * 参  数: arg---实参传入,这里传入的是线程池
 * 返回值: 空指针,当线程池关闭后就执行完毕
 **************************************************************/
void* manager(void* arg)
{
	ThreadPool* pool = (ThreadPool*)arg;
	while (!pool->shutFlag) {
		//周期性检测: 每3秒
		sleep(3);
		//功能一:增加工作线程
		pthread_mutex_lock(&pool->mutexPool);
		if (pool->liveNum < pool->queueSize && pool->liveNum < pool->maxNum) {
			int count = 0;
			for (int i = 0; i < pool->maxNum && count < NUMBER
				&& pool->liveNum < pool->maxNum; ++i)
			{
				if (pool->workerIDs[i] == 0) {
					count++;
					pthread_create(&pool->workerIDs[i], NULL, worker, pool);
					pool->liveNum++;
					printf("create thread\n");
				}
			}
		}
		pthread_mutex_unlock(&pool->mutexPool);
		//功能二:销毁工作线程
		pthread_mutex_lock(&pool->mutexPool);
		if (pool->busyNum * 2 < pool->liveNum && pool->liveNum > pool->minNum) {
			pool->exitNum = NUMBER;
			for (int i = 0; i < NUMBER; i++) {
				pthread_cond_signal(&pool->condWorker);
			}
		}
		pthread_mutex_unlock(&pool->mutexPool);
	}
	return NULL;
}

/***************************************************************
 * 函  数: threadExit
 * 功  能: 工作线程退出函,将工作线程的ID置为0,然后liveNum--,再退出
 * 参  数: pool---线程池
 * 返回值: 无
 **************************************************************/
void threadExit(ThreadPool* pool)
{
	//修改pool->workerIDs里对应的ID为0
	pthread_t tid = pthread_self();
	pthread_mutex_lock(&pool->mutexPool);
	for (int i = 0; i < pool->maxNum; ++i) {
		if (tid == pool->workerIDs[i]) {
			pool->workerIDs[i] = 0;
			pool->liveNum--;
			printf("threadExit() called, %ld exit... liveNum = %d\n", tid, pool->liveNum);
			break;
		}
	}
	pthread_mutex_unlock(&pool->mutexPool);
	//工作线程退出
	pthread_exit(NULL);
}

【2】threadpool.h:

#ifndef _THREADPOOL_H
#define _THREADPOOL_H

typedef struct ThreadPool ThreadPool;

//创建并初始化线程池
ThreadPool* threadPoolCreate(int min, int max, int capacity);

//销毁线程池
int threadPoolDestroy(ThreadPool* pool);

//向线程池中添加任务
void threadPoolAdd(ThreadPool* pool, void (*task)(void* arg), void* arg);

/*************其它函数*******************/
void* worker(void* arg);//工作线程的执行函数
void* manager(void* arg);//管理者线程的执行函数
void threadExit(ThreadPool* pool);//工作线程退出函数

#endif

【3】main.c:

#include <stdio.h>
#include "threadpool.h"
#include <pthread.h>
#include <unistd.h>


//任务函数,所有线程都执行此任务
void testTask(void* arg)
{
    int* num = (int*)arg;
    printf("threadID: %ld, num: %d\n", pthread_self(), *num);
    sleep(1);
}

int main(int argc, char** argv)
{
    //创建线程池: 最少3个工作线程,最多10个,任务队列容量为50
    ThreadPool* pool = threadPoolCreate(3, 10, 50);
    //添加任务到任务队列中
    for (int i = 1; i <= 50; ++i) {
        int* num = (int*)malloc(sizeof(int));
        *num = i;
        threadPoolAdd(pool, testTask, num);
    }
    //销毁线程池
    sleep(25);//保证所有的工作线程执行完毕
    threadPoolDestroy(pool);
    return 0;
}

【4】运行结果:

......

  • 74
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值