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

news2024/10/10 2:47:59

文章目录

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

线程

线程的同步和互斥

线程同步–条件变量

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

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

线程同步–条件变量

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

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

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

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

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

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

  • 条件变量的数据类型

    • 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]);					//将从命令行传入的字符串赋值给str
	strcpy(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

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

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

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

相关文章

新160个crackme - 078-CodeZero.1

运行分析 需要破解Serial PE分析 VB程序&#xff0c;32位&#xff0c;无壳 静态分析&动态调试 使用VB Decompiler进行分析找到check按钮事件&#xff1a; Form1 -> Command1_Click_4055F4发现直接爆出了Serial55555 验证成功

【xilinx-versal】【Petalinux】I2C驱动开发问题记录

问题 调试中发现系统起来后无I2C设备。 仔细查找后发现没有配置versal的I2C控制器。 解决方法 打开versal的I2C控制器的配置 起来后I2C设备注册成功

Acwing 区间问题

Acwing 905.区间选点 实现思路&#xff1a; 将每个区间按照右端点从小到大排序从前往后依次枚举每个区间 若当前区间中已经包含点&#xff0c;则跳过&#xff1b;否则&#xff08;即当前区间的左端点大于该点&#xff09;&#xff0c;选择当前区间的右端点&#xff1b; 证明&a…

设计模式:单例

一.什么是单例模式 单例模式是一种设计模式&#xff0c;指在整个程序生命周期中有且仅有一个实例的类。可以分为懒汉式以及饿汉式。 懒汉式&#xff1a;只有在类的实例被使用时才生成唯一实例。但是存在线程安全以及内存泄露的问题。可以节省系统资源。 饿汉式&#xff1a;程序…

《Oracle DB备份与恢复》:一文千字教你掌握备份基础知识

** List item 备份需要扎实掌握基础知识&#xff0c;这样才能规划好适合自己的备份恢复策略&#xff0c;才能在出故障的时候不慌不忙&#xff0c;从容应付。 好了不多逼逼了&#xff0c;直接上干货。** 1. 备份分类&#xff1a; 备份根据性质和目的不同分为以下几种&#…

车辆路径规划问题(VRP)优化方案

车辆路径规划问题&#xff08;VRP&#xff09;优化方案 车辆路径规划问题&#xff08;Vehicle Routing Problem, VRP&#xff09;是物流领域中一个经典的组合优化问题&#xff0c;目标是在满足客户需求的情况下&#xff0c;找到一组车辆的最优配送路径&#xff0c;以最小化总的…

如何让员工意识到六西格玛项目对公司和个人的长期利益?

当下&#xff0c;六西格玛作为一种以数据驱动的管理方法论&#xff0c;正逐步成为许多企业实现卓越运营的重要工具。然而&#xff0c;要让员工深刻认识到六西格玛项目不仅对公司长远发展至关重要&#xff0c;也对他们个人职业生涯有着深远的积极影响&#xff0c;并非易事。下面…

C++ day05(模版与容器)

目录 【1】模版 template 1》概念 2》函数模版 3》类模版 【2】容器 1》STL标准模版库 2》容器的概念 3》顺序容器 1> arrry(C11) 2> vector 3> list 4> deque 4》 关联容器 5》迭代器 iterator 【1】模版 template 1》概念 C模版可以让类或函数声…

javacpp调用pdfium的c++动态库

1、.h头文件 2、生成java代码的conf PdfiumDocumentConfigure.java package org.swdc.pdfium.conf;import org.bytedeco.javacpp.annotation.Platform; import org.bytedeco.javacpp.annotation.Properties; import org.bytedeco.javacpp.tools.InfoMap; import org.byte…

网络知识点之—EVPN

EVPN&#xff08;Ethernet Virtual Private Network&#xff09;是下一代全业务承载的VPN解决方案。EVPN统一了各种VPN业务的控制面&#xff0c;利用BGP扩展协议来传递二层或三层的可达性信息&#xff0c;实现了转发面和控制面的分离。 EVPN解决传统L2VPN的无法实现负载分担、…

springboot+vue前后端分离-使用腾讯云服务器部署网站

项目打包 参考链接 CSDN springboot打包 idea默认新建的shell窗口在项目根目录位置&#xff0c;可以看到项目根目录下有mvnw HELP.md log mvnw mvnw.cmd pom.xml src target./mvnw clean package -Dmaven.test…

Kali Linux中安装配置影音资源下载神器Amule

一、Debian系列Linux安装amule命令&#xff1a; sudo apt update sudo apt-get install amule amule-utils 二、配置Amule的要点&#xff1a; 1、首次运行Amule&#xff0c;提示是否下载服务器列表&#xff0c;点击是。 2、搜索选项的类型选择全球&#xff0c;类型的默认选项…

数据恢复与数据取证:Visual NAND Reconstructor 闪存数据恢复工具

天津鸿萌科贸发展有限公司是专业闪存数据恢复工具 VNR (Visual NAND Reconstructor) 的授权代理商。 VNR&#xff08;Visual NAND Reconstructor&#xff09;可视化 NAND 重建工具套件由硬件和软件构成。其中硬件部分包含 NAND 读卡器和一组用于不同 NAND 芯片封装的适配器。读…

LeetCode 48 Rotate Image 解题思路和python代码

题目&#xff1a; You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise). You have to rotate the image in-place, which means you have to modify the input 2D matrix directly. DO NOT allocate another 2D matrix and …

序列化流和反序列化流

序列化流 ObjectOutputStream 序列化流/对象操作输出流 包装基本流&#xff0c;属于字节流的一种&#xff0c;负责输出数据。可以把Java中的对象写到本地文件中去。 构造方法&#xff1a; public ObjectOutputStream&#xff08;OutputStream out&#xff09;把基本流包装成高…

【Java 并发编程】解决多线程中数据错乱问题

前言 承接上回&#xff0c;我们已经理解了线程的一些相关操作&#xff0c;本篇内容将会讲到如何去解决线程竞争导致的数据错乱。 前期回顾&#xff1a;线程操作 目录 前言 线程竞争的场景 竞态条件的详解 synchronized 关键字 ReentrantLock 类 线程竞争的场景 概念&#xff1a…

异步场景: promise、async函数与await命令介绍

如果你也对鸿蒙开发感兴趣&#xff0c;加入“Harmony自习室”吧&#xff01;扫描下方名片&#xff0c;关注公众号&#xff0c;公众号更新更快&#xff0c;同时也有更多学习资料和技术讨论群。 在鸿蒙的开发中&#xff0c;我们时常会遇到promise异步场景&#xff0c;有同学反馈说…

Adobe Acrobat Pro和Adobe Acrobat DC有什么区别?

主要区别 Adobe Acrobat Pro&#xff1a; 以单次购买的方式提供&#xff0c;用户需要一次性付费购买&#xff0c;之后即可永久使用该版本。不会频繁更新&#xff0c;通常只在发行新版本时进行更新。 Adobe Acrobat DC&#xff1a; 以订阅方式提供&#xff0c;用户需要每年支付…

《2024 国庆旅游数据洞察:活力与新趋势》

《2024 国庆旅游数据洞察&#xff1a;活力与新趋势》 一、国庆旅游市场整体态势 今年国庆假期&#xff0c;旅游市场的火爆程度令人瞩目。从出行人次来看&#xff0c;嘀嗒出行的国庆顺风出行预测显示&#xff0c;顺风出发高峰日预计为 9 月 29 日&#xff0c;环比 9 月出行峰值…

在Ubuntu 22.04上安装Ollama的两种方式

curl 安装 参考Linux上安装Ollama的官方文档&#xff1a;https://ollama.com/download/linux 在终端执行以下命令即可&#xff1a; curl -fsSL https://ollama.com/install.sh | shdocker 安装 官方 Ollama Docker 镜像可以直接在Docker Hub上进行拉取。 Docker Hub上的ol…