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

 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;
}

运行结果:


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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

PDF文件提取开源工具调研总结

概述 PDF是一种日常工作中广泛使用的跨平台文档格式&#xff0c;常常包含丰富的内容&#xff1a;包括文本、图表、表格、公式、图像。在现代信息处理工作流中发挥了重要的作用&#xff0c;尤其是RAG项目中&#xff0c;通过将非结构化数据转化为结构化和可访问的信息&#xff0…

简历_使用优化的Redis自增ID策略生成分布式环境下全局唯一ID,用于用户上传数据的命名以及多种ID的生成

系列博客目录 文章目录 系列博客目录WhyRedis自增ID策略 Why 我们需要设置全局唯一ID。原因&#xff1a;当用户抢购时&#xff0c;就会生成订单并保存到tb_voucher_order这张表中&#xff0c;而订单表如果使用数据库自增ID就存在一些问题。 问题&#xff1a;id的规律性太明显、…

跨境电商使用云手机用来做什么呢?

随着跨境电商的发展&#xff0c;越来越多的卖家开始尝试使用云手机来协助他们的业务&#xff0c;这是因为云手机具有许多优势。那么&#xff0c;具体来说&#xff0c;跨境电商使用云手机可以做哪些事情呢&#xff1f; &#xff08;一&#xff09;实现多账号登录和管理 跨境电商…

计算机网络 (47)应用进程跨越网络的通信

前言 计算机网络应用进程跨越网络的通信是一个复杂而关键的过程&#xff0c;它涉及多个层面和组件的协同工作。 一、通信概述 计算机网络中的通信&#xff0c;本质上是不同主机中的应用进程之间的数据交换。为了实现这种通信&#xff0c;需要借助网络协议栈中的各层协议&#x…

Open3D 计算每个点的协方差矩阵【2025最新版】

目录 一、算法原理1、计算公式2、主要函数3、函数源码二、代码实现三、结果展示博客长期更新,本文最近更新时间为:2025年1月18日。 一、算法原理 1、计算公式 对于点云数据中的任意一点 p p p,根据其邻域内点的坐标计算其协方差矩阵。计算公式如下:

e2studio开发RA0E1(16)----配置RTC时钟及显示时间

e2studio开发RA0E1.16--配置RTC时钟及显示时间 概述视频教学样品申请完整代码下载硬件准备参考程序新建工程工程模板保存工程路径芯片配置工程模板选择时钟设置UART配置UART属性配置设置e2studio堆栈e2studio的重定向printf设置R_UARTA_Open()函数原型回调函数user_uart_callba…

Go语言strings包与字符串操作:从基础到高级的全面解析

Go语言strings包与字符串操作:从基础到高级的全面解析 引言 Go语言以其简洁、高效和强大的标准库而闻名,其中strings包是处理字符串操作的核心工具。本文将深入探讨Go语言中strings包的功能及其在实际开发中的应用,帮助开发者更好地理解和使用这一工具。 1. strings包概述…

微服务学习-快速搭建

1. 速通版 1.1. git clone 拉取项目代码&#xff0c;导入 idea 中 git clone icoolkj-microservices-code: 致力于搭建微服务架构平台 1.2. git checkout v1.0.1版本 链接地址&#xff1a;icoolkj-microservices-code 标签 - Gitee.com 2. 项目服务结构 3. 实现重点步骤 …

加密货币的基本交易技术指标

是币安交易市场的基本版视图,trading View是有更复杂的参数追踪。币安的交易的技术指标有主图和副图。有很多指标&#xff0c;让ai解释一下相关概念和意义。加密货币交易中可能遇到的主图指标及其含义&#xff1a; 1. MA&#xff08;移动平均线&#xff0c;Moving Average&…

简单介绍JSONStream的使用

地址 作用 这个模块是根据需要筛选出json数据中自己所需要的数据 使用 var JSONStream require("JSONStream"); var parse require("fast-json-parse"); var fs require("fs");fs.createReadStream("./time.json").pipe(JSONSt…

UOS扩容攻略:迁移home

原文链接&#xff1a;UOS扩容攻略&#xff1a;迁移/home Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于 UOS 扩容攻略&#xff1a;迁移 /home 目录 的文章。相信很多朋友在使用 UOS 系统时&#xff0c;会遇到系统分区空间不足&#xff0c;尤其是 /home 目录存…

RK3588平台开发系列讲解(NPU篇)NPU 驱动的组成

文章目录 一、NPU 驱动组成二、查询 NPU 驱动版本三、查询 rknn_server 版本四、查询 librknn_runtime 版本沉淀、分享、成长,让自己和他人都能有所收获!😄 一、NPU 驱动组成 NPU 驱动版本、rknn_server 版本、librknn_runtime 版本以及 RKNN Toolkit 版本的对应关系尤为重…

【实践】操作系统智能助手OS Copilot新功能测评

一、引言 数字化加速发展&#xff0c;尤其人工智能的发展速度越来越快。操作系统智能助手成为提升用户体验与操作效率的关键因素。OS Copilot借助语言模型&#xff0c;人工智能等&#xff0c;对操作系统的自然语言交互操作 推出很多功能&#xff0c;值得开发&#xff0c;尤其运…

C# OpenCvSharp 部署3D人脸重建3DDFA-V3

目录 说明 效果 模型信息 landmark.onnx net_recon.onnx net_recon_mbnet.onnx retinaface_resnet50.onnx 项目 代码 下载 参考 C# OpenCvSharp 部署3D人脸重建3DDFA-V3 说明 地址&#xff1a;https://github.com/wang-zidu/3DDFA-V3 3DDFA_V3 uses the geometri…

Linux-day08

第17章 大数据定制篇-shell编程 shell编程快速入门 shell变量 设置环境变量 把行号打开 set nu 位置参数变量 预定义变量 在一个脚本中执行了另外一个脚本所以卡住了 CTRLC退出 运算符 operator运算符 条件判断 流程控制 单分支多分支 case语句 for循环 反复的把取出来的i值…

海康工业相机的应用部署不是简简单单!?

作者&#xff1a;SkyXZ CSDN&#xff1a;SkyXZ&#xff5e;-CSDN博客 博客园&#xff1a;SkyXZ - 博客园 笔者使用的设备及环境&#xff1a;WSL2-Ubuntu22.04MV-CS016-10UC 不会吧&#xff1f;不会吧&#xff1f;不会还有人拿到海康工业相机还是一脸懵叭&#xff1f;不会还有人…

ComfyUI-PromptOptimizer:文生图提示优化节点

ComfyUI-PromptOptimizer 是 ComfyUI 的一个自定义节点&#xff0c;旨在优化文本转图像模型的提示。它将用户输入的提示转换为更详细、更多样化、更生动的描述&#xff0c;使其更适合生成高质量的图像。无需本地模型。 1、功能 提示优化&#xff1a;优化用户输入的提示以生成…

力扣 完全平方数

动态规划&#xff0c;找到前几个状态做更新。 题目 从题可看出又是一道dp&#xff0c;只要找到一个最大的平方数&#xff0c;然后往回退到上个状态&#xff0c;然后再用回退的状态加回去这个平方数即加上这一种。注意这里的所含平方数并不是随着数字变大而变大的&#xff0c;因…

使用 Java 开发 Android 应用:Kotlin 与 Java 的混合编程

使用 Java 开发 Android 应用&#xff1a;Kotlin 与 Java 的混合编程 在开发 Android 应用程序时&#xff0c;我们通常可以选择使用 Java 或 Kotlin 作为主要的编程语言。然而&#xff0c;有些开发者可能会想要在同一个项目中同时使用这两种语言&#xff0c;这就是所谓的混合编…

BeanFactory 是什么?它与 ApplicationContext 有什么区别?

谈到Spring&#xff0c;那势必要讲讲容器 BeanFactory 和 ApplicationContext。 BeanFactory是什么&#xff1f; BeanFactory&#xff0c;其实就是 Spring 容器&#xff0c;用于管理和操作 Spring 容器中的 Bean。可能此时又有初学的小伙伴会问&#xff1a;Bean 是什么&#x…