【Linux】线程全解:概念、操作、互斥与同步机制、线程池实现

news2025/1/19 15:46:56

 52bc67966cad45eda96494d9b411954d.png

🎬 个人主页:谁在夜里看海.

📖 个人专栏:《C++系列》《Linux系列》《算法系列》

⛰️ 道阻且长,行则将至


目录

📚一、线程概念

📖 回顾进程

📖 引入线程

📖 总结

📖 Linux下的线程

📚二、线程操作(phread) 

📖1.线程创建

🔖语法

🔖示例

📖2.线程退出

🔖语法

🔖示例

📖3.线程取消

🔖语法

🔖示例

📖4.线程等待

🔖语法

🔖示例

📖5.分离线程

🔖语法

🔖示例

📚三、线程互斥

📖 引入

📖 互斥量的接口

🔖1.初始化

🔖2.销毁

🔖3.加锁

🔖4.解锁

🔖示例

📖 死锁

🔖四个必要条件

🔖示例

🔖解决办法

📚四、线程同步

📖 概念

📖1.条件变量

🔖基本概念

🔖语法

 🔖示例:生产消费者模型

📖2.信号量

🔖基本概念

🔖语法

🔖示例:基于环形队列的PC-model

📚五、线程池

📖 概念

📖 实现

🔖1.初始化

🔖2.任务队列

🔖3.线程同步

🔖4.工作线程

🔖5.线程池退出

🔖总结


📚一、线程概念

线程与进程密不可分,要理解线程的概念,我们先来回顾一下进程:

📖 回顾进程

我们说,进程是操作系统资源分配的基本单位,是一个程序在计算机上的运行实例,对于这句话,我们分两点进行阐述:

① 一个进程被创建时,系统会它开辟一段虚拟地址空间,每个进程都有各自的虚拟内存空间,这保证了进程间的独立性。操作系统会为每个进程分配各种资源因此说,进程是资源分配的基本单位;

② 当程序被加载到内存并开始执行时,它就变成了一个进程。换句话说,程序是静态的文件,而进程是程序运行的动态实体

📖 引入线程

程序被加载到内存中是要执行一些操作的, 但如果系统仅支持单一的进程模型,每个程序只能顺序执行(只有一个执行流),这样在面对一些复杂任务以及高并发情况时,并不能满足需求。

此时我们可以采用多进程模型执行,但是同样也会产生一些问题:由于每个进程都需要独立的虚拟内存空间,如果大量创建进程会导致巨大的内存开销;对于一些高并发的小任务,如大量 I/O 操作,并不需要让每个操作都独占一个进程,这样太浪费资源了,那有没有什么办法,可以让这些并发操作在一个进程下执行呢

于是就引入了多线程的概念:线程是进程内的执行单元,多个线程可以在同一个进程中并发执行。由于线程共享进程的内存空间,因此相比多进程所需的内存和资源消耗更少,并且线程之间的上下文切换比进程之间的上下文切换也要更快,这使得多线程模型在I/O密集型高并发等场景下具有非常大的优势。

📖 总结

如果说进程是操作系统资源分配的基本单元,那么线程就是资源调度的基本单元,也是程序执行的基本单元。我们可以把进程看作工厂,资源就是工厂内部的原材料与各种机械设备,而线程就是工厂里的工人。

因此如果进程只有一个执行流,那它也是一个线程,称之为主线程,我们在进程中创建线程其实就是在主线程下创建其他线程,线程与进程的关系如下图:

说完了线程的概念,我们下面来说一下线程的操作。

📖 Linux下的线程

线程和进程是两个独立的概念,在现代操作系统下(例如Windows),线程和进程都是通过内核进行管理和调度,和进程一样,每个线程也有独立的执行上下文,并且可以在内核调度器中进行独立的调度。操作系统的内核负责线程的创建、销毁和调度。

然而在Linux中,线程并不是一个独立实体,而是通过轻量级进程(LWP, Light Weight Process)来实现的,Linux用进程的概念来管理线程,其主要原因有下:

Linux内核本身是设计为面向进程的管理模式,即所有的任务和执行单元都被视为“进程”,而线程的概念是在Linux设计成型之后才提出的,因此为了避免增加内核复杂度,Linux选择将线程视为特殊类型的进程(即LWP),而非引入一个全新的抽象概念。

除了通过内核直接管理线程,我们还可以使用线程库来对线程进行管理:线程库提供了用户空间的接口,能过方便地管理和操作线程;而LWP的实现方式与POSIX线程库的设计要求高度一致,因此在Linux下,我们通常使用POSIX线程库(pthread)实现对线程的创建、销毁、同步等操作

📚二、线程操作(phread) 

POSIX线程库(通常称为 pthreads,即 POSIX Threads)是基于 POSIX 标准的一组接口,用于在程序中创建和管理线程,它提供了一个标准的、跨平台的线程管理框架,广泛应用于多种操作系统中,包括 UNIX、Linux、macOS 以及一些类 Unix 系统。

pthreads 提供了以下主要功能:

📖1.线程创建

线程的创建操作通过 pthread_create() 函数完成。该函数用来创建一个新的线程,并指定其执行的函数和相关的参数。

🔖语法
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

① 参数1:thread,这是一个指向 pthread_t 类型的指针,用来返回新创建线程的标识符

❓pthread_t 类型该怎么理解

✅首先我们要清楚,虽然线程之间共享进程的资源,但是线程在被创建时,系统还是会给它分配独立的线程栈、寄存器等状态信息,以确保线程的独立执行,而 pthread_t 类型的变量则用于存储线程的唯一标识符,该标识符指向一个线程控制块(TCB),这个控制块包含了线程的执行状态、栈地址、调度信息等,通过pthread_t,操作系统能够高效地查找和管理线程的上下文。

由于线程控制块TCB存放在虚拟地址空间的共享区中,因此pthread_t实际上存放的就是一个地址,指向共享区上的一块空间。

② 参数2:attr,这是一个指向 pthread_attr_t 类型的指针,指定线程的属性。如果为NULL,则使用线程的默认属性。

③ 参数3:start_routine,这是一个函数指针,指向线程执行的函数。线程启动时将从该函数开始执行。

④ 参数4:arg,这是传递给 start_routine 函数的参数。它是一个 void* 类型的指针,可以传递任何类型的数据,在函数体内部通过强制类型转换对数据接收。

⑤ 返回值:成功时返回 0,失败时返回错误码信息。

🔖示例

这里我们创建一个线程,并循环打印线程 tid:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>

long gettid(void)
{
    return syscall(SYS_gettid);
}

void *func1(void* arg)
{
    int i = 1;
    while(i)
    {
        printf("i am pthread 1, tid is %ld\n", gettid());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    if(pthread_create(&tid, NULL, func1, NULL) != 0)
        perror("pthread_create:");
    printf("thread 1 tid is %lu\n",tid);
    int i = 1;
    while(i)
    {
        printf("i am main pthread, tid is %ld\n", gettid());
        sleep(1);
    }
    return 0;
}

运行结果:

📖2.线程退出

线程退出操作通过 pthread_exit() 完成。当线程执行完自己的任务时,通常会调用 pthread_exit() 来退出,确保线程的资源得到正确释放。

🔖语法
void pthread_exit(void *retval);

参数:retval,表示线程的返回值,主线程可以通过 pthread_join() 获取这个返回值,pthread_join()是线程等待函数,后面会讲解。

返回值:因为线程终止时无法获取自身的返回值,因此该函数没有返回值。

🔖示例
#include <pthread.h>
#include <stdio.h>

void* thread_func(void *arg) {
    printf("Thread is exiting...\n");
    pthread_exit(NULL);
}

int main() {
    pthread_t thread;
    
    // 创建线程
    pthread_create(&thread, NULL, thread_func, NULL);
    
    // 等待线程退出
    pthread_join(thread, NULL);
    
    printf("Main thread finished.\n");
    return 0;
}

📖3.线程取消

终止线程还可以通过 pthread_cancel(),pthread_cancel() 用于请求取消某个线程的执行。它是异步的,不会立即终止目标线程,而是向目标线程发送取消请求,目标线程在合适的地方检查这个请求并决定是否退出。

使用流程:

① 调用 pthread_cancel() 向指定线程发送取消请求。 

② 目标线程检查取消请求,如果设置了 取消点(即线程可以响应取消请求的地方),线程就会终止。

③ 如果线程不在取消点处,它可能会继续运行,直到它进入一个取消点。

🔖语法
int pthread_cancel(pthread_t thread);

① 参数1:thread,这是目标线程的标识符,即 pthread_create() 返回的线程ID。

② 返回值:成功时,返回 0;失败时,返回错误码(ESRCH 表示线程不存在,EINVAL 表示线程不支持取消等)。

🔖示例

取消线程的执行:

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

void* thread_func(void *arg) {
    printf("Thread started...\n");
    
    // 模拟长时间运行的任务
    for (int i = 0; i < 5; i++) {
        printf("Thread running: %d\n", i);
        sleep(1);
    }
    
    printf("Thread finished.\n");
    return NULL;
}

int main() {
    pthread_t thread;
    
    // 创建线程
    pthread_create(&thread, NULL, thread_func, NULL);
    
    // 睡眠2秒后请求取消线程
    sleep(2);
    printf("Requesting thread cancellation...\n");
    pthread_cancel(thread);
    
    // 等待线程结束
    pthread_join(thread, NULL);
    
    printf("Main thread finished.\n");
    return 0;
}

在上述示例中,主线程创建了一个子线程,该子线程执行一个循环,每秒打印一次 "Thread running"。主线程睡眠 2 秒后调用 pthread_cancel(thread) 请求取消子线程。此时子线程如果处于取消点,可能会退出。子线程完成后,主线程通过 pthread_join() 等待子线程终止。

运行结果:

📖4.线程等待

线程等待操作通过 pthread_join() 完成。调用 pthread_join() 函数可以让主线程等待子线程执行完毕。

🔖语法
int pthread_join(pthread_t thread, void **retval);

① 参数1:thread,这是要等待的线程的标识符,即 pthread_create() 返回的线程ID。

② 参数2:retval,这是一个指向 void* 类型的指针,用来接收线程的返回值(返回值类型为void* ,所以此处为void** )。如果不需要返回值,可以传递 NULL

③ 返回值:成功时,返回 0;失败时,返回错误码。 

🔖示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

void *thread1(void *arg)
{
    printf("thread1 returning...\n");
    int *p = (int*)malloc(sizeof(int));
    *p = 1;
    return (void*)p;
}

void *thread2(void *arg)
{
    printf("thread2 exiting...\n");
    int *p = (int*)malloc(sizeof(int));
    *p = 2;
    pthread_exit((void*)p);
}

void *thread3(void *arg)
{
    while(1)
    {
        printf("thread3 running...\n");
        sleep(1);
    }
    return NULL;
}

int main()
{
    pthread_t tid;
    void *ret;

    // thread1 return
    pthread_create(&tid, NULL, thread1, NULL);
    pthread_join(tid, &ret);
    printf("thread return, tid is %p, return code is %d\n", tid, *(int*)ret);
    free(ret);

    // thread2 exit
    pthread_create(&tid, NULL, thread2, NULL);  
    pthread_join(tid, &ret);
    printf("thread exit, tid is %p, return code is %d\n", tid, *(int*)ret);
    free(ret);

    // thread2 exit
    pthread_create(&tid, NULL, thread3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &ret);
    if(ret == PTHREAD_CANCELED)
        printf("thread exit, tid is %p, return code is PTHREAD_CANCELED\n", tid);
    
    return 0;
}

我们分别创建3个线程,每创建一个线程,及时对线程等待,回收资源,接着创建下一个线程,由于上一个线程的资源被回收,因此下一个线程可以复用上一个线程的资源,体现在它们的TCB地址是一样的,运行结果:

📖5.分离线程

默认情况下,新创建的线程是 joinable 的(支持被等待),线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

但如果我们并不关心线程的返回值,此时join是一种负担,这个时候我们可以告诉系统,当线程退出时自动释放线程资源,也就是将线程分离:

🔖语法

可以是线程组内其他线程对目标线程进行分离:

int pthread_detach(pthread_t thread);

① 参数:thread,这是线程标识符,即通过 pthread_create() 创建的线程ID。

② 返回值:成功时,返回 0;失败时,返回错误码。

也可以是线程自己分离: 

pthread_detach(pthread_self());

pthread_self() 函数用于获取线程自身标识符。

注意❗️

不能同时调用 pthread_detach()pthread_join():一个线程不能既是joinable又是分离的

🔖示例
#include <stdio.h>
#include <pthread.h>

void *pthread(void *arg)
{
    pthread_detach(pthread_self());
    printf("%s\n",(char*)arg);
    return NULL;
}

int main()
{
    pthread_t tid;
    pthread_create(&tid, NULL, pthread, "pthread1 running...");

    int ret = 0;
    sleep(1); // 让线程先分离,再进行等待
    if(pthread_join(tid, NULL) == 0)
    {
        printf("pthread wait success\n"); // 说明线程joinable
        ret = 0;
    }
    else
    {
        printf("pthread wait failed\n"); // 说明线程unjoinable
        ret = 1;        
    }
    return ret;
}

线程分离之后,调用 pthread_join() 等待该线程就会失败:

 

📚三、线程互斥

📖 引入

理解线程互斥之前,我们先了解两个概念:临界资源临界区,多线程执行流共享的资源,就叫做临界资源;每个线程内部,访问临界资源的代码,就叫临界区。

而线程互斥,就是在任何时刻,只能有一个执行流进入临界区,访问临界资源。为什么要这么规定呢?因为多个线程同时对临界资源访问时,可能会导致资源竞争问题,举个例子:

当多个线程同时要对变量i进行++操作时,由于i++操作在底层分为3步:① 从内存提取i到寄存器中;② 在寄存器中完成++操作;③ 将++后的值重新写入到内存中。对应三条汇编指令

① load:将共享变量 i 从内存加载到寄存器中;

② update:更新寄存器里面的值,执行+1操作;

③ store:将新值,从寄存器写回共享变量 i 的内存地址。

这就会导致一些问题,比如线程1和线程2同时将i提取到各自的寄存器中,线程1先完成了++操作并重写会内存,但是由于这一步并不会更新到线程2的寄存器中,因此虽然变量i被执行了两次++操作,但实际上只有一次。

为了更直观地展示资源竞争现象,我们模拟了一个抢票的流程:

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

int ticket = 100;
pthread_mutex_t mutex;

void *buy(void *arg)
{
    // pthread_detach(pthread_self());
    char *id = (char*)arg;
    while(1)
    {
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket: %d\n", id, ticket);
            --ticket;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, buy, "thread 1");
    pthread_create(&t2, NULL, buy, "thread 2");
    pthread_create(&t3, NULL, buy, "thread 3");
    pthread_create(&t4, NULL, buy, "thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
    // sleep(30);
    return 0;
}

这里的临界资源为总票数 ticket,我们创建了4个线程来模拟抢票过程,进入临界区对ticket进行--操作,此时会出现什么结果呢:

我们发现,ticket最终变成了负数,理论上 ticket 变为0时就应该终止了,这就是由于非原子行操作ticket--所造成的资源竞争问题。

要解决上面问题,需要做到三点:

① 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

② 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

③ 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量

📖 互斥量的接口

🔖1.初始化

初始化互斥量有两种方法:

静态分配:

 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配:

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

① 参数1:mutex,指向互斥量对象的指针,传入一个未初始化的 pthread_mutex_t 类型变量。 

② 参数2:attr,用于设置互斥量属性,通常设置为 NULL,表示使用默认的互斥量属性。

③ 返回值:成功时,返回0;失败时,返回错误码。

❗️注意:如果使用 PTHREAD_MUTEX_INITIALIZER 静态分配互斥量,就不需要调用pthread_mutex_init 函数进行初始化。

🔖2.销毁

线程在不再需要互斥量时,可以调用 pthread_mutex_destroy() 来销毁互斥量。销毁互斥量之前,必须确保没有任何线程持有该互斥量的锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

❗️注意:如果使用 PTHREAD_MUTEX_INITIALIZER 静态分配互斥量,就不需要调用pthread_mutex_destroy 函数对其销毁。

🔖3.加锁

 线程在访问共享资源之前,需要通过 pthread_mutex_lock() 获得互斥量的锁。若其他线程已持有该互斥量的锁,调用线程会被阻塞,直到互斥量被释放。

int pthread_mutex_lock(pthread_mutex_t *mutex);

① 参数:mutex,指向要锁定的互斥量对象的指针。

② 返回值:成功时,返回0;失败时,返回错误码。 

🔖4.解锁

 当线程完成对共享资源的访问后,需要释放互斥量的锁。其他线程可以在解锁后获得该锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

① 参数:mutex,指向要锁定的互斥量对象的指针。

② 返回值:成功时,返回0;失败时,返回错误码。 

🔖示例

 我们运用互斥锁来改进上面的售票模拟代码:

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

int ticket = 100;
pthread_mutex_t mutex;

void *buy(void *arg)
{
    // pthread_detach(pthread_self());
    char *id = (char*)arg;
    while(1)
    {
        pthread_mutex_lock(&mutex); // 访问临界区前加锁
        if(ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket: %d\n", id, ticket);
            --ticket;
            pthread_mutex_unlock(&mutex); // 操作完成后解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&t1, NULL, buy, "thread 1");
    pthread_create(&t2, NULL, buy, "thread 2");
    pthread_create(&t3, NULL, buy, "thread 3");
    pthread_create(&t4, NULL, buy, "thread 4");

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
    // sleep(30);
    return 0;
}

❗️注意:这里我们需要在判断 ticket 语句之前就进行加锁,而不是在++ticket前加锁,因为对ticket的判断语句也属于临界区。执行结果:

ticket在减到0时,所有线程停止访问并退出,符合预期。

📖 死锁

使用互斥锁实现线程间互斥时,如果多个线程之间在获取资源时,发生了相互依赖的情况,导致个线程在互相等待对方释放资源,从而形成了一个无限等待的状态,导致死锁的发生。

🔖四个必要条件

要发生死锁,必须满足以下四个必要条件:

① 互斥条件:一个资源只能由一个线程占用。如果有其他线程请求这个资源,那它必须等待;

② 占用且等待:一个线程至少占有了一个资源,并且等待获取被其他线程占用的资源;

③ 非抢占条件:当线程持有某个资源时,不能强行剥夺其资源,只能等待线程自己释放资源;

④ 循环等待:线程形成一个环形等待关系,其中每个线程都在等待下一个线程释放资源。

🔖示例

假设有两个互斥锁 mutex1 和 mutex2:

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

pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;

void* thread1_func(void* arg) {
    pthread_mutex_lock(&mutex1);
    printf("Thread 1: Locked mutex1\n");

    // 模拟一些工作
    sleep(1);

    pthread_mutex_lock(&mutex2);
    printf("Thread 1: Locked mutex2\n");

    pthread_mutex_unlock(&mutex2);
    pthread_mutex_unlock(&mutex1);

    return NULL;
}

void* thread2_func(void* arg) {
    pthread_mutex_lock(&mutex2);
    printf("Thread 2: Locked mutex2\n");

    // 模拟一些工作
    sleep(1);

    pthread_mutex_lock(&mutex1);
    printf("Thread 2: Locked mutex1\n");

    pthread_mutex_unlock(&mutex1);
    pthread_mutex_unlock(&mutex2);

    return NULL;
}

int main() {
    pthread_t t1, t2;

    pthread_create(&t1, NULL, thread1_func, NULL);
    pthread_create(&t2, NULL, thread2_func, NULL);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    return 0;
}

在这个例子中:

① 线程1锁住了 mutex1,然后等待 mutex2。

② 线程2锁住了 mutex2,然后等待 mutex1。 

这样,两个线程就互相等待对方释放锁,造成了死锁:

🔖解决办法

为了避免死锁,可以采取以下策略:

避免占有且等待:尝试在一个线程内一次性获取所有必需的锁,而不是按顺序逐个获取资源。这样,线程不会在持有一个资源的同时等待其他资源。 

锁的顺序:确保所有线程都按相同的顺序请求锁。这样可以避免循环等待。例如,如果线程都按 mutex1 -> mutex2 的顺序请求锁,则可以避免死锁。 

③ 死锁检测和恢复:设计死锁检测算法,定期检查是否存在死锁现象。一旦检测到死锁,可以通过中止线程、回滚或其他方法来恢复。 

④ 使用超时机制:在请求锁时,可以设置超时。如果线程在一段时间内未能获取到锁,可以放弃并重新尝试,避免无限等待。

⑤ 使用非阻塞锁:使用诸如 pthread_mutex_trylock非阻塞式锁操作,如果锁被占用,线程会立即返回,而不是等待,这样也能避免死锁。 

📚四、线程同步

📖 概念

上面讲到,线程互斥保证了任意时刻只有一个线程访问共享资源,避免了资源竞争和冲突,但是这并不能控制线程执行的顺序。因此,它适用于线程之间相对独立、不需要互相协调的场景,例如抢票系统。在这种情况下,多个线程访问共享资源(总票数)时,只需要保证同一时刻只有一个线程修改票数,谁抢到算谁的,不需要额外的线程协作。

但是线程之间有时需要相互协作,例如生产消费者模型。在这种情况下,消费者线程需要等待生产者线程生产数据,才能进行消费;换句话说,消费线程与生产线程之间具有一定的执行顺序,此时如果只用线程互斥并不能满足需求,于是就需要引入线程同步机制:

线程同步是在线程互斥的基础上,进一步控制线程的执行顺序,确保线程之间在特定的时刻能够按规定的顺序访问资源。线程同步使用于需要线程协作的场景,例如生产消费者模型、读者写者模型等等。线程同步不仅确保线程在访问共享资源时不会发生冲突,还控制了线程之间的执行顺序,保证程序逻辑的正确性

下面我们来介绍线程同步的实现方法。

📖1.条件变量

线程可以通过条件变量等待特定条件的发生,并在条件满足时被唤醒。条件变量常用于生产者消费者模型,生产者线程生产数据,消费者线程消费数据,确保它们在正确的时机执行。

🔖基本概念

条件变量(Condition Variable)允许线程在特定条件下进入等待状态,并在条件满足时被唤醒。它通常与 互斥锁 一起使用,以保证对共享资源的安全访问。

🔖语法
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_wait

让调用线程阻塞,直到被其他线程通过 pthread_cond_signalpthread_cond_broadcast 唤醒。它需要一个互斥锁作为参数,以确保在等待条件时对共享资源的访问是安全的。 

pthread_cond_signal
唤醒一个在条件变量上等待的线程。如果有多个线程在等待,系统随机选择一个线程唤醒。

pthread_cond_broadcast
唤醒所有在条件变量上等待的线程。

如何理解条件变量的工作原理? 

✅ 当线程调用 pthread_cond_wait 时,它会释放与条件变量相关联的互斥锁,并进入等待状态。当另一个线程调用 pthread_cond_signalpthread_cond_broadcast 时,等待的线程会被唤醒。唤醒后,线程会重新获取互斥锁,然后继续执行,保证了线程之间的协调,避免了数据竞争。 

 🔖示例:生产消费者模型
#include <iostream>
using namespace std;
#include <queue>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM 8

class BlockQueue{
private:
    queue<int> q;
    int cap = NUM;
    pthread_mutex_t lock;
    pthread_cond_t full;
    pthread_cond_t empty;

    void QueueLock(){
        pthread_mutex_lock(&lock);
    }
    void QueueUnlock(){
        pthread_mutex_unlock(&lock);
    }
    void ProductWait(){
        pthread_cond_wait(&full, &lock);
    }
    void ConsumeWait(){
        pthread_cond_wait(&empty, &lock);
    }
    void NotifyProcduct(){
        pthread_cond_signal(&full);
    }
    void NotifyConsume(){
        pthread_cond_signal(&empty);
    }
    bool IsEmpty()
    {
        if(q.size() == 0) return true;
        else return false;
    }
    bool IsFull()
    {
        if(q.size() == cap) return true;
        else return false;
    }

public:
    BlockQueue(){
        pthread_mutex_init(&lock, nullptr);
        pthread_cond_init(&full, nullptr);
        pthread_cond_init(&empty, nullptr);
    }
    ~BlockQueue(){
        pthread_mutex_destroy(&lock);
        pthread_cond_destroy(&full);
        pthread_cond_destroy(&empty);
    }
    void PushData(const int &data){
        QueueLock();
        while(IsFull()){
            cout << "Queue is full, product is waiting..." << endl;
            ProductWait();
        }
        q.push(data);
        NotifyConsume();
        QueueUnlock();
    }
    void PopData(int &data){
        QueueLock();
        while(IsEmpty()){
            NotifyProcduct();
            cout << "Queue is empty, consume is waiting..." << endl;
            ConsumeWait();
        }
        data = q.front();
        q.pop();
        NotifyProcduct();
        QueueUnlock();
    }
};

void *Procduct(void *arg){
    BlockQueue *q = (BlockQueue*)arg;
    srand((unsigned long)time(nullptr));
    while(1){
        int data = rand() % 1024;
        q->PushData(data);
        cout << "Produce data done: " << data << endl;
        // sleep(1);
    }
}

void *Consume(void *arg){
    BlockQueue *q = (BlockQueue*)arg;
    while(1){
        int data;
        q->PopData(data);
        cout << "Consume data done: " << data << endl;
        // sleep(1);
    }
}

int main()
{
    BlockQueue q;
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, Procduct, &q);
    pthread_create(&t2, nullptr, Consume, &q);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    return 0;
}

我们创建了一个消费者线程和一个生产者线程,共享资源为容量8的队列:

① 当队列为空时,消费线程阻塞等待生产线程生产数据,一旦生产者生产了数据,就通知唤醒消费线程消费数据;

② 当队列为满时,生产线程阻塞等待消费线程消费数据,一旦消费线程消费了数据,就通知唤醒生产线程生产数据......

部分运行结果:

📖2.信号量

上面的生产消费者模型是基于queue队列模拟实现,其空间大小是动态分配的,我们判断生产消费线程阻塞等待的情况分别是队列为满与队列为空,都可以直接用内置函数size()进行判断。但是如果数据存储的空间大小是一定的,队列为空为满就不好直接判断,此时需要借助信号量,来实时记录当前空间大小。

🔖基本概念

信号量(Semaphore)维护一个计数值,线程在访问资源前需要检查信号量的值。当信号量的值大于零时,线程可以进入临界区并访问资源;当信号量的值为零时,线程会被阻塞,直到信号量的值增加。在生产消费者模型中,信号量表示为当前剩余空间以及数据量大小。

🔖语法
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_wait
让调用线程阻塞,直到被其他线程通过 pthread_cond_signalpthread_cond_broadcast 唤醒。它需要一个互斥锁作为参数,以确保在等待条件时对共享资源的访问是安全的。

pthread_cond_signal
唤醒一个在条件变量上等待的线程。如果有多个线程在等待,系统随机选择一个线程唤醒。

pthread_cond_broadcast
唤醒所有在条件变量上等待的线程。

如何理解条件变量的工作原理?

✅ 当线程调用 pthread_cond_wait 时,它会释放与条件变量相关联的互斥锁,并进入等待状态。当另一个线程调用 pthread_cond_signalpthread_cond_broadcast 时,等待的线程会被唤醒。唤醒后,线程会重新获取互斥锁,然后继续执行,同样保证了线程之间的协调,避免了数据竞争。

❗️注意:信号量的操作( sem_wait()sem_post())在实现时是由操作系统提供的原子操作。也就是说,信号量的计数值是通过原子操作进行修改的,这些操作是不可中断的,保证在多线程环境下操作信号量时不会出现竞争,因此在使用信号量时不需要用互斥锁来保护对信号量的修改

🔖示例:基于环形队列的PC-model
#include <iostream>
using namespace std;
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>
#include <pthread.h>
#include <vector>

#define NUM 8

class RingQueue{
private:
    vector<int> q;
    int cap = NUM;
    sem_t data_sem;
    sem_t space_sem;
    int product_step;
    int consume_step;

public:
    RingQueue(){
        q.resize(cap);
        sem_init(&data_sem, 0, 0);
        sem_init(&space_sem, 0, cap);
        product_step = 0;
        consume_step = 0;
    }
    ~RingQueue(){
        sem_destroy(&data_sem);
        sem_destroy(&space_sem);
    }
    void PushData(const int &data){
        sem_wait(&space_sem);
        q[product_step] = data;
        ++product_step;
        product_step %= cap;
        sem_post(&data_sem);
    }
    void PopData(int &data){
        sem_wait(&data_sem);
        data = q[consume_step];
        ++consume_step;
        consume_step %= cap;
        sem_post(&space_sem);
    }
};

void *Produce(void *arg){
    RingQueue *q = (RingQueue*)arg;
    srand((unsigned long)time(nullptr));
    while(1){
        int data = rand() % 1024;
        q->PushData(data);
        cout << "Produce data done: " << data << endl;
        // sleep(1);
    }
}
void *Consume(void *arg){
    RingQueue *q = (RingQueue*)arg;
    while(1){
        int data;
        q->PopData(data);
        cout << "Consume data done: " << data << endl;
        // sleep(1);
    }
}

int main()
{
    RingQueue q;
    pthread_t t1, t2;

    pthread_create(&t1, nullptr, Produce, &q);
    pthread_create(&t2, nullptr, Consume, &q);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    return 0;
}

创建了一个消费者线程和一个生产者线程,共享源为容量8的环形队列:

① 当数据信号量为0时,表示当前没有数据,消费线程阻塞等待,生产线程生产数据之后,增加数据信号量,此时消费线程会被唤醒;

② 当空间信号量为0时,表示当前没有空间,生产线程阻塞等待,消费线程消费数据之后,增加空间信号量,此时生产线程会被唤醒......

部分运行结果:

📚五、线程池

相比于多进程,多线程大大减少了空间开辟和释放的开销,但并不意味着多线程的创建和销毁的开销可以忽略不计,在面对超高并发情况,线程的频繁创建和销毁还是会带来巨大的开销,如果线程一次性创建过多,甚至会导致资源耗尽,系统崩溃的情况,如春运期间铁路12306的访问量非常大,频繁创建和销毁线程的开销会变得十分严重。为了应对这种情况,便引入了线程池技术

📖 概念

线程池是一种线程管理机制,它通过维护一个预先创建的线程集合(线程池),来避免频繁地创建和销毁线程所带来的性能开销。线程池管理一定数量的工作线程,将任务提交给空闲线程来执行,执行完成后线程会返回池中等待下一个任务。这种方式可以提高系统性能,尤其在高并发场景下,通过复用线程来减少线程创建和销毁的成本。

线程池通常管理着以下几个重要组件:

① 线程池中的工作线程:执行实际的任务

② 任务队列:存放等待执行的任务

③ 核心线程数:线程池中始终保持活跃的线程数量

④ 核心线程数:线程池中始终保持活跃的线程数量

⑤ 线程池大小的动态调整:根据负载和任务量,线程池可能会动态增减线程数量

📖 实现

下面介绍简易线程池的具体实现:

🔖1.初始化

在创建线程池时,线程池会创建一定数量的线程,这些线程将用来执行任务。线程池的初始化通过 PoolInit() 函数完成。

bool PoolInit() {
    pthread_t tid;
    for (int i = 0; i < _thread_max; ++i) {
        int ret = pthread_create(&tid, NULL, thr_start, this);
        if (ret != 0) {
            cout << "create pool thread error" << endl;
            return false;
        }
    }
    return true;
}

① PoolInit():初始化线程池,创建指定数量的工作线程(_thread_max)。每个线程执行 thr_start() 函数。

② pthread_create():用来创建线程,每个线程将运行 thr_start 函数。thr_start 会一直循环地从任务队列中取出任务并执行,直到线程池退出。 

🔖2.任务队列

任务队列是线程池中的关键组件之一。它用于存储待处理的任务,工作线程从队列中取出任务并执行。

queue<ThreadTask *> _task_queue;

这里使用了 std::queue 来实现任务队列。ThreadTask 是任务对象,每个任务都包含一个待处理的数据和对应的处理函数;当任务被推送到队列时,工作线程会从队列中弹出并执行。

队列相关操作:① PushTask:将任务加入队列;② PopTask:从队列中弹出一个任务。

bool PushTask(ThreadTask *tt) {
    LockQueue();
    if (_tp_isquit) {
        UnLockQueue();
        return false;
    }
    _task_queue.push(tt);
    WakeUpOne();
    UnLockQueue();
    return true;
}

bool PopTask(ThreadTask **tt) {
    *tt = _task_queue.front();
    _task_queue.pop();
    return true;
}
🔖3.线程同步

线程池中的多个线程需要访问共享资源(即任务队列),因此需要使用 互斥锁 来保护共享资源的访问。条件变量 用于线程间的同步,控制线程何时等待,何时被唤醒。 

pthread_mutex_t _lock;
pthread_cond_t _cond;

① _lock:互斥锁,用来保护任务队列 _task_queue,避免多个线程同时修改队列;

② _cond:条件变量,用来通知线程池中的工作线程何时可以执行任务。

线程同步操作:

① LockQueueUnLockQueue:用于加锁和解锁任务队列,确保对任务队列的访问是线程安全的

② WakeUpOneWakeUpAll:分别唤醒一个等待的线程或所有等待的线程

③ ThreadWait:让当前线程阻塞,直到任务队列中有任务或线程池退出

void LockQueue() {
    pthread_mutex_lock(&_lock);
}

void UnLockQueue() {
    pthread_mutex_unlock(&_lock);
}

void WakeUpOne() {
    pthread_cond_signal(&_cond);
}

void WakeUpAll() {
    pthread_cond_broadcast(&_cond);
}

void ThreadWait() {
    if (_tp_isquit) {
        ThreadQuit();
    }
    pthread_cond_wait(&_cond, &_lock);
}
🔖4.工作线程

每个工作线程都执行 thr_start 函数,该函数包含一个无限循环,不断从任务队列中取出任务并执行。直到线程池退出。 

static void *thr_start(void *arg) {
    ThreadPool *tp = (ThreadPool*)arg;
    while (1) {
        tp->LockQueue();
        while (tp->IsEmpty()) {
            tp->ThreadWait();
        }
        ThreadTask *tt;
        tp->PopTask(&tt);
        tp->UnLockQueue();
        tt->Run();
        delete tt;
    }
    return NULL;
}

① 每个工作线程在 thr_start 中会一直等待任务;

② 如果任务队列为空,线程会通过 ThreadWait 阻塞,直到有任务被推送到队列中;

③ 任务一旦加入队列,线程会被唤醒,并执行任务;

④ 行完任务后,线程会回到等待状态,直到线程池退出。 

🔖5.线程池退出

当线程池不再接受新任务并且所有任务执行完毕时,线程池需要退出。线程池的退出通过 PoolQuit 函数来完成。 

bool PoolQuit() {
    LockQueue();
    _tp_isquit = true;
    UnLockQueue();
    while (_thread_cur > 0) {
        WakeUpAll();
        usleep(1000);
    }
    return true;
}

① PoolQuit:标志 _tp_isquit 被设置为 true,然后唤醒所有线程并让它们检查退出条件;

② 工作线程在执行 ThreadWait 时会检查 _tp_isquit,如果为 true,则退出。

🔖总结

1.线程池的初始化:线程池通过 PoolInit 创建多个工作线程,这些线程通过执行 thr_start 来不断地获取并处理任务。

2.任务队列:任务队列是线程池的重要组成部分,用于存储待处理的任务,任务由主线程推送到队列,工作线程从队列中取出并执行。

3.线程同步机制:互斥锁和条件变量用于确保线程安全地访问任务队列,并协调工作线程的等待和唤醒。

4.工作线程执行任务:每个工作线程都从队列中获取任务,并执行任务,直到线程池退出。

5.线程池退出:当线程池不再接收任务时,调用 PoolQuit 函数退出线程池,并通知所有工作线程退出。

完整代码如下:

// threadpool.hpp
#ifndef __M_TP_H__
#define __M_TP_H__
#include <iostream>
using namespace std;
#include <queue>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define MAX_THREAD 5
typedef bool (*hander_t)(int);
class ThreadTask
{
private:
    int _data;
    hander_t _handler;
public:
    ThreadTask():_data(-1), _handler(NULL){}
    ThreadTask(int data, hander_t handler){
        _data = data;
        _handler = handler;
    }
    void SetTask(int data, hander_t handler){
        _data = data;
        _handler = handler;        
    }
    void Run(){
        _handler(_data);
    }
};

class ThreadPool
{
private:
    int _thread_max;
    int _thread_cur;
    bool _tp_isquit;
    queue<ThreadTask *> _task_queue;
    pthread_mutex_t _lock;
    pthread_cond_t _cond;
private:
    void LockQueue(){
        pthread_mutex_lock(&_lock);
    }
    void UnLockQueue(){
        pthread_mutex_unlock(&_lock);
    }
    void WakeUpOne(){
        pthread_cond_signal(&_cond);
    }
    void WakeUpAll(){
        pthread_cond_broadcast(&_cond);
    }
    void ThreadQuit(){
        _thread_cur--;
        UnLockQueue();
        pthread_exit(NULL);
    }
    void ThreadWait(){
        if(_tp_isquit){
            ThreadQuit();
        }
        pthread_cond_wait(&_cond, &_lock);
    }
    bool IsEmpty(){
        return _task_queue.empty();
    }
    static void *thr_start(void *arg){
        ThreadPool *tp = (ThreadPool*)arg;
        while(1){
            tp->LockQueue();
            while(tp->IsEmpty()){
                tp->ThreadWait();
            }
            ThreadTask *tt;
            tp->PopTask(&tt);
            tp->UnLockQueue();
            tt->Run();
            delete tt;
        }
        return NULL;
    }
public:
    ThreadPool(int max=MAX_THREAD):_thread_max(max), _thread_cur(max),
    _tp_isquit(false){
        pthread_mutex_init(&_lock, NULL);
        pthread_cond_init(&_cond, NULL);
    }
    ~ThreadPool(){
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }
    bool PoolInit(){
        pthread_t tid;
        for(int i = 0; i < _thread_max; ++i){
            int ret = pthread_create(&tid, NULL, thr_start, this);
            if(ret != 0){
                cout << "create pool thread error" << endl;
                return false;
            }
        }
        return true;
    }
    bool PushTask(ThreadTask *tt){
        LockQueue();
        if(_tp_isquit){
            UnLockQueue();
            return false;
        }
        _task_queue.push(tt);
        WakeUpOne();
        UnLockQueue();
        return true;
    }
    bool PopTask(ThreadTask **tt){
        *tt = _task_queue.front();
        _task_queue.pop();
        return true;
    }
    bool PoolQuit(){
        LockQueue();
        _tp_isquit = true;
        UnLockQueue();
        while(_thread_cur > 0){
            WakeUpAll();
            usleep(1000);
        }
        return true;
    }
};
#endif
// main.cpp
#include "threadpool.hpp"

bool handler(int data)
{
    srand(time(NULL));
    int n = rand() % 5;
    printf("Thread: %p Run Tast: %d--sleep %d sec\n", pthread_self(), data, n);
    sleep(n);
    return true;
}

int main()
{
    ThreadPool pool;
    pool.PoolInit();
    for(int i = 0; i < 10; ++i){
        ThreadTask *tt = new ThreadTask(i, handler);
        pool.PushTask(tt);
    }
    pool.PoolQuit();
    return 0;
}

运行结果:


以上就是【深入理解线程:概念、操作、互斥与同步机制、线程池实现】的全部内容,欢迎指正~ 

码文不易,还请多多关注支持,这是我持续创作的最大动力!  

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

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

相关文章

掌握未来:从零开始学习生成式AI大模型!

随着人工智能技术的飞速发展&#xff0c;生成式AI大模型已成为当今科技领域的热点。从文本生成、图像创作到音乐创作&#xff0c;生成式AI大模型在各个领域展现出惊人的潜力。本文将带领大家从零开始&#xff0c;逐步学习生成式AI大模型&#xff0c;掌握未来的关键技术。 一、生…

多肽合成 -- 液相合成(liquid-phase peptide synthesis (LPPS))

液相合成的定义 液相合成&#xff08;Solution Synthesis&#xff09;是指在液体介质中进行的化学合成反应&#xff0c;是化学合成中一种基本且重要的方法。它涉及到将反应物溶解在溶剂中&#xff0c;在一定的温度、压力和其他反应条件下进行化学反应&#xff0c;从而生成所需的…

第23篇 基于ARM A9处理器用汇编语言实现中断<五>

Q&#xff1a;怎样修改HPS Timer 0定时器产生的中断周期&#xff1f; A&#xff1a;在上一期实验的基础上&#xff0c;可以修改按键中断服务程序&#xff0c;实现红色LED上的计数值递增的速率&#xff0c;主程序和其余代码文件不用修改。 实现以下功能&#xff1a;按下KEY0…

ChatGPT大模型极简应用开发-CH1-初识 GPT-4 和 ChatGPT

文章目录 1.1 LLM 概述1.1.1 语言模型和NLP基础1.1.2 Transformer及在LLM中的作用1.1.3 解密 GPT 模型的标记化和预测步骤 1.2 GPT 模型简史&#xff1a;从 GPT-1 到 GPT-41.2.1 GPT11.2.2 GPT21.2.3 GPT-31.2.4 从 GPT-3 到 InstructGPT1.2.5 GPT-3.5、Codex 和 ChatGPT1.2.6 …

2025春秋杯冬季赛 day1 crypto

文章目录 通往哈希的旅程小哈斯RSA1ez_rsa 通往哈希的旅程 根据提示推断是哈希函数&#xff0c;ai一下&#xff0c;推测大概率是一个sha1&#xff0c;让ai写一个爆破脚本即可 import hashlib# 给定目标 SHA-1 哈希值 target_hash "ca12fd8250972ec363a16593356abb1f3cf…

广播网络实验

1 实验内容 1、构建星性拓扑下的广播网络,实现hub各端口的数据广播,验证网络的连通性并测试网络效率 2、构建环形拓扑网络,验证该拓扑下结点广播会产生数据包环路 2 实验流程与结果分析 2.1 实验环境 ubuntu、mininet、xterm、wireshark、iperf 2.2 实验方案与结果分析…

RustDesk ID更新脚本

RustDesk ID更新脚本 此PowerShell脚本自动更新RustDesk ID和密码&#xff0c;并将信息安全地存储在Bitwarden中。 特点 使用以下选项更新RustDesk ID&#xff1a; 使用系统主机名生成一个随机的9位数输入自定义值 为RustDesk生成新的随机密码将RustDesk ID和密码安全地存储…

JavaEE之常见的锁策略

前面我们学习过线程不安全问题&#xff0c;我们通过给代码加锁来解决线程不安全问题&#xff0c;在生活中我们也知道有很多种类型的锁&#xff0c;同时在代码的世界当中&#xff0c;也对应着很多类型的锁&#xff0c;今天我们对锁一探究竟&#xff01; 1. 常见的锁策略 注意: …

数字图像处理:实验二

任务一&#xff1a; 将不同像素&#xff08;32、64和256&#xff09;的原图像放大为像素大 小为1024*1024的图像&#xff08;图像自选&#xff09; 要求&#xff1a;1&#xff09;输出一幅图&#xff0c;该图包含六幅子图&#xff0c;第一排是原图&#xff0c;第 二排是对应放大…

WEB渗透技术研究与安全防御

目录 作品简介I IntroductionII 1 网络面临的主要威胁1 1.1 技术安全1 2 分析Web渗透技术2 2.1 Web渗透技术的概念2 2.2 Web漏洞产生的原因2 2.3 注入测试3 2.3.1 注入测试的攻击流程3 2.3.2 进行一次完整的Sql注入测试4 2.3.3 Cookie注入攻击11 3 安全防御方案设计…

使用 Thermal Desktop 进行航天器热分析

介绍 将航天器保持在运行温度下的轨道上是一个具有挑战性的问题。航天器需要处理太空非常寒冷的背景温度&#xff0c;同时还要管理来自内部组件、地球反照率和太阳辐射的高热负荷。航天器在轨道上可以进行的各种轨道机动使解决这个问题变得更加复杂。 Thermal Desktop 是一款…

乘联会:1月汽车零售预计175万辆 环比暴跌33.6%

快科技1月18日消息&#xff0c;据乘联会的初步推算&#xff0c;2025年1月狭义乘用车零售总市场规模预计将达到约175万辆左右。与去年同期相比&#xff0c;这一数据呈现了-14.6%的同比下降态势&#xff1b;而相较于上个月&#xff0c;则出现了-33.6%的环比暴跌情况。 为了更清晰…

SQL 递归 ---- WITH RECURSIVE 的用法

SQL 递归 ---- WITH RECURSIVE 的用法 开发中遇到了一个需求&#xff0c;传递一个父类id&#xff0c;获取父类的信息&#xff0c;同时获取其所有子类的信息。 首先想到的是通过程序中去递归查&#xff0c;但这种方法着实孬了一点&#xff0c;于是想&#xff0c;sql能不能递归查…

【机器学习实战入门项目】使用深度学习创建您自己的表情符号

深度学习项目入门——让你更接近数据科学的梦想 表情符号或头像是表示非语言暗示的方式。这些暗示已成为在线聊天、产品评论、品牌情感等的重要组成部分。这也促使数据科学领域越来越多的研究致力于表情驱动的故事讲述。 随着计算机视觉和深度学习的进步&#xff0c;现在可以…

windows 搭建flutter环境,开发windows程序

环境安装配置&#xff1a; 下载flutter sdk https://docs.flutter.dev/get-started/install/windows 下载到本地后&#xff0c;随便找个地方解压&#xff0c;然后配置下系统环境变量 编译windows程序本地需要安装vs2019或更新的开发环境 主要就这2步安装后就可以了&#xff0…

【Linux】15.Linux进程概念(4)

文章目录 程序地址空间前景回顾C语言空间布局图&#xff1a;代码1代码2代码3代码4代码5代码6代码7 程序地址空间前景回顾 历史核心问题&#xff1a; pid_t id fork(); if(id 0) else if(id>0) 为什么一个id可以放两个值呢&#xff1f;之前没有仔细讲。 C语言空间布局图&am…

一文读懂服务器的HBA卡

什么是 HBA 卡 HBA 卡&#xff0c;全称主机总线适配器&#xff08;Host Bus Adapter&#xff09; &#xff0c;是服务器与存储装置间的关键纽带&#xff0c;承担着输入 / 输出&#xff08;I/O&#xff09;处理及物理连接的重任。作为一种电路板或集成电路适配器&#xff0c;HBA…

oracle使用case when报错ORA-12704字符集不匹配原因分析及解决方法

问题概述 使用oracle的case when函数时&#xff0c;报错提示ORA-12704字符集不匹配&#xff0c;如下图&#xff0c;接下来分析报错原因并提出解决方法。 样例演示 现在有一个TESTTABLE表&#xff0c;本表包含的字段如下图所示&#xff0c;COL01字段是NVARCHAR2类型&#xff0…

Linux-----线程同步(条件变量)

目录 相关API restrict关键字 线程间条件切换函数 条件变量pthread_cond_t 案例 在前面的锁的基础上进一步提高线程同步效率&#xff0c;也就是两个线程只用锁去执行的话依然会存在资源竞争的情况&#xff0c;也就是抢锁&#xff0c;这里就需要在锁的这边加上限制&#xf…

每日进步一点点(网安)

今日练习题目是PHP反序列化&#xff0c;也学习一下说明是序列化和反序列化 1.PHP序列化 序列化是指将数据结构或对象转换为可传输或可储存的格式的过程。这通常需要将数据转换为字节流或者其他编码格式&#xff0c;以便在不同系统和应用程序之间进行传输或存储 在PHP中&…