线程知识整理

异步与线程

​ 异步是说,A发起一个操作后(一般都是比较耗时的操作,如果不耗时的操作就没有必要异步了),可以继续自顾自的处理它自己的事儿,不用干等着这个耗时操作返回。

​ **异步调用并不是要减少线程的开销, 它的主要目的是让调用方法的主线程不需要同步等待在这个函数调用上,从而可以让主线程继续执行它下面的代码。**与此同时, 系统会通过从ThreadPool中取一个线程来执行,帮助我们将我们要写/读的数据发送到网卡。 由于不需要我们等待, 我们等于同时做了两件事情. 这个效果跟自己另外启动一个线程来执行等待方式的写操作是一样的.但是异步线程可以利用操作系统/.Net的线程池, 系统可以根据吞吐量动态的管理线程池的大小.

​ **异步是目的,多线程只是我们实现异步的一个手段.**什么是异步:异步是当一个调用请求发送给被调用者,而调用者不用等待其结果的返回.实现异步可以采用多线程技术或则交给另外的进程来处理

异步操作的本质

​ 硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS 这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。

异步操作的优缺点

因为异步操作无须额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不必使用共享变量(即使无法完全不用,最起码可以减少 共享变量的数量),减少了死锁的可能。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理。

线程的本质

线程不是一个计算机硬件的功能,而是操作系统提供的一种逻辑功能,线程本质上是进程中一段并发运行的代码,所以线程需要操作系统投入CPU资源来运行和调度。

​ 线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。

​ 当需要执行I/O操作时,使用异步操作比使用线程+同步 I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.net Remoting等跨进程的调用。

而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作

异步有的时候用普通的线程,有的时候用系统的异步调用功能。有一些IO操作也是异步的,但是未必需要一个线程来运行。例如:硬件是有DMA功能的,在调用DMA传输数据的时候,CPU是不需要执行处理的,只需要发起传输和等待传输结束即可。具体到.net平台,比如Socket的BeginSend,如果是运行在Windows 2000以后的平台,在底层就会调用异步的完成端口来发送。

回调

线程

1. 多线程特点

线程是轻量级的进程(LWP:light weight process),在 Linux 环境下线程的本质仍是进程。在计算机上运行的程

序是一组指令及指令参数的组合,指令按照既定的逻辑控制计算机运行。操作系统会以进程为单位,分配系统资源,

可以这样理解,进程是资源分配的最小单位线程是操作系统调度执行的最小单位

先从概念上了解一下线程和进程之间的区别:

  • 进程有自己独立的地址空间,多个线程共用同一个地址空间

    • 线程更加节省系统资源,效率不仅可以保持的,而且能够更高
    • 在一个地址空间中多个线程独享:每个线程都有属于自己的栈区,寄存器 (内核中管理的)
    • 在一个地址空间中多个线程共享:代码段,堆区,全局数据区,打开的文件 (文件描述符表) 都是线程共享的
  • 线程是程序的最小执行单位,进程是操作系统中最小的资源分配单位

    • 每个进程对应一个虚拟地址空间,一个进程只能抢一个 CPU 时间片
    • 一个地址空间中可以划分出多个线程,在有效的资源基础上,能够抢更多的 CPU 时间片

    在这里插入图片描述

    • CPU 的调度和切换:线程的上下文切换比进程要快的多

上下文切换:进程 / 线程分时复用 CPU 时间片,在切换之前会将上一个任务的状态进行保存,下次切换回这个任务的时候,加载这个状态继续运行,任务从保存到再次加载这个过程就是一次上下文切换

线程更加廉价,启动速度更快,退出也快,对系统资源的冲击小。

在处理多任务程序的时候使用多线程比使用多进程要更有优势,但是线程并不是越多越好,如何控制线程的个数呢?

  1. 文件 IO 操作:文件 IO 对 CPU 是使用率不高,因此可以分时复用 CPU 时间片,线程的个数 = 2 * CPU 核心数 (效率最高)
  2. 处理复杂的算法 (主要是 CPU 进行运算,压力大),线程的个数 = CPU 的核心数 (效率最高)

2. 创建线程

//每一个线程都有一个唯一的线程 ID,ID 类型为 pthread_t,这个 ID 是一个无符号长整形数
pthread_t pthread_self(void);	// 返回当前线程的线程ID

在一个进程中调用线程创建函数,就可得到一个子线程,和进程不同,需要给每一个创建出的线程指定一个处理函数,否则这个线程无法工作。

#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

//返回值:线程创建成功返回 0,创建失败返回对应的错误号
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

void *working(void *arg)
{
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    int i;
    for (i = 0; i < 9; ++i)
    {
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功,线程ID:%d\n", tid);

    printf("我是主线程,线程ID:%d\n", pthread_self());
    int i = 0;
    for (i = 0; i < 3; ++i)
    {
        printf("i=%d\n", i);
    }
    //sleep(1);
    return 0;
}

编译器链接不到,需要在编译的时候通过参数指定出来线程库文件(动态库),动态库名为 libpthread.so 需要使用的参数为 -l,根据规则掐头去尾最终形态应该写成:-lpthread

gcc pthread_create.c -lpthread -o create -std=c11
./create

在这里插入图片描述
注释掉sleep可能会出现
在这里插入图片描述

在打印的日志输出中为什么子线程处理函数没有执行完毕呢(只看到了子线程的部分日志输出)?
主线程一直在运行,执行期间创建出了子线程,说明主线程有 CPU 时间片,在这个时间片内将代码执行完毕了,主线程就退出了。

​ 子线程被创建出来之后需要抢cpu时间片, 抢不到就不能运行,如果主线程退出了, 虚拟地址空间就被释放了, 子线程就一并被销毁了。

​ 但是如果某一个子线程退出了, 主线程仍在运行, 虚拟地址空间依旧存在

得到的结论:在没有人为干预的情况下,虚拟地址空间的生命周期和主线程是一样的,与子线程无关

目前的解决方案:让子线程执行完毕,主线程再退出,可以在主线程中添加挂起函数 sleep();

3. 线程退出

​ 如果想要让线程退出,但是不会导致虚拟地址空间的释放(针对于主线程),我们就可以调用线程库中的线程退出函数,只要调用该函数当前线程就马上退出了,并且不会影响到其他线程的正常运行,不管是在子线程或者主线程中都可以使用

#include <pthread.h>
//参数:线程退出的时候携带的数据,当前子线程的主线程会得到该数据。如果不需要使用,指定为 NULL
void pthread_exit(void *retval);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
    sleep(1);
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        if(i==6)
        {
            pthread_exit(NULL);	// 直接退出子线程
        } 
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    printf("子线程创建成功, 线程ID: %ld\n", tid);
    // 2. 子线程不会执行下边的代码, 主线程执行
    printf("我是主线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<3; ++i)
    {
        printf("i = %d\n", i);
    }

    // 主线程调用退出函数退出, 地址空间不会被释放
    pthread_exit(NULL);
    
    return 0;
}

4. 线程回收

​ 线程和进程一样,子线程退出的时候其内核资源主要由主线程回收,线程库中提供的线程回收函叫pthread_join(),

这个函数是一个阻塞函数,如果还有子线程在运行,调用该函数就会阻塞,子线程退出函数解除阻塞进行资源的回

收,函数被调用一次,只能回收一个子线程,如果有多个子线程则需要循环进行回收。

#include <pthread.h>
// 这是一个阻塞函数, 子线程在运行这个函数就阻塞
// 子线程退出, 函数解除阻塞, 回收对应的子线程资源, 类似于回收进程使用的函数 wait()
int pthread_join(pthread_t thread, void **retval);
  • 参数:

    • thread: 要被回收的子线程的线程 ID
    • retval: 二级指针,指向一级指针的地址,是一个传出参数,这个地址中存储了 pthread_exit () 传递出的数据,如果不需要这个参数,可以指定为 NULL
  • 返回值:线程回收成功返回 0,回收失败返回错误号。

回收子线程数据

  1. 使用子线程栈

    // pthread_join.c
    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <string.h>
    #include <pthread.h>
    
    // 定义结构
    struct Persion
    {
        int id;
        char name[36];
        int age;
    };
    
    // 子线程的处理代码
    void* working(void* arg)
    {
        printf("我是子线程, 线程ID: %ld\n", pthread_self());
        for(int i=0; i<9; ++i)
        {
            printf("child == i: = %d\n", i);
            if(i == 6)
            {
                struct Persion p;
                p.age  =12;
                strcpy(p.name, "tom");
                p.id = 100;
                // 该函数的参数将这个地址传递给了主线程的pthread_join()
                pthread_exit(&p);
            }
        }
        return NULL;	// 代码执行不到这个位置就退出了
    }
    
    int main()
    {
        // 1. 创建一个子线程
        pthread_t tid;
        pthread_create(&tid, NULL, working, NULL);
    
        // 阻塞等待子线程退出
        void* ptr = NULL;
        // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
        // 这个内存地址就是pthread_exit() 参数指向的内存
        pthread_join(tid, &ptr);
        // 打印信息
        struct Persion* pp = (struct Persion*)ptr;
        printf("子线程返回数据: name: %s, age: %d, id: %d\n", pp->name, pp->age, pp->id);
        printf("子线程资源被成功回收...\n");
        
        return 0;
    }
    

    如果多个线程共用同一个虚拟地址空间,每个线程在栈区都有一块属于自己的内存,相当于栈区被这几个线程平分了,当线程退出,线程在栈区的内存也就被回收了,因此随着子线程的退出,写入到栈区的数据也就被释放了

  2. 使用全局变量

    ​ 位于同一虚拟地址空间中的线程,虽然不能共享栈区数据,但是可以共享全局数据区和堆区数据,因此在子线程退出的时候可以将传出数据存储到全局变量、静态变量或者堆内存中。

  3. 使用主线程栈

    ​ 虽然每个线程都有属于自己的栈区空间,但是位于同一个地址空间的多个线程是可以相互访问对方的栈空间上

    的数据的。由于很多情况下还需要在主线程中回收子线程资源,所以主线程一般都是最后退出,

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

// 定义结构
struct Persion
{
    int id;
    char name[36];
    int age;
};


// 子线程的处理代码
void* working(void* arg)
{
    struct Persion* p = (struct Persion*)arg;
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        printf("child == i: = %d\n", i);
        if(i == 6)
        {
            // 使用主线程的栈内存
            p->age  =12;
            strcpy(p->name, "tom");
            p->id = 100;
            // 该函数的参数将这个地址传递给了主线程的pthread_join()
            pthread_exit(p);
        }
    }
    return NULL;
}

int main()
{
    // 1. 创建一个子线程
    pthread_t tid;

    struct Persion p;
    // 主线程的栈内存传递给子线程
    pthread_create(&tid, NULL, working, &p);

    // 阻塞等待子线程退出
    void* ptr = NULL;
    // ptr是一个传出参数, 在函数内部让这个指针指向一块有效内存
    // 这个内存地址就是pthread_exit() 参数指向的内存
    pthread_join(tid, &ptr);
    // 打印信息
    printf("name: %s, age: %d, id: %d\n", p.name, p.age, p.id);
    printf("子线程资源被成功回收...\n");
    
    return 0;
}

5. 线程分离

​ 在某些情况下,程序中的主线程有属于自己的业务处理流程,如果让主线程负责子线程的资源回收,调用

pthread_join() 只要子线程不退出主线程就会一直被阻塞,主要线程的任务也就不能被执行了。

​ 在线程库函数中为我们提供了线程分离函数 pthread_detach(),调用这个函数之后指定的子线程就可以和主线程分

离,当子线程退出的时候,其占用的内核资源就被系统的其他进程接管并回收了。线程分离之后在主线程中使用

pthread_join() 就回收不到子线程资源了。

#include <pthread.h>
// 参数就子线程的线程ID, 主线程就可以和这个子线程分离了
int pthread_detach(pthread_t thread);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        printf("child == i: = %d\n", i);
    }
    return NULL;
}

int main()
{
    // 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    // 设置子线程和主线程分离
    pthread_detach(tid);

    // 让主线程自己退出即可 不让虚拟内存释放掉,不然子线程无法执行
    pthread_exit(NULL);
    
    return 0;
}

6. 线程取消

线程取消的意思就是在某些特定情况下在一个线程中杀死另一个线程。使用这个函数杀死一个线程需要分两步:

  1. 在线程 A 中调用线程取消函数 pthread_cancel,指定杀死线程 B,这时候线程 B 是死不了的
  2. 在线程 B 中进程一次系统调用(从用户区切换到内核区),否则线程 B 可以一直运行。

主线程调用线程取消函数,只要在子线程中进行了系统调用,当子线程执行到这个位置就挂掉了。

关于系统调用有两种方式:

  1. 直接调用 Linux 系统函数
  2. 调用标准 C 库函数,为了实现某些功能,在 Linux 平台下标准 C 库函数会调用相关的系统函数
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

// 子线程的处理代码
void* working(void* arg)
{
    int j=0;
    for(int i=0; i<9; ++i)
    {
        j++;
    }
    // 这个函数会调用系统函数, 因此这是个间接的系统调用
    printf("我是子线程, 线程ID: %ld\n", pthread_self());
    for(int i=0; i<9; ++i)
    {
        printf(" child i: %d\n", i);
    }

    return NULL;
}

int main()
{
    // 1. 创建一个子线程
    pthread_t tid;
    pthread_create(&tid, NULL, working, NULL);

    // 杀死子线程, 如果子线程中做系统调用, 子线程就结束了
    pthread_cancel(tid);

    // 让主线程自己退出即可
    pthread_exit(NULL);
    
    return 0;
}

线程同步

所谓的同步并不是多个线程同时对内存进行访问,而是按照先后顺序依次进行的

两个线程交替数数(每个线程数 50 个数,交替数到 100)

两个线程在数数的时候需要分时复用 CPU 时间片,并且测试程序中调用了 sleep() 导致线程的 CPU 时间片没用完就被迫挂起了,这样就能让 CPU 的上下文切换(保存当前状态,下一次继续运行的时候需要加载保存的状态)更加频繁,更容易再现数据混乱的这个现象。

img

CPU 对应寄存器、一级缓存、二级缓存、三级缓存是独占的,用于存储处理的数据和线程的状态信息,数据被 CPU 处理完成需要再次被写入到物理内存中,物理内存数据也可以通过文件 IO 操作写入到磁盘中。

​ 在测试程序中两个线程共用全局变量 number 当线程变成运行态之后开始数数,从物理内存加载数据,然后将数据放到 CPU 进行运算,最后将结果更新到物理内存中。如果数数的两个线程都可以顺利完成这个流程,那么得到的结果肯定是正确的。

​ 如果线程 A 执行这个过程期间就失去了 CPU 时间片,线程 A 被挂起了最新的数据没能更新到物理内存。线程 B 变成运行态之后从物理内存读数据,很显然它没有拿到最新数据,只能基于旧的数据往后数,然后失去 CPU 时间片挂起。线程 A 得到 CPU 时间片变成运行态,第一件事儿就是将上次没更新到内存的数据更新到内存,但是这样会导致线程 B 已经更新到内存的数据被覆盖,活儿白干了,最终导致有些数据会被重复数很多次。

​ 假设线程A数数 1 2 3 4 5 但是CPU处理到5的时候,时间片用完了数字5还没来得及写入物理内存中;线程B再次执行的时候会从物理内存中读取4,从4开始数数。

同步的方式

​ 常用的线程同步方式有四种:互斥锁读写锁条件变量信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对应的共享资源也被称之为临界资源。

image-20200106092600543

找到临界资源之后,再找和临界资源相关的上下文代码,这样就得到了一个代码块,这个代码块可以称之为临界区

确定好临界区(临界区越小越好)之后,就可以进行线程同步了,线程同步的大致处理思路是这样的:

  • 在临界区代码的上边,添加加锁函数,对临界区加锁。
    • 哪个线程调用这句代码,就会把这把锁锁上,其他线程就只能阻塞在锁上了。
  • 在临界区代码的下边,添加解锁函数,对临界区解锁。
    • 出临界区的线程会将锁定的那把锁打开,其他抢到锁的线程就可以进入到临界区了。
  • 通过锁机制能保证临界区代码最多只能同时有一个线程访问,这样并行访问就变为串行访问了。

互斥锁

​ 互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有的线程只能

序执行 (不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的

降低,因为默认临界区多个线程是可以并行处理的,现在只能串行处理。

​ 在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程 ID)。

​ 一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被

解锁,被阻塞的线程才能被解除阻塞。一般情况下,每一个共享资源对应一个把互斥锁,锁的个数和线程的个数无

pthread_mutex_t  mutex;


/*
	参数:
		mutex: 互斥锁变量的地址
		attr: 互斥锁的属性,一般使用默认属性即可,这个参数指定为 NULL
*/

// 初始化互斥锁
// restrict: 是一个关键字, 用来修饰指针, 只有这个关键字修饰的指针可以访问指向的内存地址, 其他指针是不行的
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       							const pthread_mutexattr_t *restrict attr);
// 释放互斥锁资源            
int pthread_mutex_destroy(pthread_mutex_t *mutex);

加锁

// 修改互斥锁的状态, 将其设定为锁定状态, 这个状态被写入到参数 mutex 中
int pthread_mutex_lock(pthread_mutex_t *mutex);

这个函数被调用,首先会判断参数 mutex 互斥锁中的状态是不是锁定状态:

  • 没有被锁定,是打开的,这个线程可以加锁成功,这个这个锁中会记录是哪个线程加锁成功了
  • 如果被锁定了,其他线程加锁就失败了,这些线程都会阻塞在这把锁上
  • 当这把锁被解开之后,这些阻塞在锁上的线程就解除阻塞了,并且这些线程是通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞

尝试加锁

// 尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);

调用这个函数对互斥锁变量加锁还是有两种情况:

  • 如果这把锁没有被锁定是打开的,线程加锁成功
  • 如果锁变量被锁住了,调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误号

解锁

// 对互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

不是所有的线程都可以对互斥锁解锁,哪个线程加的锁,哪个线程才能解锁成功。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <pthread.h>

#define _MAX 50
int number = 0;
pthread_mutex_t mutex;

void *funcA(void *arg)
{
    int i = 0;
    for (; i < _MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        ++number;
        printf("num:%d, pthread ID:%d\n", number, pthread_self());
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

void *funcB(void *arg)
{
    int i = 0;
    for (; i < _MAX; ++i)
    {
        pthread_mutex_lock(&mutex);
        ++number;
        printf("num:%d, pthread ID:%d\n", number, pthread_self());
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

/*
    两个线程交替数数 数到一百
*/

int main()
{
    pthread_t p1, p2;

    pthread_mutex_init(&mutex, NULL);

    pthread_create(&p1, NULL, funcA, NULL);
    pthread_create(&p2, NULL, funcB, NULL);

    // pthread_detach(p1);
    // pthread_detach(p2);
    pthread_join(p1, NULL);
    pthread_join(p2, NULL);

    // pthread_exit(NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

读写锁

​ 读写锁是互斥锁的升级版,在做读操作的时候可以提高程序的执行效率,如果所有的线程都是做读操作, 那么读是并行的,但是使用互斥锁,读操作也是串行的。

pthread_rwlock_t rwlock;

是因为这把锁既可以锁定读操作,也可以锁定写操作。为了方便理解,可以大致认为在这把锁中记录了这些信息:

  • 锁的状态:锁定 / 打开
  • 锁定的是什么操作:读操作 / 写操作,使用读写锁锁定了读操作,需要先解锁才能去锁定写操作,反之亦然。
  • 哪个线程将这把锁锁上了

因为通过一把读写锁可以锁定读或者写操作,下面介绍一下关于读写锁的特点:

  • 使用读写锁的读锁锁定了临界区,线程对临界区的访问是并行的,读锁是共享的
  • 使用读写锁的写锁锁定了临界区,线程对临界区的访问是串行的,写锁是独占的。
  • 使用读写锁分别对两个临界区加了读锁和写锁,两个线程要同时访问者两个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高

​ 如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势,和互斥锁是一样的,如果说程序中所有的线程都对共享资源有写也有读操作,并且对共享资源读的操作越多,读写锁更有优势

/*
	参数:
		rwlock: 读写锁的地址,传出参数
		attr: 读写锁属性,一般使用默认属性,指定为 NULL
*/

#include <pthread.h>
pthread_rwlock_t rwlock;
// 初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
           										const pthread_rwlockattr_t *restrict attr);
// 释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

/*
	如果读写锁是打开的,那么加锁成功;
	如果读写锁已经锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的;
	如果读写锁已经锁定了写操作,调用这个函数的线程会被阻塞。
*/

// 在程序中对读写锁加读锁, 锁定的是读操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);


/*
	如果读写锁已经锁定了写操作,调用这个函数加锁失败,对应的线程不会被阻塞,
	可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
*/
// 这个函数可以有效的避免死锁
// 如果加读锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

/*
	如果读写锁是打开的,那么加锁成功;
	如果读写锁已经锁定了读操作或者锁定了写操作,调用这个函数的线程会被阻塞。
*/
// 在程序中对读写锁加写锁, 锁定的是写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);


//线程不会阻塞,可以在程序中对函数返回值进行判断,添加加锁失败之后的处理动作。
// 这个函数可以有效的避免死锁
// 如果加写锁失败, 不会阻塞当前线程, 直接返回错误号
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

题目要求:8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。

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

/*
    8 个线程操作同一个全局变量,3 个线程不定时写同一全局资源,5 个线程不定时读同一全局资源。
*/
int number = 0;
pthread_rwlock_t rwlock;
void *readFunc(void *arg)
{
    while (number <= 100)
    {
        pthread_rwlock_rdlock(&rwlock);
        printf("--read number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
    return NULL;
}
void *writeFunc(void *arg)
{
    while (number <= 100)
    {
        pthread_rwlock_wrlock(&rwlock);
        ++number;
        printf("--write number = %d, tid = %ld\n", number, pthread_self());
        pthread_rwlock_unlock(&rwlock);
        sleep(0.1);
    }

    return NULL;
}
int main()
{
    pthread_rwlock_init(&rwlock, NULL);

    pthread_t rtid[5];
    pthread_t wtid[3];
    int i;
    for (i = 0; i < 5; ++i)
    {
        pthread_create(&rtid[i], NULL, readFunc, NULL);
    }
    for (i = 0; i < 3; ++i)
    {
        pthread_create(&wtid[i], NULL, writeFunc, NULL);
    }

    for (i = 0; i < 5; ++i)
    {
        pthread_join(rtid[i], NULL);
    }
    for (i = 0; i < 3; ++i)
    {
        pthread_join(wtid[i], NULL);
    }

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

条件变量

​ 严格意义上来说,条件变量的主要作用不是处理线程同步,而是进行线程的阻塞。如果在多线程程序中只使用条件

变量无法实现线程的同步,必须要配合互斥锁来使用。

​ 虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样的,二者的区别如下:

  • 假设有 A-Z 26 个线程,这 26 个线程共同访问同一把互斥锁,如果线程 A 加锁成功,那么其余 B-Z 线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区
  • 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界资源,这种情况下还是会出现共享资源中数据的混乱。

一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用。

#include <pthread.h>

pthread_cond_t cond;

/*
	参数:
		cond: 条件变量的地址
		attr: 条件变量属性,一般使用默认属性,指定为 NULL
*/

// 初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
      											const pthread_condattr_t *restrict attr);

// 销毁释放资源        
int pthread_cond_destroy(pthread_cond_t *cond);

阻塞线程

​ 通过函数原型可以看出,该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同

步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。

​ 该函数会对这个互斥锁做以下几件事情:

  1. 在阻塞线程时候,如果线程已经对互斥锁 mutex 上锁,那么会将这把锁打开,这样做是为了避免死锁
  2. 当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个 mutex 互斥锁锁上,继续向下访问临界区
// 线程阻塞函数, 哪个线程调用这个函数, 哪个线程就会被阻塞
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 将线程阻塞一定的时间长度, 时间到达之后, 线程就解除阻塞了
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
           pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

/*
	这个函数的前两个参数和 pthread_cond_wait 函数是一样的,第三个参数表示线程阻塞的时长,
	但是需要额外注意一点:struct timespec 这个结构体中记录的时间是从1971.1.1到某个时间点的时间,总长度使用秒/纳秒表示。因此赋值方式相对要麻烦一点:
*/
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

唤醒阻塞在条件变量上的线程,至少有一个被解除阻塞

// 唤醒阻塞在条件变量上的线程, 至少有一个被解除阻塞
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒阻塞在条件变量上的线程, 被阻塞的线程全部解除阻塞
int pthread_cond_broadcast(pthread_cond_t *cond);

​ 调用上面两个函数中的任意一个,都可以唤醒被 pthread_cond_wait 或者 pthread_cond_timedwait 阻塞的线程,区别就在于 pthread_cond_signal 是唤醒至少一个被阻塞的线程(总个数不定),pthread_cond_broadcast 是唤醒所有被阻塞的线程。

生产者和消费者

生产者和消费者模型的组成:

  • 生产者线程 -> 若干个

    • 商品或者任务放入到任务队列中
    • 任务队列满了就阻塞,不满的时候就工作
    • 通过一个生产者的条件变量控制生产者线程阻塞和非阻塞
  • 消费者线程 -> 若干个

    • 读任务队列,将任务或者数据取出
    • 任务队列中有数据就消费,没有数据就阻塞
    • 通过一个消费者的条件变量控制消费者线程阻塞和非阻塞
  • 队列 -> 存储任务 / 数据,对应一块内存,为了读写访问可以通过一个数据结构维护这块内存

    • 可以是数组、链表,也可以使用 stl 容器:queue /stack/list/vector

1564644834918

​ 使用条件变量实现生产者和消费者模型,

​ 生产者有 5 个,往链表头部添加节点

​ 消费者也有 5 个,删除链表头部的节点

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

struct Node
{
    int number;
    struct Node *next;
};
// typedef struct Node Node;
pthread_cond_t cond;
pthread_mutex_t mutex;

struct Node *head = NULL;

void *producer(void *arg)
{
    while (1)
    {
        printf("-----1------\n");
        pthread_mutex_lock(&mutex);

        //生产数据
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->number = rand() % 1000;
        newNode->next = head;
        head = newNode;
        printf("add node, num : %d, tid : %ld\n", newNode->number, pthread_self());

        //通知消费者可以消费 (解除某处的阻塞)
        pthread_cond_signal(&cond);

        pthread_mutex_unlock(&mutex);
        usleep(100);
    }
    return NULL;
}
void *consumer(void *arg)
{
    while (1)
    {
        printf("------32-----\n");
        pthread_mutex_lock(&mutex);
        struct Node *tmp = head;
        if (head == NULL)
        {
            //没有数据可以消费 阻塞等待
            
            //当这个函数调用阻塞的时候,进入睡眠状态前 会对互斥锁进行 **解锁**
            //当不阻塞的,继续向下执行,会重新加锁。
            //生产线程 发送pthread_cond_signal信号 这里的阻塞停止
            
            //若阻塞解除苏醒,但 pthread_cond_wait()没有立即返回 ,它还要做一件事:重新锁定 mutex
            pthread_cond_wait(&cond, &mutex);
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            //有数据可以消费
            head = head->next;
            printf("del node, num : %d, tid : %ld\n", tmp->number, pthread_self());
            free(tmp);
            pthread_mutex_unlock(&mutex);
            usleep(100);
        }
    }
    return NULL;
}
int main()
{
    // 5个生产者 5个消费者
    pthread_t pid[5], cid[5];
    //初始化变量
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
    int i;
    for (i = 0; i < 5; ++i)
    {
        pthread_create(&pid[i], NULL, producer, NULL);
        pthread_create(&cid[i], NULL, consumer, NULL);
    }

    for (i = 0; i < 5; ++i)
    {
        pthread_join(pid[i], NULL);
        pthread_join(cid[i], NULL);
    }

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);

    return 0;
}

信号量

​ 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。

#include <semaphore.h>
sem_t sem;
/*
	参数:
		sem:信号量变量地址
	pshared:
		0:线程同步
		非 0:进程同步
	value:初始化当前信号量拥有的资源数(>=0),如果资源数为 0,线程就会被阻塞了。
*/
/ 初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 资源释放, 线程销毁之后调用这个函数即可
// 参数 sem 就是 sem_init() 的第一个参数            
int sem_destroy(sem_t *sem);

/*
   当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数-1,
直到 sem 中的资源数减为 0 时,资源被耗尽,因此线程也就被阻塞了。
*/
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_wait(sem_t *sem);

/*
	sem 中的资源数减为 0 时,资源被耗尽,但是线程不会被阻塞,直接返回错误号
	因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况
*/
// 函数被调用sem中的资源就会被消耗1个, 资源数-1
int sem_trywait(sem_t *sem);

// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示
struct timespec {
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};
// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/*
	调用该函数会将 sem 中的资源数 +1,
	如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。
*/
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);

生产者消费者

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

struct Node
{
    int num;
    struct Node *next;
};

// 头结点
struct Node *head = NULL;

// 创建一个互斥量  保证数据安全问题
pthread_mutex_t mutex;

// 创建两个信号量
sem_t psem;
sem_t csem;

void *producer(void *arg)
{
    while (1)
    {
        // 生产者拿一个信号灯 
        sem_wait(&psem);
		
        // 加锁, 这句代码放到 sem_wait()上边, 有可能会造成死锁
        pthread_mutex_lock(&mutex);
        struct Node *newNode = (struct Node *)malloc(sizeof(struct Node));
        newNode->next = head;
        head = newNode;
        newNode->num = rand() % 1000;
        printf("add node, num : %d, tid : %ld\n", newNode->num, pthread_self());
        pthread_mutex_unlock	(&mutex);
		
        // 通知消费者消费
        sem_post(&csem);
    }
    return NULL;
}

void *cosumer(void *arg)
{
    while (1)
    {
        sem_wait(&csem);

        pthread_mutex_lock(&mutex);
        // 保存头结点的指针
        struct Node *tmp = head;
        head = head->next;
        printf("del node, num : %d, tid : %ld\n", tmp->num, pthread_self());
        free(tmp);
        pthread_mutex_unlock(&mutex);

        //消耗一个产品之后,psem(产品队列可容纳数) + 1
        // 通知生产者生成, 给生产者加信号灯
        sem_post(&psem);
    }
    return NULL;
}

int main()
{
    pthread_mutex_init(&mutex, NULL);
    //生产者队列长度为8
    //生成者线程一共有8个信号灯
    sem_init(&psem, 0, 8);
    // 消费者线程一共有0个信号灯
    sem_init(&csem, 0, 0);

    // 创建5个生产者线程,和5个消费者线程
    pthread_t ptids[5], ctids[5];

    int i;
    for (i = 0; i < 5; i++)
    {
        pthread_create(&ptids[i], NULL, producer, NULL);
        pthread_create(&ctids[i], NULL, cosumer, NULL);
    }

    for (i = 0; i < 5; i++)
    {
        pthread_detach(ptids[i]);
        pthread_detach(ctids[i]);
    }

    while (1)
    {
        sleep(10);
    }

    pthread_mutex_destroy(&mutex);

    pthread_exit(NULL);

    return 0;
}

线程池

​ 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低

系统的效率,因为频繁创建线程和销毁线程需要时间。----> 有没有一种办法使得线程可以复用,就是执行完一个任

务,并不被销毁,而是可以继续执行其他的任务呢?

线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:

  • 任务队列存储需要处理的任务,由工作的线程来处理这些任务
    • 通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
    • 已处理的任务会被从任务队列中删除
    • 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
  • 工作的线程(任务队列任务的消费者) ,N个
    • 线程池中维护了一定数量的工作线程,他们的作用是不停的读任务队列,从里边取出任务并处理
    • 工作的线程相当于是任务队列的消费者角色,
    • 如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)
    • 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
  • 管理者线程(不处理任务队列中的任务),1个
    • 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
    • 当任务过多的时候,可以适当的创建一些新的工作线程
    • 当任务过少的时候,可以适当的销毁一些工作的线程
      在这里插入图片描述

任务队列

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

线程池定义

// 线程池结构体
struct ThreadPool
{
    // 任务队列
    Task* taskQ;
    int queueCapacity;  // 容量
    int queueSize;      // 当前任务个数
    int queueFront;     // 队头 -> 取数据
    int queueRear;      // 队尾 -> 放数据

    // 管理者线程ID
    pthread_t managerID;  
    // 工作的线程ID
    pthread_t *threadIDs;   
    
    int minNum;             // 最小线程数量
    int maxNum;             // 最大线程数量
    int busyNum;            // 忙的线程的个数
    int liveNum;            // 存活的线程的个数
    int exitNum;            // 要销毁的线程个数
    
    // 锁整个的线程池
    pthread_mutex_t mutexPool; 
    // 锁busyNum变量
    pthread_mutex_t mutexBusy;  
    // 任务队列是不是满了  --->若是满了 就不能加入新任务 阻塞在添加任务处
    pthread_cond_t notFull;  
    // 任务队列是不是空了  --->若是空了 就不能取数据 阻塞在获取任务处
    pthread_cond_t notEmpty;    

     // 是不是要销毁线程池, 销毁为1, 不销毁为0
    int shutdown;          
};

头文件声明

#ifndef _THREADPOOL_H
#define _THREADPOOL_H

typedef struct ThreadPool ThreadPool;

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

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

// 给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);

// 获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);

// 获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);

// 单个线程退出
void threadExit(ThreadPool* pool);

//
// 工作的线程(消费者线程)任务函数
void* worker(void* arg);
// 管理者线程任务函数
void* manager(void* arg);

#endif  // _THREADPOOL_H

int threadPoolBusyNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexBusy);
    int busyNum = pool->busyNum;
    pthread_mutex_unlock(&pool->mutexBusy);
    return busyNum;
}

int threadPoolAliveNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexPool);
    int aliveNum = pool->liveNum;
    pthread_mutex_unlock(&pool->mutexPool);
    return aliveNum;
}

创建线程池

//min --->最少的线程数  max--->最大的线程数   queueSize--->任务队列的容量

ThreadPool* threadPoolCreate(int min, int max, int queueSize)
{
    ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
    do 
    {
        if (pool == NULL)
        {
            printf("malloc threadpool fail...\n");
            break;
        }
		
        //以最大的线程数分配内存 以最小的线程数创建线程
        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * max);
        if (pool->threadIDs == NULL)
        {
            printf("malloc threadIDs fail...\n");
            break;
        }
        memset(pool->threadIDs, 0, sizeof(pthread_t) * max);
        pool->minNum = min;
        pool->maxNum = max;
        pool->busyNum = 0;
        pool->liveNum = min;    // 和最小个数相等
        pool->exitNum = 0;

        if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
            pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
            pthread_cond_init(&pool->notEmpty, NULL) != 0 ||
            pthread_cond_init(&pool->notFull, NULL) != 0)
        {
            printf("mutex or condition init fail...\n");
            break;
        }

        // 任务队列
        pool->taskQ = (Task*)malloc(sizeof(Task) * queueSize);
        pool->queueCapacity = queueSize;
        pool->queueSize = 0;
        pool->queueFront = 0;
        pool->queueRear = 0;

        pool->shutdown = 0;

        // 创建线程
        pthread_create(&pool->managerID, NULL, manager, pool);
        for (int i = 0; i < min; ++i)
        {
            pthread_create(&pool->threadIDs[i], NULL, worker, pool);
        }
        return pool;
    } while (0);

    // 释放资源
    if (pool && pool->threadIDs) free(pool->threadIDs);
    if (pool && pool->taskQ) free(pool->taskQ);
    if (pool) free(pool);

    return NULL;
}

销毁线程池

int threadPoolDestroy(ThreadPool* pool)
{
    if (pool == NULL)
    {
        return -1;
    }

    // 关闭线程池
    pool->shutdown = 1;
    
    // 阻塞回收管理者线程
    pthread_join(pool->managerID, NULL);
    
    // 唤醒阻塞的消费者线程
    for (int i = 0; i < pool->liveNum; ++i)
    {
        pthread_cond_signal(&pool->notEmpty);
    }
    
    // 释放堆内存
    if (pool->taskQ)
    {
        free(pool->taskQ);
    }
    if (pool->threadIDs)
    {
        free(pool->threadIDs);
    }

    pthread_mutex_destroy(&pool->mutexPool);
    pthread_mutex_destroy(&pool->mutexBusy);
    pthread_cond_destroy(&pool->notEmpty);
    pthread_cond_destroy(&pool->notFull);

    free(pool);
    pool = NULL;

    return 0;
}

给线程池添加任务

void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg)
{
    pthread_mutex_lock(&pool->mutexPool);
    /*
	为什么使用if 有bug:
    	当任务队列为空, 所有的消费者线程都会被这个函数阻塞 pthread_cond_wait(&cond, &mutex);
    也就是阻塞在代码的第9行
	
    当生产者生产了1个节点, 调用 pthread_cond_broadcast(&cond); 唤醒了所有阻塞的线程
      - 有一个消费者线程通过 pthread_cond_wait()加锁成功, 其余没有加锁成功的线程继续阻塞
      - 加锁成功的线程向下运行, 并成功删除一个节点, 然后解锁
      - 没有加锁成功的线程解除阻塞继续抢这把锁, 另外一个子线程加锁成功
      - 但是这个线程删除链表节点的时候链表已经为空了, 后边访问这个空节点的时候就会出现段错误
    解决方案:
      - 需要循环的对链表是否为空进行判断, 需要将if 该成 while
*/

    while (pool->queueSize == pool->queueCapacity && !pool->shutdown)
    {
        // 阻塞生产者线程
        pthread_cond_wait(&pool->notFull, &pool->mutexPool);
    }
    if (pool->shutdown)
    {
        pthread_mutex_unlock(&pool->mutexPool);
        return;
    }
    
    // 添加任务 队尾添加任务
    pool->taskQ[pool->queueRear].function = func;
    pool->taskQ[pool->queueRear].arg = arg;
    pool->queueRear = (pool->queueRear + 1) % pool->queueCapacity;
    pool->queueSize++;
	
    //通知消费线程来拿任务队列中的数据
    pthread_cond_signal(&pool->notEmpty);
    
    pthread_mutex_unlock(&pool->mutexPool);
}

工作的线程

线程执行函数 若有很多空闲线程 manager提供notEmpty信号,自动销毁

void* worker(void* arg)
{
    ThreadPool* pool = (ThreadPool*)arg;

    while (1)
    {
        pthread_mutex_lock(&pool->mutexPool);
        // 当前任务队列是否为空
        while (pool->queueSize == 0 && !pool->shutdown)
        {
            // 阻塞工作线程
            pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);

            // 判断是不是要销毁线程
            if (pool->exitNum > 0)
            {
                //自减操作不能放在if语句中 若if条件不满足 exitNum无法自减 
                //若加入任务后,唤醒工作线程工作,但是exitNum仍大于0 会直接自毁而不是去工作
                pool->exitNum--;
                if (pool->liveNum > pool->minNum)
                {
                    pool->liveNum--;
                    pthread_mutex_unlock(&pool->mutexPool);
                    threadExit(pool);
                }
            }
        }

        // 判断线程池是否被关闭了
        if (pool->shutdown)
        {
            pthread_mutex_unlock(&pool->mutexPool);
            threadExit(pool);
        }

        // 从任务队列中取出一个任务
        Task task;
        task.function = pool->taskQ[pool->queueFront].function;
        task.arg = pool->taskQ[pool->queueFront].arg;
        // 移动头结点
        pool->queueFront = (pool->queueFront + 1) % pool->queueCapacity;
        pool->queueSize--;
        
        //
        pthread_cond_signal(&pool->notFull);
        // 解锁
        pthread_mutex_unlock(&pool->mutexPool);

        printf("thread %ld start working...\n", pthread_self());
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum++;
        pthread_mutex_unlock(&pool->mutexBusy);
        
        task.function(task.arg);
        free(task.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;
}

管理者线程

每隔3秒轮询 判断是否需要创建线程或者销毁线程

void* manager(void* arg)
{
    ThreadPool* pool = (ThreadPool*)arg;
    while (!pool->shutdown)
    {
        // 每隔3s检测一次
        sleep(3);

        // 取出线程池中任务的数量和当前线程的数量
        pthread_mutex_lock(&pool->mutexPool);
        int queueSize = pool->queueSize;
        int liveNum = pool->liveNum;
        pthread_mutex_unlock(&pool->mutexPool);

        // 取出忙的线程的数量
        pthread_mutex_lock(&pool->mutexBusy);
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexBusy);

        // 添加线程
        // 任务的个数>存活的线程个数 && 存活的线程数<最大线程数
        if (queueSize > liveNum && liveNum < pool->maxNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            int counter = 0;
            for (int i = 0; i < pool->maxNum && counter < NUMBER
                && pool->liveNum < pool->maxNum; ++i)
            {
                if (pool->threadIDs[i] == 0)
                {
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);
                    counter++;
                    pool->liveNum++;
                }
            }
            pthread_mutex_unlock(&pool->mutexPool);
        }
        
        // 销毁线程
        // 忙的线程*2 < 存活的线程数 && 存活的线程>最小线程数
        if (busyNum * 2 < liveNum && liveNum > pool->minNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            pool->exitNum = NUMBER;
            pthread_mutex_unlock(&pool->mutexPool);
            // 让工作的线程自杀
            for (int i = 0; i < NUMBER; ++i)
            {
                pthread_cond_signal(&pool->notEmpty);
            }
        }
    }
    return NULL;
}

单个线程退出

void threadExit(ThreadPool* pool)
{
    pthread_t tid = pthread_self();
    for (int i = 0; i < pool->maxNum; ++i)
    {
        if (pool->threadIDs[i] == tid)
        {
            pool->threadIDs[i] = 0;
            printf("threadExit() called, %ld exiting...\n", tid);
            break;
        }
    }
    pthread_exit(NULL);
}

图片引用自 爱编程的大丙 https://subingwen.cn/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值