并行编程实战——TBB框架的应用之一Supra的基础

一、TBB的应用

在前面分析了TBB框架的各种基本知识和相关的基础应用。这些基础的应用很容易通过学习文档或相关的代码来较为轻松的掌握。为了能够更好的理解TBB框架的优势,这里从一个开源的应用程序来分析一下TBB在其中的更高一层的抽象应用,以方便开发者能够更好的理解和深入掌握TBB框架。也从设计角度为开发者提供了一个TBB应用的方向,从而能够进一步将TBB框架从基础的技术应用上升到架构设计。

二、Supra项目的介绍

SUPRA: Open Source Software Defined Ultrasound Processing for Real-Time Applications。它是一个开源的超声实时应用软件,主要是在医学领域的超声数据的图像重建和输出,同时允许在图像数据的处理过程中对其进行完善和修改。它支持CPU和GPU两种模式,支持2D和3D图像的格式。
SUPRA的整体的框架基础是在TBB的框架基础上进行设计的,它在TBB的任务、节点等抽象的层次上又进行了一次抽象的封装,让其更接近于人们的认知形态。

三、整体架构分析

SUPRA的架构分析将整体略过上层应用的部分,因为这块与UI和实际业务强相关,与今天要分析的TBB没有什么关系。主要谈一下其库的内容设计。SUPRA将整体的设计划分成了几层:
1、TBB节点的抽象层
SUPRA在TBB现有节点的基础上,再次抽象。实现了输入、输出和算法等节点。但是,其这种抽象,与TBB本身的输入、输出完全不同,其设计的节点完全是纯逻辑上的意义。看一下它的代码定义:

class AbstractNode {
protected:typedef tbb::flow::function_node<std::shared_ptr<RecordObject>, std::shared_ptr<RecordObject>, tbb::flow::rejecting>NodeTypeDiscarding;typedef tbb::flow::function_node<std::shared_ptr<RecordObject>, std::shared_ptr<RecordObject>, tbb::flow::queueing>NodeTypeQueueing;typedef tbb::flow::function_node<std::shared_ptr<RecordObject>, tbb::flow::continue_msg, tbb::flow::rejecting>NodeTypeOneSidedDiscarding;typedef tbb::flow::function_node<std::shared_ptr<RecordObject>, tbb::flow::continue_msg, tbb::flow::queueing>NodeTypeOneSidedQueueing;public:/// Base constructor for all nodesAbstractNode(const std::string &nodeID, bool queueing) : m_nodeID(nodeID), m_queueing(queueing) {m_configurationDictionary.setValueRangeDictionary(&m_valueRangeDictionary);}virtual ~AbstractNode() {}virtual size_t getNumInputs() = 0;virtual size_t getNumOutputs() = 0;/// Returns a pointer to the input port with the given numbervirtual tbb::flow::graph_node *getInput(size_t index) { return nullptr; }/// Returns a pointer to the output port with the given numbervirtual tbb::flow::graph_node *getOutput(size_t index) { return nullptr; }const ValueRangeDictionary *getValueRangeDictionary() { return &m_valueRangeDictionary; }const ConfigurationDictionary *getConfigurationDictionary() { return &m_configurationDictionary; }const std::string &getNodeID() { return m_nodeID; }template <typename ValueType> bool changeConfig(const std::string &configKey, const ValueType &newValue) {if (m_valueRangeDictionary.hasKey(configKey) && m_valueRangeDictionary.isInRange(configKey, newValue)) {LOG(INFO) << "Parameter: " << m_nodeID << "." << configKey << " = " << newValue;m_configurationDictionary.set(configKey, newValue);configurationEntryChanged(configKey);return true;}return false;}void changeConfig(const ConfigurationDictionary &newConfig) {configurationDictionaryChanged(newConfig);// validate the configuration entriesConfigurationDictionary validConfig = newConfig;validConfig.setValueRangeDictionary(&m_valueRangeDictionary);validConfig.checkEntriesAndLog(m_nodeID);// store all valid entriesm_configurationDictionary = validConfig;configurationChanged();}std::string getTimingInfo() { return m_callFrequency.getTimingInfo(); }protected:/// The collection of node parametersConfigurationDictionary m_configurationDictionary;/// The definition of parameters and their respective rangesValueRangeDictionary m_valueRangeDictionary;CallFrequency m_callFrequency;bool m_queueing;protected:virtual void configurationEntryChanged(const std::string &configKey) {}virtual void configurationChanged() {}virtual void configurationDictionaryChanged(const ConfigurationDictionary &newConfig) {}private:std::string m_nodeID;
};
class AbstractInput : public AbstractNode {
public:/// Base constructor for the input node. Initializes its output ports.AbstractInput(tbb::flow::graph &graph, const std::string &nodeID, size_t numPorts): AbstractNode(nodeID, false), m_numOutputs(numPorts) {m_pOutputNodes.resize(m_numOutputs);for (size_t i = 0; i < m_numOutputs; i++) {m_pOutputNodes[i] = std::unique_ptr<tbb::flow::broadcast_node<std::shared_ptr<RecordObject>>>(new tbb::flow::broadcast_node<std::shared_ptr<RecordObject>>(graph));}}~AbstractInput() { waitForFinish(); }void waitForFinish() {if (m_pInputDeviceThread && m_pInputDeviceThread->joinable()) {m_pInputDeviceThread->join();}}void detachThread() { this->m_pInputDeviceThread->detach(); }void start() {setRunning(true);m_pInputDeviceThread = std::make_shared<std::thread>(std::thread([this]() { this->startAcquisition(); }));}/// Set the state of the input node, if newState is false, the node is stoppedvirtual bool setRunning(bool newState) {bool oldState = m_running;m_running = newState;if (!m_running) {stopAcquisition();}return (oldState || newState) && !(oldState && oldState);}/// Returns whether the node is runningbool getRunning() { return m_running; }/// returns the output port with the given indextemplate <size_t index> tbb::flow::broadcast_node<std::shared_ptr<RecordObject>> &getOutputNode() {return *std::get<index>(m_pOutputNodes);}virtual size_t getNumInputs() { return 0; }/// returns the number of output ports of this nodevirtual size_t getNumOutputs() { return m_pOutputNodes.size(); }/// returns a pointer to the output port with the given indexvirtual tbb::flow::graph_node *getOutput(size_t index) {if (index < m_pOutputNodes.size()) {return m_pOutputNodes[index].get();}return nullptr;}protected:/// The nodes output. An implementing node calls this method when it has a/// dataset to send into the graph.template <size_t index> bool addData(std::shared_ptr<RecordObject> data) {return m_pOutputNodes[index]->try_put(data);}double getTimerFrequency() { return m_timer.getFrequency(); }void setUpTimer(double frequency) { m_timer.setFrequency(frequency); }void timerLoop() {bool shouldContinue = true;while (shouldContinue) {shouldContinue = timerCallback();if (shouldContinue) {m_timer.sleepUntilNextSlot();}}}private:std::vector<std::unique_ptr<tbb::flow::broadcast_node<std::shared_ptr<RecordObject>>>> m_pOutputNodes;SingleThreadTimer m_timer;std::shared_ptr<std::thread> m_pInputDeviceThread;std::atomic_bool m_running;protected:std::mutex m_mutex;const size_t m_numOutputs;// Functions to be overwritten
public:virtual void initializeDevice() {}virtual bool ready() { return false; }virtual std::vector<size_t> getImageOutputPorts() = 0;virtual std::vector<size_t> getTrackingOutputPorts() = 0;virtual void freeze() = 0;virtual void unfreeze() = 0;protected:/// The entry point for the implementing input node/// This method is called in a separate thread once the node is started.virtual void startAcquisition() = 0;virtual void stopAcquisition() {}virtual bool timerCallback() { return false; }
};

其它节点的代码就不拷贝上来了。从上面的代码可以看到,其节点的设计几乎等同于应用的逻辑表达了,也就是说抽象的层次更高了。

2、TBB节点抽象后的动态管理层
动态管理层分为几部分:首先是节点的性质、参数以太连接状态等由一个XML配置文件来实现。当然,它也支持在代码中完全动态的调整;其次,节点的管理注册由一个专门的工厂类的实现,包括节点的创建和连接等;最后,它实现了对节点重点参数的动态修改(实现了一套相关的XML自动映射机制)。
这里暂时只关注一下工厂类的处理:

std::shared_ptr<AbstractOutput> InterfaceFactory::createOutputDevice(shared_ptr<tbb::flow::graph> pG,const std::string &nodeID, std::string deviceType,bool queueing) {std::shared_ptr<AbstractOutput> retVal = std::shared_ptr<AbstractOutput>(nullptr);if (deviceType == "OpenIGTLinkClientOutputDevice") {retVal = std::make_shared<OpenIGTLinkClientOutputDevice>(*pG, nodeID, queueing);}if (deviceType == "DatasCacheOutputDevice") {retVal = std::make_shared<DatasCacheOutputDevice>(*pG, nodeID, queueing);}LOG_IF(ERROR, !((bool)retVal)) << "Error creating output device. Requested type '" << deviceType<< "' is unknown. Did you activate the corresponding "<< "module in the build of the library?";LOG_IF(INFO, (bool)retVal) << "Created output device '" << deviceType << "' with ID '" << nodeID << "'";return retVal;
}

在后面会对这一块进行较详细的分析。
3、数据处理层
在每个功能节点,都会有类似下面的writeData来连接抽象节点与TBB节点之间的联系,来处理数据:

  AbstractOutput(tbb::flow::graph &graph, const std::string &nodeID, bool queueing) : AbstractNode(nodeID, queueing) {if (queueing) {m_inputNode = std::unique_ptr<NodeTypeOneSidedQueueing>(new NodeTypeOneSidedQueueing(graph, 1, [this](const std::shared_ptr<RecordObject> &inMessage) {if (this->m_running) {writeData(inMessage);}}));} else {m_inputNode = std::unique_ptr<NodeTypeOneSidedDiscarding>(new NodeTypeOneSidedDiscarding(graph, 1, [this](const std::shared_ptr<RecordObject> &inMessage) {if (this->m_running) {writeData(inMessage);}}));}}

这样就非常巧妙的把二者整合到一想,非常值得借鉴。

4、算法处理层

其算法处理层就是在节点中拿到数据后,对相关数据进行处理,比如各种图像的处理、数据的压缩等等:

	shared_ptr<RecordObject> BeamformingMVNode::checkTypeAndBeamform(shared_ptr<RecordObject> inObj){unique_lock<mutex> l(m_mutex);shared_ptr<USImage> pImageRF = nullptr;if (inObj->getType() == TypeUSRawData){shared_ptr<const USRawData> pRawData = dynamic_pointer_cast<const USRawData>(inObj);if (pRawData){if (pRawData->getImageProperties()->getImageState() == USImageProperties::RawDelayed){m_callFrequency.measure();switch (pRawData->getDataType()){case TypeInt16:pImageRF = beamformTemplated<int16_t>(pRawData);break;case TypeFloat:pImageRF = beamformTemplated<float>(pRawData);break;default:logging::log_error("BeamformingMVNode: Input rawdata type is not supported.");break;}m_callFrequency.measureEnd();if (m_lastSeenImageProperties != pImageRF->getImageProperties()){updateImageProperties(pImageRF->getImageProperties());}pImageRF->setImageProperties(m_editedImageProperties);}else {logging::log_error("BeamformingMVNode: Cannot beamform undelayed RawData. Apply RawDelayNode first");}}else {logging::log_error("BeamformingMVNode: could not cast object to USRawData type, is it in supported ElementType?");}}return pImageRF;}

这个函数checkTypeAndBeamform(类似于writedata函数)内部调用了:

	template <typename RawDataType>std::shared_ptr<USImage> BeamformingMVNode::beamformTemplated(shared_ptr<const USRawData> rawData){shared_ptr<USImage> pImageRF = nullptr;cudaSafeCall(cudaDeviceSynchronize());cublasSafeCall(cublasSetStream(m_cublasH, rawData->getData<RawDataType>()->getStream()));switch (m_outputType){case supra::TypeInt16:pImageRF = RxBeamformerMV::performRxBeamforming<RawDataType, int16_t>(rawData, m_subArraySize, m_temporalSmoothing, m_cublasH, m_subArrayScalingPower, m_computeMeans);break;case supra::TypeFloat:pImageRF = RxBeamformerMV::performRxBeamforming<RawDataType, float>(rawData, m_subArraySize, m_temporalSmoothing, m_cublasH, m_subArrayScalingPower, m_computeMeans);break;default:logging::log_error("BeamformingMVNode: Output image type not supported:");break;}cudaSafeCall(cudaDeviceSynchronize());return pImageRF;}

这其实就进入了算法的处理。后面复杂的算法处理就不拷贝上来了,有兴趣可以自己看看。
5、输入输出层
这个也非常值得借鉴,数据的输入和经过TBB算法处理后的数据需要传递给相关的各方,此处SUPRA也提供了很好的范例

//输入bool UltrasoundInterfaceRawDataMock::timerCallback() {if (!m_frozen){double timestamp = getCurrentTime();m_callFrequency.measure();shared_ptr<USRawData> pRawData = std::make_shared<USRawData>(m_protoRawData->getNumScanlines(),m_protoRawData->getNumElements(),m_protoRawData->getElementLayout(),m_protoRawData->getNumReceivedChannels(),m_protoRawData->getNumSamples(),m_protoRawData->getSamplingFrequency(),m_pMockData,m_protoRawData->getRxBeamformerParameters(),m_protoRawData->getImageProperties(),getCurrentTime(),getCurrentTime());addData<0>(pRawData);if (!m_singleImage){if (m_lastFrame){setRunning(false);}else{readNextFrame();}}m_callFrequency.measureEnd();}return getRunning();}

输入节点通过读取MOCK的文件数据,来复现实际的图像和相关的数据。再看一下输出:

//输出void OpenIGTLinkOutputDevice::writeData(std::shared_ptr<RecordObject> data){if (m_isReady && getRunning() && m_isConnected){m_callFrequency.measure();sendMessage(data);m_callFrequency.measureEnd();}}//最终调用template <typename T>void OpenIGTLinkOutputDevice::sendImageMessageTemplated(shared_ptr<const USImage> imageData){static_assert(std::is_same<T, uint8_t>::value ||std::is_same<T, int16_t>::value ||std::is_same<T, float>::value,"Image only implemented for uchar, short and float at the moment");auto properties = imageData->getImageProperties();if (properties->getImageType() == USImageProperties::BMode ||properties->getImageType() == USImageProperties::Doppler){double resolution = properties->getImageResolution();vec3s imageSize = imageData->getSize();igtl::ImageMessage::Pointer pImageMsg = igtl::ImageMessage::New();pImageMsg->SetDimensions((int)imageSize.x, (int)imageSize.y, (int)imageSize.z);pImageMsg->SetSpacing(resolution, resolution, resolution);if (is_same<T, uint8_t>::value){pImageMsg->SetScalarTypeToUint8();}if (is_same<T, int16_t>::value){pImageMsg->SetScalarTypeToInt16();}if (is_same<T, float>::value){pImageMsg->SetScalarType(igtl::ImageMessage::TYPE_FLOAT32);}pImageMsg->SetEndian(igtl::ImageMessage::ENDIAN_LITTLE);igtl::Matrix4x4 m;igtl::IdentityMatrix(m);m[0][0] = -1;m[1][1] = -1;pImageMsg->SetMatrix(m);pImageMsg->SetNumComponents(1);pImageMsg->SetDeviceName(m_streamName.c_str());pImageMsg->AllocateScalars();igtl::TimeStamp::Pointer pTimestamp = igtl::TimeStamp::New();double timestampSeconds;double timestampFrac = modf(imageData->getSyncTimestamp(), &timestampSeconds);pTimestamp->SetTime((uint32_t)timestampSeconds, (uint32_t)(timestampFrac*1e9));pImageMsg->SetTimeStamp(pTimestamp);auto imageContainer = imageData->getData<T>();if (!imageContainer->isHost()){imageContainer = make_shared<Container<T> >(LocationHost, *imageContainer);}size_t numElements = imageSize.x * imageSize.y * imageSize.z;memcpy(pImageMsg->GetScalarPointer(), imageContainer->get(), numElements * sizeof(T));pImageMsg->Pack();int sendResult = m_clientConnection->Send(pImageMsg->GetPackPointer(), pImageMsg->GetPackSize());if (sendResult == 0) //when it could not be sent{m_isConnected = false;log_info("IGTL: Lost connection. Waiting for next connection.");waitAsyncForConnection();}}}

这个输出节点提供的是医疗上常用的IGTL通信模块来做为输出节点的最终通信方式。

四、总结

之所以从SUPRA框架入手,最主要的就是其在设计上有机的整合了TBB框架,将业务逻辑更好的与TBB框架的设计再次抽象,在整体流程实现的过程中,实现了业务逻辑与底层技术的动态组合。是一个非常值得借鉴的设计。

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

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

相关文章

【工具分享】BarRax勒索病毒解密工具

前言 BarRax勒索软件首次出现于2017年&#xff0c;迅速在全球范围内扩散&#xff0c;主要针对Windows操作系统用户进行攻击。它通过使用AES-256和RSA-2048加密算法对受害者的文件进行加密&#xff0c;使文件变得不可访问。BarRax通常通过恶意电子邮件附件或伪装的软件下载进行…

【LeetCode】每日一题 2024_9_29 买票需要的时间(模拟)

前言 每天和你一起刷 LeetCode 每日一题~ LeetCode 启动&#xff01; 昨天的每日一题是线段树二分&#xff0c;题目难度远超我的能力范围&#xff0c;所以更不出来了 题目&#xff1a;买票需要的时间 代码与解题思路 func timeRequiredToBuy(tickets []int, k int) (sum in…

VisualGLM-6B——原理与部署

VisualGLM-6B技术原理介绍 VisualGLM-6B 是一种多模态预训练模型&#xff0c;它旨在将视觉和语言模型进行结合&#xff0c;使得语言模型能够理解图像信息并与文本对话无缝结合。为了更好地理解 VisualGLM-6B 的内容&#xff0c;我们可以从以下几个方面来解析它的原理、结构、训…

Mac小白必看,快速帮助新手入门的五款软件

刚转MacOS的小白是不是还不知道如何选择软件&#xff0c;MacOS与win有着很大的区别&#xff0c;对于新手来说&#xff0c;想要快速上手Mac的使用&#xff0c;少不了一款好的软件工具&#xff0c;今天给大家推荐五款适合刚转Mac的小白使用的好用工具。 1. iShot&#xff1a;截图…

IPD变革中,数据治理是关键

IPD变革中&#xff0c;数据治理是关键 2024-09-29 14:41汉捷咨询 华为轮值董事长徐直军先生在回顾IPD变革时&#xff0c;提到&#xff1a;“华为IPD变革前期&#xff0c;对数据的关注不够&#xff0c;没有系统梳理产品的信息架构和数据标准&#xff0c;也没有对业务流中的数据…

国内旅游:现状与未来趋势分析

在当今社会快速发展的背景下&#xff0c;国内旅游更是呈现出蓬勃的发展态势。中国&#xff0c;这片拥有悠久历史、灿烂文化和壮丽山河的广袤土地&#xff0c;为国内旅游的兴起与发展提供了得天独厚的条件。 本报告将借助 DataEase 强大的数据可视化分析能力&#xff0c;深入剖…

数据库入门不再难:克服学习障碍的实用技巧与演示

文章目录 摘要引言常见的学习困难及解决方法理解抽象的数据库概念SQL语句的构建与优化理解事务与并发控制 实用的学习技巧与工具推荐推荐学习资源数据库设计与实践的常用技巧 实战演练常见问题解答总结未来展望参考资料 摘要 数据库学习对于初学者来说&#xff0c;往往会面临诸…

优思学院|六西格玛黑带是什么?取得六西格玛黑带证书有何作用?

1. 六西格玛黑带是什么&#xff1f; 六西格玛黑带&#xff08;Black Belt&#xff09;是六西格玛项目中的高级专业人员&#xff0c;负责领导跨职能团队&#xff0c;使用统计分析工具和方法&#xff0c;深入分析业务流程中的问题并制定改进方案。黑带要具备扎实的六西格玛知识&…

HTTP协议代码实现

目录 一. 服务端模块实现 二. HTTP协议模块实现 2.1 HTTP请求部分&#xff1a; 2.2 HTTP响应部分&#xff1a; 2.3 工厂类部分&#xff1a; 2.4 HTTP服务端部分&#xff1a; 2.5 回头处理业务处理函数&#xff1a; 三. 调用服务端模块实现 四. 具体效…

【含文档】基于Springboot+Vue的高校奖助学金系统(含源码+数据库+lw)

1.开发环境 开发系统:Windows10/11 架构模式:MVC/前后端分离 JDK版本: Java JDK1.8 开发工具:IDEA 数据库版本: mysql5.7或8.0 数据库可视化工具: navicat 服务器: SpringBoot自带 apache tomcat 主要技术: Java,Springboot,mybatis,mysql,vue 2.视频演示地址 3.功能 系统定…

柯桥学英语商务口语中老外最爱说的“what‘s up“是什么意思?回答错超尴尬!

“Whats up&#xff1f;” 在看美剧或者和老外聊天的时候 我们总能听到这句话 那你知道这句老外很爱说的 “whats up”是什么意思吗&#xff1f; 快和小编一起来学习吧~ 01 whats up 怎么理解&#xff1f; 很多人觉得Whats up就是更地道的“How are you”&#xff0c;嗯………

【vue3】防抖与节流

1.防抖 &#xff08;多次触发 只执行最后一次&#xff09; 作用&#xff1a; 高频率触发的事件,在指定的单位时间内&#xff0c;只响应最后一次&#xff0c;如果在指定的时间内再次触发&#xff0c;则重新计算时间防抖类似于英雄联盟回城6秒&#xff0c;如果回城中被打断&…

怎么查看员工电脑安装了什么软件

1、使用专业监控软件&#xff1a;安装如金刚钻信息网站行为审计系统、WorkWin等专业的电脑监控软件。这些软件能够实时监控员工的电脑操作&#xff0c;包括安装的软件、运行的程序等。通过软件的管理端&#xff0c;您可以轻松查看员工电脑上安装的所有软件&#xff0c;并可以设…

Library介绍(二)

时序弧&#xff08;timing arc&#xff09; 描述2个节点延迟信息的数据&#xff0c;可以分为net delay和cell delay两大类。 Net delay: drive cell output pin和drived cell input pin之间的net delay&#xff0c;取决于net rc和drive cell驱动能力及drived cell的load。 C…

为什么需要数字集群手持终端?应用优势

在当今社会&#xff0c;无论是应急响应、企业运营还是政府管理&#xff0c;高效、可靠的通讯手段都是成功的关键因素。数字集群手持终端&#xff0c;作为现代通信技术的代表&#xff0c;正成为提升通信效率和确保信息安全的强大工具。据统计&#xff0c;我国的数字集群网络已发…

红蓝攻防实战技术———实战化运营体系的落地

大家好&#xff0c;我是herosunly。985院校硕士毕业&#xff0c;现担任算法研究员一职&#xff0c;热衷于大模型算法的研究与应用。曾担任百度千帆大模型比赛、BPAA算法大赛评委&#xff0c;编写微软OpenAI考试认证指导手册。曾获得阿里云天池比赛第一名&#xff0c;CCF比赛第二…

ZYNQ:点亮LED灯

FPGA 开发流程 1、需求分析&#xff1a;分析需要实现什么功能 2、系统设计&#xff1a;对系统进行设计&#xff0c;需要哪些模块&#xff0c;实现什么功能&#xff0c;数据流怎么走&#xff0c;带宽、工作频率怎么样 3、硬件选项&#xff1a;根据功能、性能需求选择合适的FPGA…

安防区域保护:无线电干扰设备技术详解

在安防区域保护中&#xff0c;无线电干扰设备技术扮演着重要角色&#xff0c;它主要用于通过发射特定频率的无线电波来干扰无人机或其他无线电设备的通信链路、导航信号或控制信号&#xff0c;以达到削弱、阻断甚至控制这些设备运行的目的。以下是对无线电干扰设备技术的详细解…

宝塔环境下MinDoc的安装教程

安装 本教程只适用于CentOS 7&#xff0c;其它系统教程参考&#xff1a;Github地址。 1、下载MinDoc并解压 访问https://github.com/mindoc-org/mindoc/releases下载最新版本并解压 #创建一个目录 mkdir mindoc && cd mindoc#一般宝塔带wget和unzip&#xff0c;如果…

大数据新视界 --大数据大厂之Cassandra 分布式数据库在大数据中的应用与调优

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…