【C++11】多线程创建/互斥详解

目录

    • 1. 背景
    • 2. 线程创建
      • 2.1 使用std::thread类来创建线程
      • 2.2 使用std::async 函数来创建线程
      • 2.3 std::thread和std::async的区别
    • 3. 线程互斥
      • 3.1 互斥体std::mutex
      • 3.2 互斥体包装器std::lock_guard
      • 3.3 条件变量std::condition_variable
      • 3.4 原子类型std::atomic
    • 4. 线程控制自己

1. 背景

在C++11之前,C++标准库并没有提供直接支持多线程的机制。如果我们想在C++程序中实现多线程,需要依赖操作系统提供的线程API。依赖操作系统API有如下缺点↓

  • 导致有跨平台兼容性差:不同的操作系统使用不同的线程API,这使得代码只能在特定的平台上运行)
  • 编程复杂度高:使用操作系统API来进行多线程编程通常比使用高级语言的标准库要复杂得多,需要更多的代码和更精细的控制

操作系统API
在Linux平台上,通常会使用POSIX线程(pthread)API。您需要包含<pthread.h>头文件,并使用pthread_create等函数来创建和管理线程
在Windows平台上,可以使用Windows线程API。您需要包含<windows.h>头文件,并使用CreateThread等函数来创建和管理线程

2. 线程创建

C++11引入了多线程支持,通过使用std::thread类和std::async函数来创建和管理线程。解决了跨平台的问题,并且简化了多线程编程,大大提高了代码的可移植性和可维护性。

2.1 使用std::thread类来创建线程

void myThread_func_without_para() {
	std::cout << "Functions without parameters as parameters: " << std::endl;
}

void myThread_func_with_para(int i) {
	std::cout << "Functions with parameters as parameters: " << i << std::endl;
}

void myThread_funcpoint_without_para() {
	std::cout << "Functionspoint without parameters as parameters: " << std::endl;
}

void myThread_funcpoint_with_para(int i) {
	std::cout << "Functionspoint with parameters as parameters: " << i << std::endl;
}

    {
            /*
		   std::thread类的基本所用
		   使用 std::thread 类的构造函数(拷贝、复制函数已经删除,不能使用)创建一个新线程,并将一个可调用对象(如函数、函数指针或 lambda 表达式)作为参数传递给新线程
		   */
			std::cout << "Create thread by std::thread" << std::endl;

			//无参函数作为参数
			std::thread myThread1(myThread_func_without_para); // 创建一个新线程myThread1并调用myThread_func_without_para函数

			//有参函数作为参数
			std::thread myThread2(myThread_func_with_para, 2); // 创建一个新线程myThread2并调用myThread_func_with_para函数

			//无参函数指针作为参数
			std::thread myThread3(&myThread_funcpoint_without_para); // 创建一个新线程myThread3并调用myThread_funcpoint_without_para函数

			//有参函数指针作为参数
			std::thread myThread4(&myThread_funcpoint_with_para, 4); // 创建一个新线程myThread2并调用myThread_funcpoint_with_para函数

			//lambda表达式作为参数
			std::thread myThread5([](int i) {std::cout << "lambda expressions:" << i << std::endl; }, 5);  // 创建一个新线程myThread5并调用 lambda 表达式

			//堵塞当前线程,等待新线程结束。(join必须有,否则程序可能异常)
			myThread1.join();
			myThread2.join();
			myThread3.join();
			myThread4.join();
			myThread5.join();

			//上述有可能不是按照线程的创建顺序来输出,而是随机的。这是因为多线程是异步方式运行的,谁都有可能先执行完,并不是先创建先执行完。
    }

2.2 使用std::async 函数来创建线程

int myThread_async(int i) {
	std::cout << "myThread_async: " << i << std::endl;
	return i * 10;
}

void myThread_getstatus() {
	std::this_thread::sleep_for(std::chrono::seconds(5));
}
    {
            /*
		    std::async 函数的基本使用
		    可以使用 std::async 函数来创建一个新线程,并将一个可调用对象作为参数传递给新线程,并返回一个std::future对象,该对象可以用于获取任务的结果或状态。
		    */
			std::cout << "Create thread by std::async" << std::endl;

			//获取线程返回值
			/*
			异步启动方式
			std::launch::async                       一个任务被异步执行
			std::launch::deferred                    一个任务被延迟执行(延迟到调用 std::future::get() 或 std::future::wait() 时才开始)
			*/
			std::future<int> myFuture1 = std::async(std::launch::async, myThread_async, 2); // 创建一个新的异步线程并调用 myThread_async 函数
			int result = myFuture1.get(); // 阻塞当前线程等待任务结束并获取结果
			std::cout << "myThread_async result: " << result << std::endl;

			//获取线程状态(软件的加载画面)
			/*
			线程状态
			std::future_status::ready               操作已经完成
			std::future_status::timeout             操作超时
			std::future_status::deferred            操作被延迟
			*/
			std::future<void> myFuture2 = std::async(std::launch::async, myThread_getstatus);
			std::cout << "Please wait 5 seconds:" << std::flush;    // std::flush是C++中的一个操纵符,用于刷新缓冲区。当向标准输出流(如std::cout)写入数据时,数据通常首先存储在内部缓冲区中,然后在缓冲区满或者在一些特定的情况下才被实际写入到设备或者文件中。std::flush操纵符可以用于强制将缓冲区中的数据立即写入到设备(通常是控制台)或文件中。本例中如果不调用std::flush,那么这个字符串可能会在程序结束时才被写入到设备。
			while (myFuture2.wait_for(std::chrono::seconds(1)) != std::future_status::ready) {
				std::cout << '.' << std::flush;  //控制台每隔1秒输出一个'.'
			}
			std::cout << "myThread_getstatus Finished" << std::endl;
    }

2.3 std::thread和std::async的区别

  • std::thread 是用于并行执行任务的线程类;std::async 是用于在后台执行任务并返回结果的异步任务函数。
  • 使用 std::thread 时,你需要手动管理线程的生命周期。当你不再需要线程时,你需要显式地调用join或detach来管理,线程生命周期( join() 方法来等待线程结束, detach() 方法让线程在后台运行)。
  • 使用 std::async 时,你不需要手动管理线程的生命周期。当你调用std::async 函数时,它会自动在线程池中创建、调度和执行任务,并返回一个 std::future 对象,你可以使用该对象来获取任务的结果。

总结一下,当你需要更灵活地控制线程的生命周期和并行执行的方式时,可以使用 std::thread。而当你只需要简单地在后台执行任务并获取结果时,可以使用 std::async,因为它会自动管理线程的生命周期。

3. 线程互斥

3.1 互斥体std::mutex

std::mutex mtx1;    //互斥对象
void print_char1(int len, char c) {
	mtx1.lock();   // 获取互斥对象的锁  
	for (int i = 0; i < len; i++) {
		std::cout << c << std::flush;
	}
	std::cout << std::endl;
	mtx1.unlock();   // 释放互斥对象的锁  
}
		/*
		互斥体std::mutex
		std::mutex是C++标准库中提供的互斥对象,用于实现多线程的互斥。简单地说,互斥就是只允许一个线程访问共享资源。
		*/
		{
			std::thread myThread6(print_char1, 10, '#');
			std::thread myThread7(print_char1, 10, '*');

			myThread6.join();
			myThread7.join();

			/*
			不加锁,输出结果(交叉混合,每次执行结果不一样)
			###*********#####*
			##

			加锁后,输出结果(不会交叉混合,但输出顺序可能不一样)
			##########
			**********
			*/
		}

3.2 互斥体包装器std::lock_guard

std::mutex mtx2;    //互斥对象
void print_char2(int len, char c) {
	std::lock_guard<std::mutex> lock(mtx2);   // 创建lock_guard对象,自动锁定互斥体

	for (int i = 0; i < len; i++) {
		std::cout << c << std::flush;
	}
	std::cout << std::endl;  
	// 离开作用域,自动解锁互斥体  
}
		/*
		互斥体包装器std::lock_guard
		std::lock_guard是C++11标准库中的一种对象,用于封装互斥体(mutex)的锁定和解锁操作(互斥体包装器)。
		当 std::lock_guard 对象被创建时(构造函数),它会自动锁定互斥量,当对象离开作用域时,它会自动解锁互斥量(析构函数)。
        主要用途是简化并发编程中的互斥体管理。
		它减少了因手动调用互斥体的lock()和unlock()方法而产生的错误和复杂性。使用lock_guard可以确保在任何情况下,包括异常和早期返回,都能正确地释放互斥体。

		std::unique_lock 是std::lock_guard 的升级加强版
		- 创建时可以不锁定(通过指定第二个参数为std::defer_lock),而在需要时再锁定
		- std::unique_lock可以随时手动加解锁,而std::lock_guard不能手动加解锁(不手动加解锁的话,同lock_grard一样,构造时自动枷锁,析构时自动释放锁)
		- 资源占用更多(std::unique_lock切换上下文需要更多资源,而std::lock_guard更轻量级)
		- 支持移动语义(std::unique_lock支持移动语义,而std::lock_guard不支持)
		- 与条件变量配合使用时,必须时std::unique_lock,因为它允许你在等待条件变量时解锁互斥对象,等待通知后再重新锁定互斥对象。
		*/
		{
			std::thread myThread8(print_char2, 10, '$');
			std::thread myThread9(print_char2, 10, '%');

			myThread8.join();
			myThread9.join();

			/*
			输出结果(不会交叉混合,但输出顺序可能不一样)
			##########
			**********
			*/
		}

3.3 条件变量std::condition_variable

std::mutex mtx3;    //互斥对象
std::condition_variable cv;   //条件对象
bool ready = false;    // 共享条件变量 
void print_char3(int len, char c) {
	std::unique_lock<std::mutex> lock(mtx3);   // 创建unique_lock对象,自动锁定互斥体

	while (!ready) {    // 等待条件成立:防止伪唤醒。在多核处理器系统上,由于某些复杂机制的存在,可能发生伪唤醒,即一个线程在没有别的线程发送通知信号时也会被唤醒。因而,当线程唤醒时,检查条件是否成立是必要的。而且,伪唤醒可能多次发生,所以条件检查要在一个循环里进行。
		cv.wait(lock);   // 堵塞当前线程,而且当线程被阻塞时,该函数会自动调用lock.unlock()释放锁,使得其他被阻塞的线程得以继续执行。直到别的线程调用notify_* 唤醒当前线程后,wait()函数也是自动调用 lock.lock(),重新获得锁
	}
	//ready为true的时候,跳出循环
	for (int i = 0; i < len; i++) {
		std::cout << c << std::flush;
	}
	std::cout << std::endl;
	// 离开作用域,自动解锁互斥体  
}

void setReady() {
	std::unique_lock<std::mutex> lock(mtx3);   // 创建unique_lock对象,自动锁定互斥体
	for (int i = 0; i < 100000; i++);
	ready = true;   // 设置条件成立  
	cv.notify_all(); // 通知所有等待的线程
}

std::mutex mtx4;    //互斥对象
std::condition_variable cv1;   //条件对象 
int value;
void print_value() {
	std::unique_lock<std::mutex> lock(mtx4);   // 创建unique_lock对象,自动锁定互斥体

	std::cout << "Please input an integer" << std::endl;
	while (cv1.wait_for(lock, std::chrono::seconds(1)) == std::cv_status::timeout) {    // 每次等待1秒,如果1秒内没有收到通知,wait_for会返回std::cv_status::timeout,接着程序会打印一个点,然后循环继续等待1秒
		std::cout << '.' << std::flush;
	}
	std::cout << "The input integer is: " << value << std::endl;
	// 离开作用域,自动解锁互斥体  
}

void setValue() {
	//std::unique_lock<std::mutex> lock(mtx4);   // 创建unique_lock对象,自动锁定互斥体
	std::cin >> value;  
	cv1.notify_all(); // 通知所有等待的线程
}

		/*
		条件变量(std::condition_variable)
		std::condition_variable是C++11标准库中的一个类,用于在多线程编程中实现线程间的条件同步。
		
		条件变量用于在一个线程等待某个条件满足时进行阻塞,并在另一个线程满足条件时通知等待的线程继续执行(即只负责线程的阻塞和唤醒,条件的状态由其他变量或标准来完成)。
		通常与互斥量(std::mutex)一起使用,以确保线程安全。在等待条件之前,线程必须先锁定互斥量;在等待期间,线程会被阻塞,并释放与条件变量关联的互斥量。当其他线程调用notify_one或notify_all成员函数来通知满足条条件变量时,等待的线程将被唤醒并重新获取互斥量,以继续执行。

		阻塞线程(wait、wait_for、wait_until)
		wait	    Wait until notified
        wait_for	Wait for timeout or until notified
        wait_until	Wait until notified or time point

		Note
		重载版本的最后一个参数_Predicate _Pred是一个返回布尔值的函数或表达式,用于在等待条件变量时进行判断。
		当传入了这个参数的时候,只有为true,且收到通知的情况下(或者指定了等待时间、时间点),wait系列函数才会返回。

		唤醒阻塞线程(notify_one或notify_all)
		notify_one	解锁一个线程,如果有多个,则任意一个线程执行,其余继续等待
		notify_all	解锁所有线程
		*/
		{
			//wait
			std::thread myThread10(print_char3, 10, '@');
			std::thread myThread11(setReady);

			myThread10.join();
			myThread11.join();

			//wait_for
			std::thread myThread13(setValue);
			std::thread myThread12(print_value);

			myThread12.join();
			myThread13.join();

			//生产者/消费者场景
			/*
			生产者-消费者问题是一个经典的并发编程问题,涉及到共享固定大小的缓冲区。
			生产者在缓冲区中添加数据,消费者从缓冲区中消费数据。当缓冲区已满时,生产者应该等待;当缓冲区为空时,消费者应该等待。
			*/
			/*std::thread myThread14(producer);
			std::thread myThread15(consumer);

			myThread14.join();
			myThread15.join();*/
		}

3.4 原子类型std::atomic

int number = 0;
std::atomic<int> atomicNumber(0);
void myThread_atomic(int loop) {
	for (int i = 0; i < loop; i++) {
		atomicNumber++;
		number++;
	}
}
		{
			/*
			std::atomic 是C++11引入的一个模板类,用于支持原子操作。它提供了一种线程安全的方式来操作共享变量,避免了多线程环境下的竞态条件。

			在多线程编程中,多个线程可能会同时访问和修改共享数据,这可能会导致数据竞争(data race)的问题。
			为了避免数据竞争,可以使用互斥锁(mutex)或条件变量(condition variable)来保护共享数据的访问。
			然而,这些操作通常比较重量级,会引入额外的开销。为了提高性能,可以使用原子类型。原子类型是一种特殊的数据类型,它在多线程环境中保证对其操作是原子的,即不会被其他线程打断。

			Note
			std::atomic几乎支持所有的内置类型和指针,需要注意的是有些类型的性能并不是很好。
			*/

			std::thread myThread16(myThread_atomic, 10000);
			std::thread myThread17(myThread_atomic, 10000);

			myThread16.join();
			myThread17.join();

			std::cout << "Final number: " << number << std::endl; 
			std::cout << "Final atomicNumber: " << atomicNumber << std::endl; 

			/*
			输出结果
			Final number: 19930     // 多线程是同时进行且无序的,所以如果它们同时操作同一个变量,那么肯定会出错
			Final atomicNumber: 20000   // 原子操作是最小的且不可并行化的操作。所有即使没有加锁,也会像同步进行一样操作atomic对象,从而节省了上锁、解锁的时间消耗
			*/
		}

4. 线程控制自己

void myThread_this_thread() {
	//获取线程id
	std::thread::id id = std::this_thread::get_id();
	std::cout << "thread id: " << id << std::endl;

	//线程休眠3秒,并且每隔1秒输出一个点
	std::cout << "thread woke up after 3 seconds: ";
	for (int i = 0; i < 3; i++) {
		std::this_thread::sleep_for(std::chrono::seconds(1));
		std::cout << "." << std::flush;
	}
	std::cout << std::endl;
}
	{
		/*
		std::this_thread
		get_id		获取线程id
		yield		提示操作系统让出CPU使用权,让其他线程运行,然后等待操作系统重新调度当前线程并分配CPU使用权继续执行
		sleep_for	使线程休眠到指定时间
		sleep_until	使线程休眠到指定的时间点
		*/
		std::thread myThread16(myThread_this_thread);

		myThread16.join();
	}

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

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

相关文章

红魔8/8Pro/8SPro手机升级安卓14版RedMagic9.0系统+降级出厂救砖刷机

红魔8系列手机也终于引来了安卓14系统的更新&#xff0c;该系统为最新的RedMagic9.0&#xff0c;目前属于公测版本&#xff0c;如果你已经升级了官方UI8.0最新版系统&#xff0c;并且拥有公测资格&#xff0c;可以直接在线检测到最新版UI9.0系统。9.0系统目前对比之前的8.0的版…

MFRC50001T 封装SOP-32 高性能非接触式读写芯片

MFRC50001T是由NXP Semiconductors&#xff08;恩智浦半导体&#xff09;生产的一款高性能非接触式读写芯片。这款芯片主要针对13.56 MHz频段的RFID&#xff08;无线射频识别&#xff09;和MIFARE Classic协议&#xff0c;支持ISO/IEC 14443 Type A标准的多层应用。MFRC50001T芯…

HTML:认识HTML及基本语法

目录 1. HTML介绍 2. 关于软件选择和安装 3. HTML的基本语法 1. HTML介绍 HyperText Markup Language 简称HTML&#xff0c;意为&#xff1a;超文本标记语言 超文本&#xff1a;是指页面内可以包含的图片&#xff0c;链接&#xff0c;声音&#xff0c;视频等内容 标记&am…

vue2插件之@lucky-canvas/vue,大转盘、抽奖、老虎机

提示&#xff1a;vue2插件 文章目录 [TOC](文章目录) 前言一、查看nodejs版本二、创建项目三、大转盘四、抽奖五、老虎机六、官网总结 前言 lucky-canvas/vue 一、查看nodejs版本 node -v二、创建项目 1、安装插建 npm install lucky-canvas/vue --save2、目录结构 3、引用…

AI大模型探索之路-训练篇8:大语言模型Transformer库-预训练流程编码体验

系列篇章&#x1f4a5; AI大模型探索之路-训练篇1&#xff1a;大语言模型微调基础认知 AI大模型探索之路-训练篇2&#xff1a;大语言模型预训练基础认知 AI大模型探索之路-训练篇3&#xff1a;大语言模型全景解读 AI大模型探索之路-训练篇4&#xff1a;大语言模型训练数据集概…

stable diffusion controlnet前处理中的图像resize

在SD controlnet应用中&#xff0c;一般都要先安装controlnet_aux&#xff0c;并在项目代码中import相关前处理模块&#xff0c;如下所示。 在对control image进行前处理&#xff08;比如找边缘&#xff0c;人体特征点&#xff09;之前&#xff0c;往往会图像进行resize&#x…

【论文阅读——基于拍卖的水平联邦学习后付款激励机制设计与声誉和贡献度测量】

1.原文名称 Auction-Based Ex-Post-Payment Incentive Mechanism Design for Horizontal Federated Learning with Reputation and Contribution Measurement 2.本文的贡献 我们提出了一种贡献度测量方法。我们建立了一个声誉系统。声誉易于下降&#xff0c;难以提高。结合声…

Redis源码学习记录:列表 (ziplist)

ziplist redis 源码版本&#xff1a;6.0.9。ziplist 的代码均在 ziplist.c / ziplist.h 文件中。 定义 ziplist总体布局如下&#xff1a; <zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend> zlbytes&#xff1a;uin…

stm32单片机开发一、中断之外部中断实验

stm32单片机的外部中断和定时器中断、ADC中断等都由stm32的内核中的NVIC模块控制&#xff0c;stm32的中断有很多中&#xff0c;比如供电不足中断&#xff0c;当供电不足时&#xff0c;会产生的一种中断&#xff0c;这么多中断如果都接在CPU上&#xff0c;或者说CPU去处理&#…

普乐蛙元宇宙VR体验馆设备集体亮相VR文旅景区展

普乐蛙全国巡展又双叒叕开始了! 这次来到的是“好客山东”↓↓ 山东2024休闲旅游产业展 4月25日至27日&#xff0c;2024休闲旅游产业展在临沂国际博览中心举办。本次展会以“潮购文旅好品&#xff0c;乐享时尚生活”为主题&#xff0c;汇聚全国文旅产业上下游500多家企业、上万…

基于FCN网络实现的多类别图像分割任务

1、前言 FCN 作为图像分割的开山之作&#xff0c;将分割任务作为逐个像素点的分类任务 之前完成了基于unet、resnetunet、deeplab等等网络的分割任务&#xff0c;具体的可以参考本专栏&#xff1a; 图像分割_听风吹等浪起的博客-CSDN博客 因为FCN网络的实现较为复杂&#xf…

【阿里笔试题汇总】[全网首发] 2024-04-29-阿里国际春招笔试题-三语言题解(CPP/Python/Java)

&#x1f36d; 大家好这里是清隆学长 &#xff0c;一枚热爱算法的程序员 ✨ 本系列打算持续跟新阿里国际近期的春秋招笔试题汇总&#xff5e; &#x1f4bb; ACM银牌&#x1f948;| 多次AK大厂笔试 &#xff5c; 编程一对一辅导 &#x1f44f; 感谢大家的订阅➕ 和 喜欢&#x…

访学/博后/联培博士关注|不同国家的英语口音辨识度训练

在访问学者、博士后及联合培养的申请过程中&#xff0c;接收方多数都要求英文面试。如果导师的母语为非英语国家&#xff0c;将会带有口音&#xff0c;这样更增加了英语面试难度。如何提升不同国家的英语口音辨识度&#xff0c;使自己的英语表达更加流利&#xff0c;知识人网小…

01数学建模 -线性规划

1.1线性规划–介绍 翻译翻译什么叫惊喜 1.2线性规划–原理 拉格朗日乘数法手算 最值化 f ( x , y ) , s . t . g ( x , y ) c , 引入参数 λ &#xff0c;有&#xff1a; F ( x , y , λ ) f ( x , y ) λ ( g ( x , y ) − c ) 再将其分别对 x , y , λ 求导&#xff0c…

第十五届蓝桥杯省赛第二场C/C++B组H题【质数变革】题解

解题思路 首先&#xff0c;我们考虑一下整个数组都是由质数构成的情况。 当我们要将质数 x x x 向后移 k k k 个时&#xff0c;如果我们可以知道质数 x x x 在质数数组的下标 j j j&#xff0c;那么就可以通过 p r i m e s [ j k ] primes[j k] primes[jk] 来获取向后…

远程桌面的端口配置与优化

在现代企业环境中&#xff0c;远程桌面连接已成为日常工作中不可或缺的一部分。然而&#xff0c;随着网络攻击的增加&#xff0c;确保远程桌面连接的安全性变得尤为重要。其中一个关键的安全因素是端口配置。 一、远程桌面默认端口 远程桌面协议&#xff08;RDP&#xff09;默…

vue2迁移到vue3,v-model的调整

项目从vue2迁移到vue3&#xff0c;v-model不能再使用了&#xff0c;需要如何调整&#xff1f; 下面只提示变化最小的迁移&#xff0c;不赘述vue2和vue3中的常规写法。 vue2迁移到vue3&#xff0c;往往不想去调整之前的代码&#xff0c;以下就使用改动较小的方案进行调整。 I…

无人机反制:雷达探测+信号干扰器技术详解

固定翼无人机、旋翼无人机等&#xff0c;可折叠式无机、DIY无人机等。黑飞&#xff0c;监管困难给航空业带来了诸多隐患&#xff1b;给恐怖袭击及间谍侦察带来新的方式、引发了各国地区政府的忧虑&#xff0c;在中国存在的问题更加严峻。 反无人飞行器防御系统(AUDS)&#xff0…

positivessl通配符证书签发13个月仅400元

PositiveSSL是Sectigo旗下的数字证书品牌&#xff0c;旗下的数字证书产品不仅具有签发速度快、性价比高以及兼容性高等特点&#xff0c;还可以为网站提供传输信息加密服务&#xff0c;保护客户隐私安全&#xff0c;提升客户对网站的信任度。今天就随SSL盾小编了解PositiveSSL旗…

【好书推荐8】《智能供应链:预测算法理论与实战》

【好书推荐8】《智能供应链&#xff1a;预测算法理论与实战》 写在最前面编辑推荐内容简介作者简介目录精彩书摘前言/序言我为什么要写这本书这本书能带给你什么 致谢 &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴…