C++ (week5):Linux系统编程3:线程

news2024/10/7 12:27:13

文章目录

    • 三、线程
      • 1.线程的基本概念
        • 线程相关概念
        • ②我的理解
      • 2.线程的基本操作 (API)
        • (1)获取线程的标识:pthread_self
        • (2)创建线程:pthread_create()
        • (3)终止线程
          • pthread_exit():当前线程终止,子线程主动退出
          • ②pthread_cancel():发送取消请求,用一个线程终止另一个线程
        • (4)线程等待:pthread_join():(无限期)等待子线程结束,并接收子线程的返回值
        • (5)线程游离:pthread_detach():主线程将子线程设置为分离状态
        • (6)线程资源的清理:线程清理函数
          • ①pthread_cleanup_push()
          • ②pthread_cleanup_pop()
          • ③线程清理函数
      • 3.线程的同步 (sync)
        • (1)术语
        • (2)互斥
          • 互斥锁 (Mutex)
          • ②死锁 (Deadlock)
        • (3)条件变量 (Condition Variable):pthread_cond_t
          • ①初始化条件变量:pthread_cond_init()
          • 等待条件变量:pthread_cond_wait()
          • ③通知条件变量
            • i.pthread_cond_signal()
            • ii.pthread_cond_broadcast()
          • ④销毁条件变量:pthread_cond_destroy()
          • ⑤生产者-消费者模型
            • i.阻塞队列
            • ii.线程池:生产者消费者模型
      • 4.线程安全
      • 5.可重入性
      • 6.线程池 (Thread Pool)
      • 7.其他
        • (1)第一性原理
        • (2)知识图谱

三、线程

1.线程的基本概念

线程相关概念

1.什么是线程
线程是进程的一条执行流程。
线程被称为轻量级进程(Light Weight Process, LWP)


2.为什么要引入线程?/ 引入线程的好处

简单解释详细解释
①创建和销毁相对进程而言,线程的创建销毁是轻量级的线程的创建和销毁的开销比进程小。进程需要获取和释放资源,而线程拥有的资源较少:
①进程的创建和销毁更耗时,因为涉及资源的获取和释放
②线程拥有的资源少,创建和销毁比较轻量级,耗时短
②切换切换线程的开销比切换进程小。①同一进程的线程之间的切换,Cache、TLB不会失效,不需要读内存重载Cache和TLB,只需要切换线程的上下文,开销小。
②切换进程时,Cache、TLB会失效,重新载入需要读内存,开销巨大。(进程上下文切换的开销其实和线程切换上下文开销差不多)
③通信线程之间通信更简单。进程通信需要打破隔离①进程间通信,需要打破隔离,通信的代价大
②同一进程的线程之间通信开销很小,几乎没有代价
④异步引入线程机制,可以实现异步。充分利用了多核CPU的性能异步编程的优势:提高应用程序的响应性和性能

3.引入线程后
进程是资源分配的最小单位,线程是调度的最小单位。
线程们共享进程的所有资源,其他线程也可以访问主线程的栈空间。


4.主线程其他线程
主线程的栈大小:8MB
其他线程的栈大小:2MB


在这里插入图片描述


线程是进程的一条执行流程:main是主线程的执行流程,start_routine是子线程的执行流程
在这里插入图片描述


②我的理解

1.主子线程与父子进程的区别:
①父进程死亡,子进程变为孤儿进程
②主线程终止,代表整个进程终止,所有其他线程会被终止

2.主线程与子线程
①主线程代表进程的主流程,是老板,分配任务。主线程拆分任务,将任务分配给子线程。
②其他线程是员工,只完成自己的部分

3.为什么需要引入线程?其他线程和函数调用的区别是什么?
其他线程和函数调用的功能很像。
但是引入其他线程,相当于多了几个流程,几条线同时往下走,可以同时异步并发执行,利用了多核CPU
若只有进程的函数调用,则只有一根流程线 同步执行,相当于老板自己干活。只用了一个CPU。


2.线程的基本操作 (API)

1.线程函数
在这里插入图片描述

2.编译链接时,要加选项 -pthread-lpthread

3.pthread设计原则:
返回值为int类型,标识调用成功或失败
成功:返回0
失败:返回错误码errno,所以不会设置errno


(1)获取线程的标识:pthread_self
#include <pthread.h>

pthread_t pthread_self(void);  //unsigned long, %lu

①若能返回,一定成功。
②若失败,不返回。


(2)创建线程:pthread_create()

1.函数原型

#include <pthread.h>

int pthread_create(pthread_t* tid, const pthread_attr_t *attr,
                 void *(*start_routine) (void *), void *args);

参数:
①&tid:若线程创建成功,则修改tid的值为创建的线程的tid,pthread_t tid
②attr:线程属性,一般填NULL,表示采用默认属性
start_routine:线程的入口函数 (函数指针),是子线程的执行流程 (main是主线程的执行流程)
④args:线程入口函数的参数 (只能有一个参数,若需要传多个参数,可用结构体包装,并传结构体的指针)

void*:第三个参数可以返回任意值,第四个参数可以返回任意函数


2.返回值:
①成功,返回0
②失败,返回errno

如果线程创建成功,pthread_create()返回0,此时tid会被设置为新线程的线程ID。如果线程创建失败,pthread_create()返回一个非零的错误码,而tid的值不会被修改。


3.基本用法
(1)不传参,第四个参数为NULL

pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, NULL);
if(err){
	error(1, err, "pthread_create");
}
void* start_routine(void* args) {
    print_ids("new_thread");
    
    return NULL;
}

(2)传参

pthread_t tid;
int err = pthread_create(&tid, NULL, start_routine, void*(1027));
if(err){
	error(1, err, "pthread_create");
}
void* start_routine(void* args) {
    int i = (int)args; 	 //C语言是弱类型语言,可强转
    printf("new_thread: i = %d\n", i);
    
    return NULL;
}

4.代码示例
//①创建子线程,不传参数
//②创建子线程,传一个整数 [容得下,直接传]
//③在子线程中,访问主线程的栈的数据,传结构体指针 [容不下,传指向数据的指针,即传地址]

github网址:https://github.com/WangEdward1027/pthread/tree/main/pthread_create


(3)终止线程
终止进程终止线程
正常终止从main返回从start_routine返回 ×
正常终止exit()pthread_exit() √
异常终止收到信号pthread_cancel() √
注册退出函数atexit()注册进程退出函数pthread_cleanup_push()
pthread_cleanup_pop()

pthread_exit():当前线程终止,子线程主动退出

1.函数原型

#include <pthread.h>

void pthread_exit(void *retval);

(1)参数
①void* retval:返回 任意值,任意类型的数据(void*) 给主线程。

pthread_exit((void*)sum);
或者
return (void*)sum;

②主线程不需要子线程的返回结果,则子线程中调用

pthread_exit(NULL);return NULL;

③注意,不能返回指向该线程栈上数据的指针。因为当子线程退出时,子线程的栈空间会销毁

④pthread_exit()执行时,会执行线程清理函数,将cleanup栈中还未pop的栈帧全部弹出。


2.代码
//子线程使用pthread_exit()退出后, start_routine的后续代码不执行
github网址:https://github.com/WangEdward1027/pthread/blob/main/pthread_exit/pthread_exit.c


②pthread_cancel():发送取消请求,用一个线程终止另一个线程

1.功能
发送取消请求,用一个线程终止另一个线程。但对方不一定响应。

2.响应时机
会不会相应,以及何时响应,取决于目标线程的两个属性,CANCEL_STATE、CANCEL_TYPE


3.函数原型

#include <pthread.h>

int pthread_cancel(pthread_t tid);

4.返回值:
①成功,返回0
②失败,返回非0的错误码


5.取消状态、取消类型
(1)CANCEL_STATE:是否响应
①PTHREAD_CANCEL_ENABLE:能够响应。[默认值]
②PTHREAD_CANCEL_DISABLE:不响应

(2)CANCEL_TYPE:何时响应
①PTHREAD_CANCEL_DEFERRED:延迟响应,延迟到取消点才响应。[默认值]
②PTHREAD_CANCEL_ASYNCHRONOUS:立刻响应,在任何时刻都可以响应

线程的默认行为是延迟取消,即在取消点检查取消请求

(3)函数原型

#include <pthread.h>

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

(4)用法

int oldstate;  //保存旧的状态,以便后续恢复
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRNOUS, &oldstate);

6.取消点
(1)取消点有很多
①pthread_testcancel():显式地设置取消点,检查是否有响应请求,如果有就立刻响应,非阻塞状态

#include <pthread.h>

void pthread_testcancel(void);
使用场景:长时间运行的循环:在循环中插入 pthread_testcancel,使线程在每次循环迭代时检查取消请求

②pthread_join()
③sleep()
④read()
⑤write()

(2)取消点可能会陷入长时间的阻塞



(4)线程等待:pthread_join():(无限期)等待子线程结束,并接收子线程的返回值

1.概念
①父进程通过wait/waitpid获取子进程的终止信息
②主线程用pthread_join()接收子线程的返回值


2.pthread_join:
①接收子线程pthread_exit()的返回值
②接收子线程return的返回值

3.函数原型

#include <pthread.h>

int pthread_join(pthread_t tid, void **retval);

参数:
①thread:等待哪个子线程结束
②void** retval:传出参数,接收void*类型的值,所以是二级指针

4.用法:

int result;	//子线程若返回一个int类型
err = pthread_join(tid, &result);
//err = pthread_join(tid, (void**)&result);
if(err){
	error(1, err, "pthread_join %lu\n",tid);
}

5.代码
//①主线程用pthread_join()接收子线程的返回值
//②子线程不能返回自己栈上数据的指针,只能返回子线程堆上的数据。因为当子线程退出的时候,子线程的栈会销毁!

github网址:https://github.com/WangEdward1027/pthread/tree/main/pthread_join


(5)线程游离:pthread_detach():主线程将子线程设置为分离状态

1.概念
主线程不需要接收该子线程的返回值时,主线程可使用pthread_detach,将主线程和子线程分离,将子线程变为游离线程 (detached thread)。

游离线程终止后,操作系统会自动处理并释放与该线程相关的所有资源。主线程不需要也无法使用 pthread_join 来等待它结束和释放资源。

分离线程的设计就是为了避免显式的资源管理,适合于那些不需要等待其结束的后台任务。例如,后台日志写入线程、定时器线程等

在这里插入图片描述


2.函数原型

#include <pthread.h>

int pthread_detach(pthread_t thread);
pthread_detach(tid);

3.代码
//主线程主动使用pthred_detach,则子线程退出后系统会自动回收其资源,主线程不需要也无法显式地调用pthread_join来回收子进程的返回值

github网址:https://github.com/WangEdward1027/pthread/tree/main/pthread_detach

(6)线程资源的清理:线程清理函数

注册线程清理函数
用栈保存,执行顺序与注册顺序相反。

在这里插入图片描述

①pthread_cleanup_push()

1.函数原型

#include <pthread.h>

void pthread_cleanup_push(void (*routine)(void *), void *arg);

②pthread_cleanup_pop()

1.函数原型

#include <pthread.h>

void pthread_cleanup_pop(int execute);

execute == 0:出栈,不执行
execute != 0:出栈,并执行


③线程清理函数

1.代码
github网址:https://github.com/WangEdward1027/pthread/blob/main/pthread_cleanup/pthread_cleanup.c

2.执行线程清理函数的时机
①pthread_exit():✔ 会执行线程清理函数,将cleanup栈中还未pop的栈帧全部弹出。
②pthread_cancel():✔ 响应取消请求
③调用pthread_cleanup_pop(非0值):✔ 从栈上出栈一个函数,并执行,并但不会导致线程的终止
④从start_routine返回:❌ return 不会执行线程清理函数

注意:pthread_cleanup_push() 和 pthread_cleanup_pop() 必须成对出现
有多少个pthread_cleanup_push,就要对应写多少个pthread_cleanup_pop。否则编译不通过。
原因:它们是改进版宏函数,do{ 宏函数体} while(0)。所以必须成对出现,否则语法不通过。



3.线程的同步 (sync)

(1)术语

1.原子性:CPU指令是原子的
举例:i++的汇编代码:

LOAD  R1, i    ; 将变量 i 的值加载到寄存器 R1
ADD   R1, 1    ; 将寄存器 R1 的值加 1
STORE i,  R1   ; 将增加后的值存回变量 i

在STORE之前切换线程,导致其他线程读脏数据。


2.竞态条件 (race condition)
①多个执行流程 (并发执行)
共享资源
程序的结果(状态)取决于执行流程调度的情况   (调度是随机的,所以程序的结果看起来每次都不同)

若多个执行流程存在共享资源,且程序的结果取决于实际调度,则称为竟态条件。需要加上同步手段,比如互斥锁。


3.同步和异步
(1)异步
任何调度情况都可能出现。两个执行流程不做任何交流。会得到随机的结果。
②速度快

(2)同步 sync
①概念
1)两个执行流程发生了交流,让一些坏的调度情况不出现,只出现好的调度。会得到预期的结果。(让某些调度不可能出现)

同步用于协调线程之间的操作,以确保共享资源的正确访问。同步的主要机制包括互斥锁、读写锁、信号量和屏障等。

2)同步会有一些开销。
3)发生了竟态条件,一定要配合同步

②要求:
1)互斥地访问资源:互斥锁
2)等待某个条件成立

同步类似现实生活中的规则
多个执行流程,默认是并发、异步执行


4.并发和并行
(1)并发
并发是一种现象,在一个时间段中,执行流程可以交替运行。使得多个任务在宏观上看起来是同时执行的。只需要一个CPU核。

(2)并行
并行是一种技术,使得可以同时执行多个执行流程。需要多个CPU核才能实现。(并行是并发的一种)

在这里插入图片描述


(2)互斥

互斥锁、读写锁、CAS


互斥锁 (Mutex)

0.互斥锁 mutex (mutual exclusive,相互排斥的)
原子性:CPU指令

1.作用
加入了互斥锁,将原本不是原子操作的命令,变成了逻辑上的原子操作。(进入临界区之前,获取锁;执行临界区代码;退出临界区,释放锁)

互斥锁是一种用于确保在任何时刻只有一个线程可以访问共享资源的机制。它通常用于保护临界区,防止多个线程同时访问共享资源导致数据不一致

在这里插入图片描述


2.什么时候上锁?
在临界区要上锁
临界区 (critical area):对共享资源的操作的代码(指令)

注意事项:①锁的粒度 ②避免死锁


3.函数原型
(1)初始化锁:
①静态初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

②动态初始化

int pthread_mutex_init(pthread_mutex_t* restrict mutex, 
			 const pthread_mutexattr_t* restrict attr);
pthread_mutex_init(&mutex, NULL);  //attr一般填NULL,表示默认属性

传出参数,要修改,所以要加取地址,传的是指针


(2)尝试获取锁

int pthread_mutex_trylock(pthread_mutex_t* mutex); //不会阻塞,若锁不可用则立刻返回

(3)上锁

int pthread_mutex_lock(pthread_mutex_t* mutex);  //无限期阻塞
pthread_mutex_lock(&mutex);

(4)释放锁

int pthread_mutex_unlock(pthread_mutex_t* mutex);  //释放锁

(5)销毁锁

int pthread_mutex_destroy(pthread_mutex_t* mutex); 

4.代码
//sync.c
//bank.c

github网址:https://github.com/WangEdward1027/pthread/tree/main/pthread_sync

注意事项:
①选择合适粒度的锁
②避免死锁的发生


②死锁 (Deadlock)

1.死锁出现的原因 (四个必要条件,同时成立,才可能出现死锁)
①互斥
②不能抢占 (不剥夺)
③持有并等待 (保持并请求)
④循环等待

在这里插入图片描述

int transfer(Account* acctA, Account* acctB, int money) {
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);  //增加坏的调度的概率
    pthread_mutex_lock(&acctB->mutex);

    if (acctA->balance < money) {
        pthread_mutex_unlock(&acctA->mutex);	//各自先申请自己的锁
        pthread_mutex_unlock(&acctB->mutex);	//导致死锁

        return 0;
    }

    acctA->balance -= money;
    acctB->balance += money;

    pthread_mutex_unlock(&acctA->mutex);
    pthread_mutex_unlock(&acctB->mutex);

    return money;
}

2.避免死锁:破坏死锁四个必要条件中的一个或多个
(1)破坏循环等待条件:
按固定的顺序,依次获取锁 (如按id的顺序依次获取锁)

在这里插入图片描述


(2)破坏持有并等待条件:
两把锁,要么一口气都获取,要么都不能获取。则可以给获取两把锁的操作再加一个锁。

在这里插入图片描述


(3)破坏不能抢占条件:
拿到锁1后,尝试拿锁2,若拿不到则主动放弃锁1.
注意要随机睡眠来模拟随机调度,不然同频睡眠可能导致同时拿起各自的锁,询问对方,同时释放各自的锁

(经测试,该方法效率最差)
在这里插入图片描述
在这里插入图片描述


(4)互斥
很多情况下无法破坏互斥条件
CAS操作: (compare and swap)
CAS是复杂的CPU指令,导致软件层面可以实现无锁编程
Lock_free 算法、wait_free 算法


3.//deadlock.c 转账的例子

#include <func.h>

typedef struct {
    int id;
    char name[25];
    int balance;
    // 细粒度锁
    pthread_mutex_t mutex;
} Account;

Account acct1 = {1, "xixi", 1000, PTHREAD_MUTEX_INITIALIZER};
Account acct2 = {2, "peanut", 100, PTHREAD_MUTEX_INITIALIZER};

pthread_mutex_t protection = PTHREAD_MUTEX_INITIALIZER;

int transfer(Account* acctA, Account* acctB, int money) {
    // 4. 循环等待
    // 按id的顺序依次获取锁

    /* if (acctA->id < acctB->id) { */
    /*     pthread_mutex_lock(&acctA->mutex); */
    /*     sleep(1);   // 增加坏的调度的概率 */
    /*     pthread_mutex_lock(&acctB->mutex); */
    /* } else { */
    /*     pthread_mutex_lock(&acctB->mutex); */
    /*     sleep(1);   // 增加坏的调度的概率 */
    /*     pthread_mutex_lock(&acctA->mutex); */
    /* } */

    /* // 3. 不能抢占 */
/* start: */
    /* pthread_mutex_lock(&acctA->mutex); */
    /* sleep(1); */
    /* int err = pthread_mutex_trylock(&acctB->mutex); */
    /* if (err) { */
    /*     // 主动释放获取的锁 */
    /*     pthread_mutex_unlock(&acctA->mutex); */
    /*     int seconds = rand() % 10; */
    /*     sleep(seconds); */
    /*     goto start; */
    /* } */

    // 2. 持有并等待
    pthread_mutex_lock(&protection);
    pthread_mutex_lock(&acctA->mutex);
    sleep(1);
    pthread_mutex_lock(&acctB->mutex);
    pthread_mutex_unlock(&protection);

    if (acctA->balance < money) {
        pthread_mutex_unlock(&acctA->mutex);
        pthread_mutex_unlock(&acctB->mutex);

        return 0;
    }

    acctA->balance -= money;
    acctB->balance += money;

    pthread_mutex_unlock(&acctA->mutex);
    pthread_mutex_unlock(&acctB->mutex);

    return money;
}

void* start_routine1(void* args) {
    int money = (int) args;
    int ret = transfer(&acct1, &acct2, money);
    printf("%s -> %s: %d\n", acct1.name, acct2.name, ret);
    return NULL;
}

void* start_routine2(void* args) {
    int money = (int) args;
    int ret = transfer(&acct2, &acct1, money);
    printf("%s -> %s: %d\n", acct2.name, acct1.name, ret);
    return NULL;
}

int main(int argc, char* argv[])
{
    srand(time(NULL));

    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, start_routine1, (void*)900);    
    pthread_create(&tid2, NULL, start_routine2, (void*)100);    

    // 主线程等待子线程结束
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    printf("%s: balance = %d\n", acct1.name, acct1.balance);
    printf("%s: balance = %d\n", acct2.name, acct2.balance);
    return 0;
}



(3)条件变量 (Condition Variable):pthread_cond_t

等待某个条件成立:
条件变量只是提供了一个等待、唤醒机制,至于条件何时成立、何时不成立,取决于业务


①初始化条件变量:pthread_cond_init()

1.动态初始化

int pthread_cond_init(pthread_cond_t* restrict cond, 
			 const pthread_condattr_t* restrict attr);
pthread_cond_init(&cond, NULL);  //attr一般填NULL,表示默认属性

2.静态初始化

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

等待条件变量:pthread_cond_wait()

当条件不成立,等待 (在线程中等待某个条件成立):
在这里插入图片描述

问题:pthread_cond_wait()为什么需要传递互斥锁?
互斥锁:保护cond变量,是多线程共享的资源


pthread_cond_wait()的内部实现的三个步骤:
释放互斥锁 (①②是原子操作)
阻塞等待
重新获取互斥锁
当返回时,该线程一定再一次获取了mutex
返回时,cond条件曾经成立过,现在是否成立,不确定。存在虚假唤醒现象。


当条件变量被唤醒时,线程会从等待队列中移出,并重新尝试获取传入的互斥锁。只有在成功获取到互斥锁后,pthread_cond_wait才会返回,线程继续执行后续代码。
在这里插入图片描述

//惯用法
pthread_mutex_lock(&mutex);
...
while (!condition) {
    pthread_cond_wait(&cond, &mutex);
}
...  // 条件满足后执行的代码
pthread_mutex_unlock(&mutex);

pthread_cond_wait的伪代码实现:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) {
    // 原子地释放互斥锁并进入条件变量的等待队列
    enter_atomic_section();
    
    // 释放互斥锁
    pthread_mutex_unlock(mutex);
    
    // 将当前线程放入条件变量的等待队列,并进入等待状态
    add_thread_to_cond_wait_queue(cond, current_thread);
    
    // 离开原子区域
    leave_atomic_section();
    
    // 线程进入等待状态,直到条件成立, 被唤醒
    thread_wait(current_thread);
    
    // 重新获取互斥锁
    pthread_mutex_lock(mutex);
    
    return 0;
}

③通知条件变量

当条件成立时,唤醒等待该条件的线程

在这里插入图片描述

i.pthread_cond_signal()

唤醒至少一个等待该条件变量的线程

注意:在实际实现时,为了性能考虑,可能会一次性唤醒多个线程。

cond维护一个队列
在这里插入图片描述


当某个线程改变条件并使之满足时,通知等待的线程:

pthread_mutex_lock(&mutex);
// 改变条件
condition = 1;
pthread_cond_signal(&cond);  // 或者使用pthread_cond_broadcast
pthread_mutex_unlock(&mutex);

ii.pthread_cond_broadcast()

唤醒所有等待该条件变量的线程

int pthread_cond_broadcast(pthread_cond_t *cond);

在这里插入图片描述


④销毁条件变量:pthread_cond_destroy()

销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

⑤生产者-消费者模型

使用条件变量、阻塞队列来实现生产者-消费者模型:

①阻塞队列:
当队列满时,如果线程往阻塞队列中添加东西,线程会陷入阻塞
当队列空时,如果线程往阻塞队列中取东西,线程会陷入阻塞

②生产者:生产商品
如果队列满了,生产者陷入阻塞,等待队列不满(Not_full)
如果队列不满,生产商品,将商品添加到阻塞队列,队列处于非空状态(Not_empty),唤醒消费者

③消费者:消费商品
如果队列空了,消费者陷入阻塞,等待队列非空(Not_empty)
如果队列非空,从阻塞队列中获取商品,消费商品,队列处于非满状态(Not_full),唤醒生产者

在这里插入图片描述


i.阻塞队列

有界队列:有增长的上限,否则服务器会Out of Memory(OOM现象)

在这里插入图片描述

代码见github网址:https://github.com/WangEdward1027/pthread/tree/main/BlockQueue


ii.线程池:生产者消费者模型

线程池避免了频繁的创建和销毁线程,避免了冷启动

在这里插入图片描述

问题:应用程序,应该包含多少个线程(包括main线程)?
①取决于CPU的核数
②取决于任务的负载类型:
计算密集型:一比一
I/O密集型:一比N (N≥2)

在这里插入图片描述

github代码:[https://github.com/WangEdward1027/pthread/tree/main/threadpool]
(https://github.com/WangEdward1027/pthread/tree/main/threadpool)

这段代码实际上实现了一个生产者-消费者模型。线程池中的线程充当消费者,而主线程充当生产者
①生产者:主线程负责创建和添加任务到阻塞队列中。
②消费者:线程池中的线程负责从阻塞队列中取出并执行任务。
这种设计使得任务可以并发处理,从而提高程序的执行效率。在实际应用中,生产者-消费者模型是多线程编程中的一个常见模式,用于解决任务调度和负载均衡问题。



4.线程安全

1.定义:
线程安全是指在多线程环境下访问共享资源时,程序能够正确地运行,不会出现数据竞争和其他同步

2.实现
①互斥锁 (Mutex)
②读写锁(Read-Write Lock):
③条件变量
信号量(Semaphore):
④自旋锁
原子操作(Atomic Operation):



5.可重入性

1.可重入函数 (Reentrant Function)
(1)定义
可重入函数是指可以被多个线程同时调用而不引起任何问题的函数。这类函数不会依赖或修改共享数据,或在访问共享数据时使用适当的同步机制。

在这里插入图片描述



6.线程池 (Thread Pool)

线程池是一种线程管理技术,允许程序在运行时创建和管理多个线程,以便于处理大量并发任务。线程池通过复用已创建的线程来减少线程创建和销毁的开销,提高系统性能和资源利用率。


GPT实现的线程池:

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

#define THREAD_POOL_SIZE 5

// 任务结构体,用于存储任务信息
typedef struct {
    void (*function)(void *); // 函数指针,指向任务函数
    void *arg;                // 函数参数
} Task;

// 线程池结构体
typedef struct {
    pthread_mutex_t lock;     // 互斥锁,保护任务队列
    pthread_cond_t  notify;   // 条件变量,用于通知空闲线程
    pthread_t       threads[THREAD_POOL_SIZE]; // 线程数组
    Task            queue[THREAD_POOL_SIZE * 2]; // 任务队列
    int             queue_size; // 任务队列大小
    int             head;       // 队列头部指针
    int             tail;       // 队列尾部指针
    int             shutdown;   // 线程池关闭标志
} ThreadPool;

// 初始化线程池
ThreadPool* create_threadpool() {
    ThreadPool *pool = (ThreadPool*)malloc(sizeof(ThreadPool));
    if (pool == NULL) {
        fprintf(stderr, "Failed to allocate memory for thread pool\n");
        return NULL;
    }

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&(pool->lock), NULL);
    pthread_cond_init(&(pool->notify), NULL);

    // 初始化任务队列
    pool->queue_size = 0;
    pool->head = pool->tail = 0;
    pool->shutdown = 0;

    // 创建工作线程
    for (int i = 0; i < THREAD_POOL_SIZE; ++i) {
        pthread_create(&(pool->threads[i]), NULL, thread_function, (void*)pool);
    }

    return pool;
}

// 销毁线程池
void destroy_threadpool(ThreadPool *pool) {
    if (pool == NULL) return;

    // 关闭线程池
    pool->shutdown = 1;

    // 唤醒所有等待的线程
    pthread_cond_broadcast(&(pool->notify));

    // 等待所有线程结束
    for (int i = 0; i < THREAD_POOL_SIZE; ++i) {
        pthread_join(pool->threads[i], NULL);
    }

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&(pool->lock));
    pthread_cond_destroy(&(pool->notify));

    // 释放线程池内存
    free(pool);
}

// 线程函数,执行任务队列中的任务
void* thread_function(void *args) {
    ThreadPool* pool = (ThreadPool*)args;
    while (1) {
        pthread_mutex_lock(&(pool->lock));
        while (pool->queue_size == 0 && !pool->shutdown) {
            // 等待任务
            pthread_cond_wait(&(pool->notify), &(pool->lock));
        }
        if (pool->shutdown) {
            // 线程池关闭,退出线程
            pthread_mutex_unlock(&(pool->lock));
            pthread_exit(NULL);
        }
        // 取出任务
        Task task = pool->queue[pool->head];
        pool->head = (pool->head + 1) % (THREAD_POOL_SIZE * 2);
        pool->queue_size--;

        pthread_mutex_unlock(&(pool->lock));

        // 执行任务函数
        (*(task.function))(task.arg);
    }
    return NULL;
}

// 向线程池中添加任务
void add_task(ThreadPool *pool, void (*function)(void *), void *arg) {
    pthread_mutex_lock(&(pool->lock));

    // 队列已满,等待空闲线程
    while (pool->queue_size == THREAD_POOL_SIZE * 2) {
        pthread_cond_wait(&(pool->notify), &(pool->lock));
    }

    // 添加任务到队列尾部
    pool->queue[pool->tail].function = function;
    pool->queue[pool->tail].arg = arg;
    pool->tail = (pool->tail + 1) % (THREAD_POOL_SIZE * 2);
    pool->queue_size++;

    // 唤醒等待的线程
    pthread_cond_signal(&(pool->notify));
    pthread_mutex_unlock(&(pool->lock));
}

// 任务函数示例
void example_task(void *arg) {
    int num = *((int*)arg);
    printf("Task executed with argument: %d\n", num);
    usleep(1000000); // 模拟任务执行
}

int main() {
    ThreadPool *pool = create_threadpool();

    // 向线程池中添加任务
    for (int i = 0; i < 10; ++i) {
        int *arg = (int*)malloc(sizeof(int));
        *arg = i;
        add_task(pool, example_task, (void*)arg);
    }

    // 等待所有任务完成
    sleep(5);

    // 销毁线程池
    destroy_threadpool(pool);

    return 0;
}

在这个示例中,线程池被初始化为包含3个线程和10个任务的队列。任务被添加到队列中,当有空闲线程时,它们会从队列中取出任务并执行。



7.其他

(1)第一性原理

1.程序 = 数据 + 指令 (数据结构 + 算法)
①数据:类型、值、类、对象
②指令:运算符、语句、函数、方法、闭包 …

2.程序的运行方式:
①同步/异步
②并发/并行


(2)知识图谱

1.语言 (多门编程语言):
①数据:类型、值、指针、引用、对象
②指令:运算符、语句、函数、闭包、方法、lamda表达式
③程序执行方式:并发(多条执行流程)、并行、异步、同步

2.工程:
惯用法(idioms)
②设计模式
③架构

3.理论:
①数据结构与算法
②操作系统
③组成原理
④计算机网络
⑤数据库
⑥分布式
⑦编译原理

4.工具:
MySQL、Redis、shell命令、GDB等

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1710779.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C语言 | Leetcode C语言题解之第118题杨辉三角

题目&#xff1a; 题解&#xff1a; int** generate(int numRows, int* returnSize, int** returnColumnSizes) {int** ret malloc(sizeof(int*) * numRows);*returnSize numRows;*returnColumnSizes malloc(sizeof(int) * numRows);for (int i 0; i < numRows; i) {re…

【RocketMQ】安装RocketMQ5.2.0(单机版)

下载 官网下载地址&#xff1a;下载 | RocketMQ github地址&#xff1a;Tags apache/rocketmq GitHub 选择对应的版本下载。https://dist.apache.org/repos/dist/release/rocketmq/5.2.0/rocketmq-all-5.2.0-bin-release.zip 5.2.0的二进制包&#xff1a;下载地址 5.2.0的…

C语言 | Leetcode C语言题解之第117题填充每个节点的下一个右侧节点指针II

题目&#xff1a; 题解&#xff1a; void handle(struct Node **last, struct Node **p, struct Node **nextStart) {if (*last) {(*last)->next *p;}if (!(*nextStart)) {*nextStart *p;}*last *p; }struct Node *connect(struct Node *root) {if (!root) {return NULL…

随机森林算法实现分类

随机森林算法实现对编码后二进制数据的识别 1.直接先上代码&#xff01; import numpy as np import pandas as pd from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import …

数据结构之堆(优先级队列)

前言 在上一章我们讲了二叉树&#xff0c;这一节我们来讲堆&#xff08;优先级队列&#xff09;&#xff0c;所以想知道堆创建&#xff0c;可以看一下二叉树的一些简单概念。http://t.csdnimg.cn/4jUR6http://t.csdnimg.cn/4jUR6 目录 前言 堆 1.概念 2.优先级队列的模拟实…

Day06-Mybatis

1. Mybatis介绍 2. Mybatis连接数据库并返回数据事例 连接oracle数据的设置方式 spring.application.namespringboot-mybatis spring.datasource.driver-class-nameoracle.jdbc.OracleDriver spring.datasource.urljdbc:oracle:thin:192.168.100.66:1521:orcl spring.datasour…

每日一题《leetcode--59.螺旋矩阵 》

https://leetcode.cn/problems/spiral-matrix-ii/ 这道题跟我昨天发布的那道题一模一样&#xff0c;只需要注意这个矩阵是n*n。 文章代码如下&#xff1a; int** generateMatrix(int n, int* returnSize, int** returnColumnSizes) {int** array (int**)malloc(sizeof(int*) *…

Python | Leetcode Python题解之第118题杨辉三角

题目&#xff1a; 题解&#xff1a; class Solution:def generate(self, numRows: int) -> List[List[int]]:ret list()for i in range(numRows):row list()for j in range(0, i 1):if j 0 or j i:row.append(1)else:row.append(ret[i - 1][j] ret[i - 1][j - 1])ret…

HCIP-Datacom-ARST自选题库__BGP多选【22道题】

1.BGP认证可以防止非法路由器与BGP路由器建立邻居&#xff0c;BGP认证可以分为MD5认证和Keychain认证&#xff0c;请问以下哪些BGP报文会携带BCGP Keychain认证信息?(报头携带) open Update Notication Keepalive 2.传统的BGP-4只能管理IPv4单播路由信息&#xff0c;MP-B…

总线带宽(总线系统的数据传送速率)

定义 总线上每秒钟传输的最大字节数或比特数 表示方法 通常使用“比特率”来表示&#xff0c;单位为比特每秒&#xff08;bps&#xff0c;b/s&#xff09;。 计算公式 总线带宽总线宽度/传输周期 其中&#xff0c;总线宽度是指数据总线的位数&#xff08;单位&#xff1a…

GBB和Prob IoU[旋转目标检测理论篇]

在开始介绍YOLOv8_obb网络之前,需要先介绍一下arxiv.org/pdf/2106.06072 这篇文章的工作,因为v8_obb就是基于这篇论文提出的GBB和prob IoU来实现旋转目标检测的。 1.高斯分布 一维高斯分布的规律是中间高两边低,且当x为均值的时候取到最大值,表达式如下,标准正态分布图如…

数据库(10)——图形化界面工具DataGrip

以后关于数据库的图片演示就使用DataGrip了 : ) 创建数据库和表 在连接上数据库之后&#xff0c;可以选择Schema创建一个新的数据库。 点击OK后&#xff0c;就已经创建了一个空的表。 要在数据库中建立一张新的表&#xff0c;右键数据库&#xff0c;点击new table 要给新表添…

基于开源项目HAL STM32F4 +DSP库跑SVPWM开环速度测试

HAL STM32F4 ARM DSP库跑SVPWM开环速度测试 ✨本篇硬件电路和代码来源于此开源项目&#xff1a;https://github.com/MengYang-x/STM3F401-FOC/tree/main&#x1f4cd;硬件电路和项目介绍&#xff0c;立创开源广场&#xff1a;https://oshwhub.com/shadow27/tai-yang-neng-wu-re…

STL库--string

目录 string的定义 string中内存的访问 string常用函数实例解析 string的定义 定义string的方式跟基本类型相同&#xff0c;只需要在string后跟上变量名即可&#xff1a; string str; 如果要初始化&#xff0c;可以直接给string类型的变量进行赋值&#xff1a; string s…

Visual Studio 的使用

目录 1. 引言 2. 安装和配置 2.1 系统要求 2.2 安装步骤 2.3 初次配置 3. 界面介绍 3.1 菜单栏和工具栏 3.2 解决方案资源管理器 3.3 编辑器窗口 3.4 输出窗口 3.5 错误列表 3.6 属性窗口 4. 项目管理 4.1 创建新项目 4.2 导入现有项目 4.3 项目属性配置 5. 代…

stm32-DMA转运数据

在配置前要记得先定义一下DMA转运的源端数组和目标数组两个数组哦。 接下来我们就开始准备配置吧 配置 初始化 1.RCC开启时钟&#xff08;开启DMA的时钟&#xff09; void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState) 作用&#xff1a;开启时…

kafka-生产者发送消息消费者消费消息

文章目录 1、生产者发送消息&消费者消费消息1.1、获取 kafka-console-producer.sh 的帮助信息1.2、生产者发送消息到某个主题1.3、消费主题数据 1、生产者发送消息&消费者消费消息 1.1、获取 kafka-console-producer.sh 的帮助信息 [rootlocalhost ~]# kafka-console…

详解make file中的notdir

在 Makefile 中&#xff0c;$(notdir names…) 是一个函数&#xff0c;用于获取一组文件名或路径中的文件名部分&#xff0c;并将其返回。 这个函数通常用于从给定的路径中提取文件名部分&#xff0c;非常适合在 Makefile 中进行文件处理操作。 语法&#xff1a; makefile C…

基于单片机智能防触电装置的研究与设计

摘 要 &#xff1a; 针对潮湿天气下配电线路附近易发生触电事故等问题 &#xff0c; 对单片机的控制算法进行了研究 &#xff0c; 设 计 了 一 种 基 于 单片机的野外智能防触电装置。 首先建立了该装置的整体结构框架 &#xff0c; 再分别进行硬件设计和软件流程分析 &#xf…

从零开始写 Docker(十六)---容器网络实现(上):为容器插上”网线”

本文为从零开始写 Docker 系列第十六篇&#xff0c;利用 linux 下的 Veth、Bridge、iptables 等等相关技术&#xff0c;构建容器网络模型&#xff0c;为容器插上”网线“。 完整代码见&#xff1a;https://github.com/lixd/mydocker 欢迎 Star 推荐阅读以下文章对 docker 基本实…