当前位置: 首页 > news >正文

线程(四)线程的同步——条件变量

文章目录

  • 线程
    • 线程的同步和互斥
      • 线程同步--条件变量
        • 什么是线程同步
        • 示例--条件变量的使用
        • 示例--使用两个线程对同一个文件进行读写
        • 示例--一个读者一个写者使用条件变量来实现同步

线程

线程的同步和互斥

线程同步–条件变量

  • 是一个宏观概念,在微观上包含线程的相互排斥和线程先后执行的约束问题
  • 解决同步方式
    • 条件变量
    • 线程信号量
什么是线程同步

举个例子:假如现在要做一款产品,在产品发行之前要进行研发和测试两个环节。现在将研发和测试看成两个线程,将产品看作两个线程操作的共享资源。对产品的测试只能是当产品研发完成以后才能进行测试,这里其实就是涉及到了线程的互斥和同步,两个线程同一时间之间只能有一个线程进行操作,而测试又只能基于研发线程操作以后的结果才能进行测试,所以这里其实包含线程先后执行的约束问题。如果这里单单是线程的互斥,那么只能保证一个同一时间内只能有一个线程执行,然后另外一个线程可以继续执行,线程的互斥并不强调线程的先后执行顺序,但是线程的同步建立在线程互斥的基础上还要注重线程先后执行的约束问题,谁先执行,谁后执行,哪个线程依赖哪一个线程执行的结果,在这个结果上继续操作,这是线程同步关心的问题。

线程同步–条件变量

  • 互斥锁的缺点是它只有两种状态:锁定和非锁定

  • 条件变量通过允许线程阻塞和等待另外一个线程发送信号的方法弥补了互斥锁的不足。

  • 条件变量内部是一个等待队列,放值等待的线程,线程在条件变量上等待和通知,互斥锁用来保护等待队列(对等待队列进行上锁),条件变量通常和互斥锁一起使用

    关于上边案例中的同步可以这样做:首先将测试线程放到等待队列中,此时由于产品没有完成不满足条件所以线程会处于阻塞状态。当研发线程将产品开发好以后,然后给等待队列中的线程发送信号,将测试线程唤醒然后将测试线程从等待队列中删除,然后由测试线程对产品进行操作。这里有一点需要注意:既然每一个线程都可以通过系统调用将线程本身放入到等待队列中进入等待,也就是说这个等待队列也是所有的线程都可以操作的共享资源。那么对共享资源的操作就要涉及到线程安全的问题,所以这里在操作等待队列的时候要使用互斥锁对共享资源进行保护。

  • 条件变量允许线程等待特定条件发生,当条件不满足时,线程通常先进入阻塞状态,等待条件发生变化。一旦其他的某个线程改变了条件,可唤醒一个或者多个阻塞的线程。

  • 具体的判断条件还需用户给出

  • 条件变量的数据类型

    • pthread_cond_t

条件变量的初始化和销毁

#include <pthread.h>int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);/*
功能:pthread_cond_init	对条件变量进行初始化pthread_cond_destroy	销毁一个已经初始化的条件变量,释放其占用资源参数:pthread_cond_t *cond		指向要初始化的条件变量指针pthread_condattr_t *attr	指向条件变量属性对象指针,一般设置为NULL表示默认属性返回值:成功执行返回0,失败返回错误码
*/

条件变量等待操作

#include <pthread.h>int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, struct timespec *timeout);/*
功能:pthread_cond_wait	阻塞当前线程,直到另外一个线程通过调用pthread_cond_signal或者pthread_cond_broadcast唤醒它pthread_cond_timewait		作用与pthread_cond_wait类似,但允许指定一个超时时间,如果条件变量在超时时间内没有被信号量唤醒,则线程会自动解除阻塞状态互斥锁的作用是对条件变量的保护参数:pthread_cond_t *cond		指向要等待其改变的条件变量的指针pthread_mutex_t *mutex		指向与条件变量关联的互斥锁的指针。在调用pthread_cond_wait之前,线程必须已经锁定这个互斥锁struct timespec *timeout	指向一个timespec结构体,表示绝对时间点。如果在这个时间点之前条件变量没有被信号量唤醒,则函数返回返回值:成功时返回0;失败时返回错误码。如果线程被条件变量唤醒,则返回值大于0。如果因为超时而返回,则返回值等于0或ETIMEDOUT。其他错误情况下返回相应的错误码。
*/ 

条件变量通知操作

#include <pthread.h>int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_boardcast(pthread_cond_t *cond);/*功能:pthread_cond_signal	当条件满足的时候通知单个线程pthread_cond_boardcast		当条件满足的时候通知在等待队列中所有的线程当在一个等待队列中有若干个等待线程时,由于pthread_cond_signal函数只能够唤醒一个线程,而等待队列中的线程又遵循系统调度,所以不知道唤醒的线程是否是你需要的线程,所以当等待队列中的线程较多时直接使用pthread_cond_broadcast唤醒所有的线程参数:pthread_cond_t *cond		指向条件变量的指针返回值:成功执行返回0,否则返回错误码
*/
示例–条件变量的使用
#include "header.h"typedef struct
{int retult;int is_wait;pthread_mutex_t mutex;      //定义互斥锁类型pthread_cond_t cond;         //定义条件变量类型
}Result;void* cal_func(void *arg)
{Result *r = (Result*)arg;int i, sum = 0;//计算1-100的和然后存放到结构体中for(i = 1; i <= 100; i++)sum += i;       r->retult = sum;printf("[cal thread id]: %lx\n",pthread_self());pthread_mutex_lock(&r->mutex);while(!r->is_wait){//如果进入到这个循环中就说明等待线程还没有准备好,这时候就要释放互斥锁给等待的//线程有机会拿到互斥锁然后对共享资源进行修改,下次判断等待线程准备好就可以给等待//线程发送信号唤醒,让等待线程拿到计算结果并打印出来pthread_mutex_unlock(&r->mutex);    usleep(100);pthread_mutex_lock(&r->mutex);}pthread_mutex_unlock(&r->mutex);        //释放锁,上锁和释放锁是一一对应的pthread_cond_broadcast(&r->cond);       //唤醒所有在等待队列中的线程pthread_exit(NULL);
}void* get_func(void *arg)
{Result *r = (Result*)arg;//加锁,对共享资源进行保护pthread_mutex_lock(&r->mutex);r->is_wait = 1;pthread_cond_wait(&r->cond, &r->mutex);     //这里传互斥锁进入是为了保证等待队列这个共享资源的安全,实际上在内部做了多次加锁释放锁的操作pthread_mutex_unlock(&r->mutex);//通过阻塞等待计算线程将结果存放到结构体中,然后被另外一个线程使用broadcast唤醒printf("[get thread id:%lx] sum = %d\n",pthread_self(),r->retult);pthread_exit(NULL);
}int main(void)
{   int err = -1;pthread_t get,cal;Result r;memset(&r,'\0',sizeof(r));r.is_wait = 0;pthread_mutex_init(&r.mutex, NULL);      //以默认属性创建互斥锁pthread_cond_init(&r.cond, NULL);        //以默认属性创建条件变量//创建获取结果线程,计算结果从结构体中获取并打印出来if((err = pthread_create(&get, NULL, get_func, (void*)&r)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}//创建计算结果线程,然后将结果存放到结构体中if((err = pthread_create(&cal, NULL, cal_func, (void*)&r)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}//等待子线程退出pthread_join(get, NULL);pthread_join(cal, NULL);pthread_mutex_destroy(&r.mutex);      //销毁互斥锁pthread_cond_destroy(&r.cond);         //销毁条件变量return 0;
}

image-20240923180927384

通过编译执行可以看到获取结果的线程拿到了计算线程的结果,这就是线程的同步。这里有两点比较重要:

  1. 首先是is_wait变量的使用,上边这个代码中的is_wait的这个比较难以理解,详细解释一下:当cal的线程将计算结果存放到结构体后要确保get线程已经准备好了。is_wait这个变量就是用来检测get线程是否已经准备好了,由于is_wait属于一个共享资源,所以在操作的时候要使用互斥锁来进行保护共享资源的安全。在cal线程中判断is_wait的值来确保get线程已经准备好了,准备好以后就使用pthread_cond_broadcast唤醒get线程。而由于线程之间的执行顺序没办法保证,所以当cal线程先拿到锁以后检测到get线程还没有准备好的时候就要去释放锁,如果不释放锁,get线程在拿锁的时候就会阻塞,导致它一直修改不了is_wait的值,从而导致get线程陷入一个死循环。所以这里在判断get线程没有准备好的时候要立即释放锁,否则会造成死循环。在延时过后还要进行上锁的原因是可能这时候get线程还没有准备好还需要上锁进行保护。在退出循环后要释放锁,保证加锁释放锁是成对出现的。

  2. 关于pthread_cond_wait()函数内部的实现机制并不是表面看到的调用阻塞线程,等待另一个线程去唤醒它。在它内部实际上有这些步骤:

    1. pthread_cong_wait函数之前已经上锁,所以在函数内部首先会释放锁,来保证其他线程能够拿到锁对共享资源进行操作。
    2. 然后再获取互斥锁来保证等待队列的操作,由于等待队列也属于共享资源,所以对它的操作也要加互斥锁,加锁以后将当前线程加入到等待队列中去。
    3. 然后释放锁,等待另外的线程调用pthread_cond_signalpthread_cond_broadcast将当前线程唤醒。
    4. 当线程被唤醒后,此时在函数内部又会获取到互斥锁,这时候获取互斥锁的作用是将线程从等待队列中删除。
    5. 最后去执行调用pthread_cond_wait()这个线程内部的代码功能。

    所以程序中的第一句上锁和最后一句的释放锁并不是对应的,而是和pthread_cond_wait()函数内部的加锁释放锁对应的。

示例–使用两个线程对同一个文件进行读写
#include "header.h"typedef struct
{int fd;						//用于获取主线程打开的文件描述符int write_done;				//用于检测是否已经写入文件char str[32];				//用于写入文件的字符串char filename[12];			//用于存储文件名字的字符串pthread_mutex_t mutex;		//创建互斥锁类型pthread_cond_t cond;		//创建条件变量类型
}OperArg;void* read_func(void *arg)
{OperArg *r = (OperArg*)arg;char buffer[32];memset(buffer, '\0', sizeof(buffer));pthread_mutex_lock(&r->mutex);	//对共享资源进行保护while(!r->write_done){pthread_cond_wait(&r->cond, &r->mutex);		//等待另外的线程唤醒,否则阻塞}//从文件中读取lseek(r->fd, 0, SEEK_SET);		//由于写入的线程将文件指针偏移到末尾,所以读取的时候要重新指向if(read(r->fd, buffer, 32) < 0){pthread_mutex_unlock(&r->mutex);perror("read error");exit(EXIT_FAILURE);}printf("[read thread id:%lx] successfully read [%s] from the [%s]\n",pthread_self(),buffer,r->filename);pthread_mutex_unlock(&r->mutex);pthread_exit(NULL);
}void* write_func(void *arg)
{OperArg *r = (OperArg*)arg;int length = strlen(r->str);pthread_mutex_lock(&r->mutex);		//加锁对共享资源进行保护if(write(r->fd, r->str, length) != length){pthread_mutex_unlock(&r->mutex);		//程序异常退出释放锁防止造成死锁perror("write error");exit(EXIT_FAILURE);}printf("[write thread id:%lx] successfully write [%s] to the [%s]\n",pthread_self(),r->str,r->filename);r->write_done = 1;		//表明已经成功向文件写入pthread_cond_broadcast(&r->cond);		//文件写入以后就可以通知读取线程进行读取了pthread_mutex_unlock(&r->mutex);		//释放互斥锁给另外的线程拿到锁pthread_exit(NULL);
}int main(int argc, char **argv)
{	if(argc < 3){fprintf(stderr,"usage: %s [filepath] [string]\n",argv[0]);exit(EXIT_FAILURE);}int fd = -1;int err = -1;pthread_t read, write;OperArg r;memset(&r, '\0', sizeof(r));//以文件的拥有者、同组人有可读可写可执行的权限打开文件,如果文件不存在就创建文件fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, S_IRWXU | S_IRWXG);if(fd < 0){perror("open file error");exit(EXIT_FAILURE);}r.fd = fd;r.write_done = 0;						//表示还未将字符串写入到文件strcpy(r.str, argv[2]);					//将从命令行传入的字符串赋值给strstrcpy(r.filename, argv[1]);			//将文件名传给结构体pthread_mutex_init(&r.mutex, NULL);		//初始化互斥锁pthread_cond_init(&r.cond, NULL);		//初始化条件变量//创建从文件中读取的线程if((err = pthread_create(&read, NULL, read_func, (void*)&r)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}	//创建向文件写入的线程if((err = pthread_create(&write, NULL, write_func, (void*)&r)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}//等待子线程退出并回收其资源pthread_join(read, NULL);pthread_join(write, NULL);//销毁互斥锁和条件变量pthread_mutex_destroy(&r.mutex);pthread_cond_destroy(&r.cond);close(fd);return 0;
}

image-20240925180357429

通过编译执行可以看到成功创建文件并且对文件进行写入和读取,在这个代码中要write_func线程先对文件进行写入后read_func线程才能读取,若两个线程的执行顺序颠倒,read()函数若在读取普通文件时若文件为空,那么read()函数就什么都没有读到。注意这里的read()函数不会被阻塞直到write()写入,因为read()函数只有在读取管道文件和套接字文件的时候才会阻塞,所以这里的read()函数在文件为空或者读到文件末尾的时候会直接返回。这里对文件读取就是使用条件变量,当文件未写入的时候read_func线程调用pthread_cond_wait()函数一直阻塞,直到write_func()线程将内容写入到文件里后才调用pthread_cond_broadcast()函数将其唤醒后才能读取。

示例–一个读者一个写者使用条件变量来实现同步
#include "header.h"typedef struct
{int value;int rd_wait;							//判断读者线程运行条件int wr_wait;							//判断写者线程运行条件pthread_mutex_t rd_mutex;				pthread_mutex_t wr_mutex;					pthread_cond_t rd_cond;pthread_cond_t wr_cond;					//定义两种互斥锁和条件变量
}Storage;void* read_func(void *arg)
{Storage *s = (Storage*)arg;int i = 1;for(; i <= 10; i++){pthread_mutex_lock(&s->rd_mutex);			//对共享资源进行保护s->rd_wait = 1;								//表示读者线程已经准备好了pthread_cond_wait(&s->rd_cond, &s->rd_mutex);		//将自己加入等待队列等待另外一个线程唤醒printf("[read thread id:%lx] read [%d] from the structure\n",pthread_self(),s->value);pthread_mutex_unlock(&s->rd_mutex);pthread_mutex_lock(&s->wr_mutex);			//加锁,判断写者线程是否准备好了while(!s->wr_wait){pthread_mutex_unlock(&s->wr_mutex);		//如果没有准备好就要释放互斥锁给另外的线程修改wr_wait的机会sleep(1);pthread_mutex_lock(&s->wr_mutex);			//再次上锁判断wr_wait的值检测写者线程是否准备好了		}s->wr_wait = 0;pthread_cond_signal(&s->wr_cond);pthread_mutex_unlock(&s->wr_mutex);}pthread_exit(NULL);
}void* write_func(void *arg)
{Storage *s = (Storage*)arg;int i = 1;for(; i <= 10; i++){pthread_mutex_lock(&s->rd_mutex);		//对共享资源进行加锁,保证共享资源的安全性s->value = i + 10;						//写者线程赋值给结构体中的成员在读者线程中读取printf("[write thread id:%lx] write [%d] to the structure\n",pthread_self(),s->value);while(!s->rd_wait){//若此线程先运行,那么读者线程还没有修改rd_wait的值以此证明读者//线程已经准备好了,所以判断它没有准备好就要释放互斥锁让读者线程//能够拿到锁对rd_wait进行修改pthread_mutex_unlock(&s->rd_mutex);		sleep(1);pthread_mutex_lock(&s->rd_mutex);	//当读者线程修改完后还需要写着线程去判断rd_wait的值,所以还需要再次上锁	}//若退出循环则说明写着线程已经准备好了,把rd_wait初始化等待下一次循环判断s->rd_wait = 0;						//当写者线程准备好的时候就要给它发信号唤醒它pthread_cond_signal(&s->rd_cond);		pthread_mutex_unlock(&s->rd_mutex);//写者加锁来修改写者线程的条件变量pthread_mutex_lock(&s->wr_mutex);s->wr_wait = 1;					//表示写者线程已经准备好了pthread_cond_wait(&s->wr_cond, &s->wr_mutex);	//写者线程等待读者线程调用signal唤醒pthread_mutex_unlock(&s->wr_mutex);}pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t read, write;Storage s;memset(&s, '\0', sizeof(s));//初始化互斥锁和条件变量pthread_mutex_init(&s.rd_mutex, NULL);pthread_mutex_init(&s.wr_mutex, NULL);pthread_cond_init(&s.rd_cond, NULL);pthread_cond_init(&s.wr_cond, NULL);//创建读者线程用于从结构体中读取数据if((err = pthread_create(&read, NULL, read_func, (void*)&s)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}//创建写者线程用于向结构体中写入数据if((err = pthread_create(&write, NULL, write_func, (void*)&s)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}//等待子线程退出并回收其资源pthread_join(read, NULL);pthread_join(write, NULL);//销毁互斥锁和条件变量pthread_mutex_destroy(&s.rd_mutex);pthread_mutex_destroy(&s.wr_mutex);pthread_cond_destroy(&s.rd_cond);pthread_cond_destroy(&s.wr_cond);return 0;
}

image-20240927200704294

通过编译结果可以看代码的执行流程是写者先运行向结构体中写入数据,然后读者线程从结构体中读取数据,然后两个线程交替运行。通过条件变量来控制两个线程的执行顺序来实现一个线程写入一个线程读取,使用互斥锁来保护共享资源的安全。

相关文章:

线程(四)线程的同步——条件变量

文章目录 线程线程的同步和互斥线程同步--条件变量什么是线程同步示例--条件变量的使用示例--使用两个线程对同一个文件进行读写示例--一个读者一个写者使用条件变量来实现同步 线程 线程的同步和互斥 线程同步–条件变量 是一个宏观概念&#xff0c;在微观上包含线程的相互…...

二维数组的旋转与翻转(C++)(上(这只是简单讲解))

二维数组的旋转与翻转&#xff08;C&#xff09; 引言 在计算机科学中&#xff0c;二维数组是一种常见的数据结构&#xff0c;广泛应用于图像处理、数据挖掘、机器学习等多个领域。二维数组的旋转与翻转是处理二维数据时经常需要用到的操作。本文将详细介绍二维数组的旋转与翻…...

【在Linux世界中追寻伟大的One Piece】System V共享内存

目录 1 -> System V共享内存 1.1 -> 共享内存数据结构 1.2 -> 共享内存函数 1.2.1 -> shmget函数 1.2.2 -> shmot函数 1.2.3 -> shmdt函数 1.2.4 -> shmctl函数 1.3 -> 实例代码 2 -> System V消息队列 3 -> System V信号量 1 -> Sy…...

【大数据】Spark弹性分布式数据集RDD详细说明

文章目录 整体介绍一、定义与特性二、操作与转换三、存储级别与持久化四、依赖关系与容错机制五、优化与性能调优 常见操作支持的数据格式1.文本文件 (Text Files)2. CSV 文件3. JSON 文件4. Parquet 文件5. Sequence Files6.Hadoop文件读取A. 读取HDFS上的文本文件B. 使用Hado…...

人参玉桂膏简介

一、产品基本信息 产品名称&#xff1a;人参玉桂膏 产品类别&#xff1a;植物饮料&#xff08;专为特定体质设计&#xff09; 配料&#xff1a;精选薏苡仁、荷叶、玉米须、赤小豆等纯然植物成分&#xff0c;辅以麦芽糖醇液、低聚果糖调节口感&#xff0c;陈皮、肉桂、人参&…...

消费者Rebalance机制

优质博文&#xff1a;IT-BLOG-CN 一、消费者Rebalance机制 在Apache Kafka中&#xff0c;消费者组 Consumer Group会在以下几种情况下发生重新平衡Rebalance&#xff1a; 【1】消费者加入或离开消费者组&#xff1a; 当一个新的消费者加入消费者组或一个现有的消费者离开消费…...

消息队列介绍

一、ActiveMQ 优点&#xff1a;性能单台&#xff08;6000&#xff09;成熟&#xff0c;已经在很多公司得到应用。各种协议支持好&#xff0c;有多个语言的成熟客户端 缺点&#xff1a;性能较弱&#xff0c;缺乏大规模吞吐的场景的应用&#xff0c;有江河日下之感 二、RabbitMQ…...

告别@Value,Spring Boot 3.3更优雅的配置注入方案

在Spring Boot的早期版本中&#xff0c;我们常使用Value注解来注入配置文件中的属性值。然而&#xff0c;这种方式虽然简单直接&#xff0c;却存在一些局限&#xff0c;比如它只能注入基本类型的值&#xff0c;并且需要显式地在每个需要注入的字段上使用注解。随着Spring Boot的…...

甲虫身体图像分割系统源码&数据集分享

甲虫身体图像分割系统源码&#xff06;数据集分享 [yolov8-seg-EfficientRepBiPAN&#xff06;yolov8-seg-C2f-FocusedLinearAttention等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challen…...

Qt - QMenu

QMenu 1、menu转string输出 //GlobalEnum.h #include <QObject> #include <QMetaEnum> class GlobalEnum : public QObject {Q_OBJECT public:EnumTest();enum Enum_Test{ZhangSan 0,WangWu,};Q_ENUM(Enum_Test) };#define EnumToString(e) \ QMetaEnum::fromTy…...

舵机驱动详解(模拟/数字 STM32)

目录 一、介绍 二、模块原理 1.舵机驱动原理 2.引脚描述 三、程序设计 main.c文件 servo.h文件 servo.c文件 四、实验效果 五、资料获取 项目分享 一、介绍 舵机(Servo)是在程序的控制下&#xff0c;在一定范围内连续改变输出轴角度并保持的电机系统。即舵机只支持…...

dvwa:文件包含、文件上传

文件包含 本地文件包含&#xff08;敏感信息泄露&#xff09;和远程文件包含&#xff08;命令执行&#xff09; 本地文件包含一般包含一些本地的敏感文件&#xff0c;如&#xff1a;/etc/passwd或/etc/shadow等 远程文件包含能使得服务器代码执行&#xff0c;如包含黑客vps的…...

基于 C# .NET Framework 4.0 开发实现 WCF 服务实例详解(二)——实现Windows服务内嵌WCF服务

目录 引言 1. 创建一个新的Windows服务项目 2. 添加WCF服务 2.1 添加服务接口和实现 2.2 添加服务配置 3. 实现Windows服务 3.1 修改Service1类 3.2 在项目中添加ServiceInstaller 4. 安装和运行Windows服务 4.1 编译项目 4.2 使用InstallUtil.exe安装服务 …...

【ArcGIS/C#】调用控制台处理代码

代码示例 private static string[] run_conda_process(string command, Action<string> on_msg, CancellationTokenSource cancel){if (string.IsNullOrEmpty(command)){return new string[]{null,ArcGIS.Desktop.Internal.Core.Conda.Resources.Error_Unexpected + &qu…...

06_23 种设计模式之《适配器模式》

文章目录 一、适配器模式基础知识实例 一、适配器模式基础知识 适配器模式定义&#xff1a;将一个类的接口转换成客户希望的另一个接 口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可 以一起工作。 Client&#xff1a;客户端&#xff0c;调用自已需要的领域接口…...

Go语言--快速入门

Go语言特点 我们先看一下简单的Go语言程序 package mainimport "fmt"func main() { // 错误&#xff0c;{ 不能在单独的行上fmt.Println("Hello, World!") }我们可以看到外型非常像我们的JAVA&#xff0c;但是不需要&#xff1b;作为结尾&#xff0c;…...

京东云主机怎么用?使用京东云服务器建网站(图文教程)

京东云主机怎么用&#xff1f;非常简单&#xff0c;本文京东云服务器网jdyfwq.com使用以使用京东云服务器搭建WordPress博客网站为例&#xff0c;来详细说下京东云主机的使用方法。使用京东云服务器快速搭建WordPress网站教程&#xff0c;3分钟基于应用镜像一键搞定&#xff0c…...

Linux 基础入门操作-实验七 进程的介绍

实验七 进程的介绍 7.1 进程基础概念 Linux进程在内存中包含三部分数据&#xff1a;码段、堆栈段和数据段。代码段存放了程序的代码。代码段可以为机器中运行同一程序的数个进程共享。堆栈段存放的是子程序的返回地址、子程序的参数及程序的局部变量。而数据段则存放程序的全…...

SQL进阶技巧:SQL中的正则表达式应用?

目录 0 引言 1. 正则表达式函数 1.1 regexp_extract 1.2 regexp_replace 1.3 regexp_like 2. 在WHERE子句中使用正则表达式 3. 在GROUP BY中使用正则表达式 4. 性能考虑 5. 高级正则表达式技巧 5.1 使用正则表达式进行数据清洗 5.2 使用正则表达式处理JSON 6. 正则…...

算法数组面试理论

数组是存放在连续内存空间内的相同类型数据的集合 &#xff08;所以在删除添加元素的时候需要移动其他的元素的地址&#xff09; 数组的元素是不能删除的&#xff0c;只能覆盖。&#xff08;因为内存地址是连续的&#xff0c;所以不能删除。或者可以这么理解&#xff1a;在一…...

网络编程(Modbus进阶)

思维导图 Modbus RTU&#xff08;先学一点理论&#xff09; 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议&#xff0c;由 Modicon 公司&#xff08;现施耐德电气&#xff09;于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...

多场景 OkHttpClient 管理器 - Android 网络通信解决方案

下面是一个完整的 Android 实现&#xff0c;展示如何创建和管理多个 OkHttpClient 实例&#xff0c;分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...

pam_env.so模块配置解析

在PAM&#xff08;Pluggable Authentication Modules&#xff09;配置中&#xff0c; /etc/pam.d/su 文件相关配置含义如下&#xff1a; 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块&#xff0c;负责验证用户身份&am…...

ESP32读取DHT11温湿度数据

芯片&#xff1a;ESP32 环境&#xff1a;Arduino 一、安装DHT11传感器库 红框的库&#xff0c;别安装错了 二、代码 注意&#xff0c;DATA口要连接在D15上 #include "DHT.h" // 包含DHT库#define DHTPIN 15 // 定义DHT11数据引脚连接到ESP32的GPIO15 #define D…...

django filter 统计数量 按属性去重

在Django中&#xff0c;如果你想要根据某个属性对查询集进行去重并统计数量&#xff0c;你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求&#xff1a; 方法1&#xff1a;使用annotate()和Count 假设你有一个模型Item&#xff0c;并且你想…...

五年级数学知识边界总结思考-下册

目录 一、背景二、过程1.观察物体小学五年级下册“观察物体”知识点详解&#xff1a;由来、作用与意义**一、知识点核心内容****二、知识点的由来&#xff1a;从生活实践到数学抽象****三、知识的作用&#xff1a;解决实际问题的工具****四、学习的意义&#xff1a;培养核心素养…...

江苏艾立泰跨国资源接力:废料变黄金的绿色供应链革命

在华东塑料包装行业面临限塑令深度调整的背景下&#xff0c;江苏艾立泰以一场跨国资源接力的创新实践&#xff0c;重新定义了绿色供应链的边界。 跨国回收网络&#xff1a;废料变黄金的全球棋局 艾立泰在欧洲、东南亚建立再生塑料回收点&#xff0c;将海外废弃包装箱通过标准…...

2025盘古石杯决赛【手机取证】

前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来&#xff0c;实在找不到&#xff0c;希望有大佬教一下我。 还有就会议时间&#xff0c;我感觉不是图片时间&#xff0c;因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...

解决本地部署 SmolVLM2 大语言模型运行 flash-attn 报错

出现的问题 安装 flash-attn 会一直卡在 build 那一步或者运行报错 解决办法 是因为你安装的 flash-attn 版本没有对应上&#xff0c;所以报错&#xff0c;到 https://github.com/Dao-AILab/flash-attention/releases 下载对应版本&#xff0c;cu、torch、cp 的版本一定要对…...

3-11单元格区域边界定位(End属性)学习笔记

返回一个Range 对象&#xff0c;只读。该对象代表包含源区域的区域上端下端左端右端的最后一个单元格。等同于按键 End 向上键(End(xlUp))、End向下键(End(xlDown))、End向左键(End(xlToLeft)End向右键(End(xlToRight)) 注意&#xff1a;它移动的位置必须是相连的有内容的单元格…...