Qt之条件变量QWaitCondition详解

QWaitCondition内部实现结构图: 

相关系列文章

C++之Pimpl惯用法

目录

1.简介

2.示例

2.1.全局配置

2.2.生产者Producer

2.3.消费者Consumer

2.4.测试例子

3.原理分析

3.1.辅助函数CreateEvent

3.2.辅助函数WaitForSingleObject 

3.3.QWaitConditionEvent

3.4.QWaitConditionEventPrivate

3.5.流程分析

4.总结


1.简介

QWaitCondition是用来同步线程的条件变量,头文件<QWaitCondition>,类中的所有函数都是线程安全的。主要的公共函数(以Qt5.12.12为例)如下表:

返回类型函数名称含义
QWaitCondition ()构造函数
~QWaitCondition ()析构函数
boolwait ( QMutex * mutex, unsigned long time = ULONG_MAX )mutex将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来:
(1)另一个线程使用wakeOne()或wakeAll()传输给它;
(2)time毫秒过去。
boolwait(QMutex *lockedMutex, QDeadlineTimer deadline)同上
boolwait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX )readWriteLock将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来:
(1)另一个线程使用wakeOne()或wakeAll()传输给它;
(2)time毫秒过去。
boolwait(QReadWriteLock *lockedReadWriteLock, QDeadlineTimer deadline)同上
voidwakeAll ()唤醒所有等待的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
voidwakeOne()唤醒等待QWaitCondition的线程中的一个线程,线程唤醒的顺序不确定,由操作系统的调度策略决定
voidnotify_all()同wakeAll()
voidnotify_one()同wakeOne()

QWaitCondition允许线程告诉其他线程某种条件已经满足。一个或多个线程可以阻止等待QWaitCondition使用wakeOne()或wakeAll()设置条件。使用wakeOne()唤醒一个随机选择的线程,或使用wakeAll()唤醒所有线程。

2.示例

生产者/消费者模型为例,看一下具体实现

2.1.全局配置

//! [0]
const int DataSize = 127;const int BufferSize = 8192;
char buffer[BufferSize];QWaitCondition bufferNotEmpty;
QWaitCondition bufferNotFull;
QMutex mutex;
int numUsedBytes = 0;
//! [0]

        主要有缓冲区buffer, 循环缓冲区大小BufferSize及生产的数量,小于 DataSize,这意味着在某一时刻生产者将达到缓冲区的末尾,并从开始位置重新启动。

        要同步生产者和消费者,需要两个 wait 条件和一个 mutex。当生产者生成一些数据时,bufferNotEmpty 条件被发射,告诉消费者可以读取它了;当消费者读取一些数据时,bufferNotFull 条件被发射,告诉生产者生成更多的数据。numUsedBytes 为缓冲区中所包含数据的字节数。

        总之,wait 条件、mutex、和 numUsedBytes 计数器确保生产者不会先于消费者超过 BufferSize 的大小,而消费者永远不会读取生产者尚未生成的数据。

2.2.生产者Producer

生产者的代码如下:

//! [1]
class Producer : public QThread
//! [1] //! [2]
{
public:Producer(QObject *parent = NULL) : QThread(parent){}void run() override{for (int i = 0; i < DataSize; ++i) {mutex.lock();if (numUsedBytes == BufferSize)bufferNotFull.wait(&mutex);mutex.unlock();buffer[i % BufferSize] = i;"ACGT"[QRandomGenerator::global()->bounded(4)];mutex.lock();++numUsedBytes;bufferNotEmpty.wakeAll();mutex.unlock();}}
};
//! [2]

        生产者根据DataSize的大小循环生产数据。在往循环缓冲区写入一个字母之前,它必须检查缓冲区是否已满(即满足numUsedBytes等于BufferSize条件),如果缓冲区满了,现成就会在bufferNotFull条件上等待。

        满足条件后,生产者增加 numUsedBytes,并且标志 bufferNotEmpty 条件为 true,从而唤醒消费者线程去消费。

2.3.消费者Consumer

消费者的代码如下:

//! [3]
class Consumer : public QThread
//! [3] //! [4]
{Q_OBJECT
public:Consumer(QObject *parent = NULL) : QThread(parent){}void run() override{for (int i = 0; i < DataSize; ++i) {mutex.lock();if (numUsedBytes == 0)bufferNotEmpty.wait(&mutex);mutex.unlock();fprintf(stderr, "%d\n", buffer[i % BufferSize]);mutex.lock();--numUsedBytes;bufferNotFull.wakeAll();mutex.unlock();}fprintf(stderr, "\n");}signals:void stringConsumed(const QString &text);
};
//! [4]

代码非常类似于生产者,在读取字节之前,需要先检查缓冲区是否为空(numUsedBytes 为 0),而非它是否为已满。并且,当它为空时,等待 bufferNotEmpty 条件。在读取字节后,减小 numUsedBytes (而非增加),并标志 bufferNotFull 条件(而非 bufferNotEmpty 条件)。

2.4.测试例子

代码如下:

//! [5]
int main(int argc, char *argv[])
//! [5] //! [6]
{QCoreApplication app(argc, argv);Producer producer;Consumer consumer;producer.start();consumer.start();producer.wait();consumer.wait();return 0;
}
//! [6]#include "waitconditions.moc"

        上面的测试代码是一个生产者对一个消费者,生产一个消费一个,所以看到的结果是按照顺序输出,结果如下:

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126

        此示例中提出的“生产者 - 消费者”模式,适用于编写高并发多线程应用。在多处理器计算机中,程序可能比基于 mutex 的方案快达两倍之多,因为两个线程可以同一时间在缓冲区的不同部分处于激活状态。

        上面我们讲解了QWaitCondition的用法,如果还有兴趣继续探究它的实现原理的话,则可以继续往下看。

3.原理分析

在Qt5.12.12版本上,以windows端来讲解它的实现原理。

3.1.辅助函数CreateEvent

CreateEvent的定义如下:

HANDLE CreateEvent([in, optional] LPSECURITY_ATTRIBUTES lpEventAttributes,[in]           BOOL                  bManualReset,[in]           BOOL                  bInitialState,[in, optional] LPCSTR                lpName
);

创建或打开一个命名或未命名的事件对象。

lpEventAttributes: 指向 SECURITY_ATTRIBUTES 结构的指针。 如果此参数为 NULL,则子进程无法继承句柄。结构的 lpSecurityDescriptor 成员为新事件指定 安全描述符 。 如果 lpEventAttributes 为 NULL,则事件将获取默认的安全描述符。 事件的默认安全描述符中的 ACL 来自创建者的主要令牌或模拟令牌。

bManualReset: 如果此参数为 TRUE,则函数将创建手动重置事件对象,该对象需要使用 ResetEvent 函数将事件状态设置为非签名。 如果此参数为 FALSE,则函数将创建一个自动重置事件对象,在释放单个等待线程后,系统会自动将事件状态重置为未签名。

bInitialState: 如果此参数为 TRUE,则会向事件对象发出初始状态信号;否则,它将不进行签名。

lpName: 可选项,事件对象的名称。 名称限制为 MAX_PATH 个字符。 名称比较区分大小写。

如果 lpName 与现有命名事件对象的名称匹配,则此函数请求 EVENT_ALL_ACCESS 访问权限。 在这种情况下, bManualReset 和 bInitialState 参数将被忽略,因为它们已由创建过程设置。 如果 lpEventAttributes 参数不是 NULL,它将确定是否可以继承句柄,但忽略其安全描述符成员。

如果 lpName 为 NULL,则创建不带名称的事件对象。

如果 lpName 与同一命名空间中另一种对象的名称匹配, (例如现有信号灯、互斥体、可等待计时器、作业或文件映射对象) ,则函数将失败, GetLastError 函数将返回 ERROR_INVALID_HANDLE。 发生这种情况的原因是这些对象共享相同的命名空间。

名称可以具有“Global”或“Local”前缀,以在全局命名空间或会话命名空间中显式创建对象。 名称的其余部分可以包含除反斜杠字符 (\) 以外的任何字符。 有关详细信息,请参阅 内核对象命名空间。 使用终端服务会话实现快速用户切换。 内核对象名称必须遵循终端服务概述的准则,以便应用程序可以支持多个用户。

可以在专用命名空间中创建 对象。 有关详细信息,请参阅 对象命名空间。

返回值:如果函数成功,则返回值是事件对象的句柄。 如果命名事件对象在函数调用之前存在,则函数将返回现有对象的句柄, GetLastError 将返回 ERROR_ALREADY_EXISTS

如果函数失败,则返回值为 NULL。 要获得更多的错误信息,请调用 GetLastError。

3.2.辅助函数WaitForSingleObject 

这个是windows系统多线程,进程中用的最多的一个函数,它的定义如下:

DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);

这个函数的作用是等待一个内核对象,在Windows系统上一个内核对象通常使用其句柄来操作,参数hHandle即需要等待的内核对象,参数dwMilliseconds是等待这个内核对象的最大时间,时间单位是毫秒,其类型是DWORD,这是一个unsigned long类型。如果我们需要无限等待下去,可以将这个参数值设置为INFINITE宏。

在Windows上可以调用WaitForSingleObject等待的常见对象如下表所示:

可以被等待的对象等待对象成功的含义对象类型
线程等待线程结束HANDLE
Process等待进程结束HANDLE
Event (事件)等待 Event 有信号HANDLE
Mutex (互斥体)等待持有 Mutex 的线程释放该 Mutex,等待成功,拥有该MutexHANDLE
Semaphore(信号量)等待该 Semaphore 对象有信号HANDLE

上面介绍的等待线程对象上文中已经详细介绍过了,这里不再重复了,等待进程退出与等待线程退出类似,也不再赘述。下文中我们将详细介绍 Event、Mutex、Semaphore 这三种类型的资源同步对象,这里我们先接着介绍WaitForSingleObject函数的用法,该函数的返回值一般有以下类型:

  • WAIT_FAILED,表示WaitForSingleObject函数调用失败了,调用失败时,可以通过GetLastError 函数得到具体的错误码;
  • WAIT_OBJECT_0,表示WaitForSingleObject成功“等待”到设置的对象;
  • WAIT_TIMEOUT,等待超时;
  • WAIT_ABANDONED,当等待的对象是Mutex类型时,如果持有该Mutex对象的线程已经结束,但是没有在结束前释放该Mutex,此时该Mutex已经处于废弃状态,其行为是未知的,不建议再使用该Mutex。

上面我们讲解了CreateEvent和WaitForSingleObject函数,下面看一个示例:

#include <windows.h>
#include <stdio.h>#define THREADCOUNT 4 HANDLE ghWriteEvent; 
HANDLE ghThreads[THREADCOUNT];DWORD WINAPI ThreadProc(LPVOID);void CreateEventsAndThreads(void) 
{int i; DWORD dwThreadID; // Create a manual-reset event object. The write thread sets this// object to the signaled state when it finishes writing to a // shared buffer. ghWriteEvent = CreateEvent( NULL,               // default security attributesTRUE,               // manual-reset eventFALSE,              // initial state is nonsignaledTEXT("WriteEvent")  // object name); if (ghWriteEvent == NULL) { printf("CreateEvent failed (%d)\n", GetLastError());return;}// Create multiple threads to read from the buffer.for(i = 0; i < THREADCOUNT; i++) {// TODO: More complex scenarios may require use of a parameter//   to the thread procedure, such as an event per thread to  //   be used for synchronization.ghThreads[i] = CreateThread(NULL,              // default security0,                 // default stack sizeThreadProc,        // name of the thread functionNULL,              // no thread parameters0,                 // default startup flags&dwThreadID); if (ghThreads[i] == NULL) {printf("CreateThread failed (%d)\n", GetLastError());return;}}
}void WriteToBuffer(VOID) 
{// TODO: Write to the shared buffer.printf("Main thread writing to the shared buffer...\n");// Set ghWriteEvent to signaledif (! SetEvent(ghWriteEvent) ) {printf("SetEvent failed (%d)\n", GetLastError());return;}
}void CloseEvents()
{// Close all event handles (currently, only one global handle).CloseHandle(ghWriteEvent);
}int main( void )
{DWORD dwWaitResult;// TODO: Create the shared buffer// Create events and THREADCOUNT threads to read from the bufferCreateEventsAndThreads();// At this point, the reader threads have started and are most// likely waiting for the global event to be signaled. However, // it is safe to write to the buffer because the event is a // manual-reset event.WriteToBuffer();printf("Main thread waiting for threads to exit...\n");// The handle for each thread is signaled when the thread is// terminated.dwWaitResult = WaitForMultipleObjects(THREADCOUNT,   // number of handles in arrayghThreads,     // array of thread handlesTRUE,          // wait until all are signaledINFINITE);switch (dwWaitResult) {// All thread objects were signaledcase WAIT_OBJECT_0: printf("All threads ended, cleaning up for application exit...\n");break;// An error occurreddefault: printf("WaitForMultipleObjects failed (%d)\n", GetLastError());return 1;} // Close the events to clean upCloseEvents();return 0;
}DWORD WINAPI ThreadProc(LPVOID lpParam) 
{// lpParam not used in this example.UNREFERENCED_PARAMETER(lpParam);DWORD dwWaitResult;printf("Thread %d waiting for write event...\n", GetCurrentThreadId());dwWaitResult = WaitForSingleObject( ghWriteEvent, // event handleINFINITE);    // indefinite waitswitch (dwWaitResult) {// Event object was signaledcase WAIT_OBJECT_0: //// TODO: Read from the shared buffer//printf("Thread %d reading from buffer\n", GetCurrentThreadId());break; // An error occurreddefault: printf("Wait error (%d)\n", GetLastError()); return 0; }// Now that we are done reading the buffer, we could use another// event to signal that this thread is no longer reading. This// example simply uses the thread handle for synchronization (the// handle is signaled when the thread terminates.)printf("Thread %d exiting\n", GetCurrentThreadId());return 1;
}

上面示例使用事件对象来防止在主线程写入该缓冲区时从共享内存缓冲区读取多个线程。 首先,主线程使用 CreateEvent 函数创建初始状态为非签名的手动重置事件对象。 然后,它会创建多个读取器线程。 主线程执行写入操作,然后在完成写入后将事件对象设置为信号状态。

在开始读取操作之前,每个读取器线程都使用 WaitForSingleObject 等待手动重置事件对象发出信号。 当 WaitForSingleObject 返回时,这表示main线程已准备好开始其读取操作。

3.3.QWaitConditionEvent

QWaitConditionEvent实际是对CreateEvent的封装,代码如下:

class QWaitConditionEvent
{
public:inline QWaitConditionEvent() : priority(0), wokenUp(false){event = CreateEvent(NULL, TRUE, FALSE, NULL);}inline ~QWaitConditionEvent() { CloseHandle(event); }int priority;bool wokenUp;HANDLE event;
};

这个定义源码在.\Qt\Qt5.12.12\5.12.12\Src\qtbase\src\corelib\thread\qwaitcondition_win.cpp中,从中可以看出没生成一个QWaitConditionEvent就会创建一个手动重置事件对象。

3.4.QWaitConditionEventPrivate

QWaitConditionEventPrivate的定义如下:

typedef QList<QWaitConditionEvent *> EventQueue;class QWaitConditionPrivate
{
public:QMutex mtx;EventQueue queue;EventQueue freeQueue;QWaitConditionEvent *pre();bool wait(QWaitConditionEvent *wce, unsigned long time);void post(QWaitConditionEvent *wce, bool ret);
};

上面代码定义了两个事件队列,一个是等待事件队列,一个空闲时间队列;还定义了3个对事件队列操作的接口,下面说明各接口的用法:

1)  pre() :  从空闲队列中freeQueue取出一个事件对象 QWaitConditionEvent放入queue。

2)wait() : 在事件对象wce上等待time时间,该函数会阻塞当前线程的运行,直到time到或SetEvent。

3)  post() : 把使用后的QWaitConditionEvent归还到空事件队列freeQueue里面。

3.5.流程分析

QWaitCondition的d指针是QWaitConditionEventPrivate,对QWaitCondition的操作转换为对QWaitConditionEventPrivate的操作。关键步骤流程如下:

1)wait函数执行流程

2)wakeOne函数执行流程

3) wakeAll函数执行流程

wakeAll的流程同wakeOne的流程相似,只是wakeOne是把事件队列的第一个事件对象SetEvent,而wakeAll是把事件队列中的所有事件对象SetEvent。

QWaitCondition类的设计思想也遵循Qt大部分类的设计思想Pimpl技法,关于Pimpl技法的一些详细介绍,可参考我的博客C++之Pimpl惯用法-CSDN博客

4.总结

QMutex 和 QWaitCondition 联合使用是多线程中的一个常用的习惯用法,不仅是 Qt,对于 C++ 的 std::condition_variable 和 std::mutex ,以及 java 的 synchronized / wait / notify 也都适用。

参考:

createEventA 函数 (synchapi.h) - Win32 apps | Microsoft Learn

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

服务降级(Sentinel)

服务降级 采用 SentinelResource 注解方式实现&#xff0c; 必要的 依赖必须引入 以及 切面Bean 接口代码 RequestMapping("/degrade")SentinelResource(value DEGRADE_RESOURCE_NAME, blockHandler "blockHandlerForDegrade",entryType EntryType.IN…

数学实验第三版(主编:李继成 赵小艳)课后练习答案(十)(2)(3)

实验十&#xff1a;非线性函数极值求解 练习二 1.求解极值问题: (1) s.t. function [c,ceq]fun(x) c(1)-(25-x(1)^2-x(2)^2); c(2)-(7-x(1)^2x(2)^2); ceq0;换一个窗口运行下面的程序&#xff1a; clc;clear; f(x)-2*x(1)-x(2); a[]; b[]; aeq[];beq[]; u[5;10]; l[0;0];…

Spring Cloud Hystrix 参数配置、简单使用、DashBoard

Spring Cloud Hystrix 文章目录 Spring Cloud Hystrix一、Hystrix 服务降级二、Hystrix使用示例三、OpenFeign Hystrix四、Hystrix参数HystrixCommand.Setter核心参数Command PropertiesFallback降级配置Circuit Breaker 熔断器配置Metrix 健康统计配置Request Context 相关参数…

React Native开发iOS实战录

文章目录 背景环境准备基础工具&#xff1a;xcode安装主要工具安装CocoaPods 基本步骤采用Expo go运行iOS模拟器运行安装在真机上测试发布到苹果商店 原生模块与编译链接问题静态库和 Frameworkuse_frameworks!和use_modular_headers! 常见问题ruby3在macOS上编译失败import of…

如何在Django中使用分布式定时任务并结合消息队列

如何在Django中使用分布式定时任务并结合消息队列 如何在Django中使用分布式定时任务并结合消息队列项目背景与意义实现步骤1. 安装Celery和Django-celery-beat2. 配置Celery3. 配置Django-celery-beat4. 定义定时任务5. 启动Celery worker 和 beat6. Celery 指令7. 对接消息队…

【数据结构】LRU Cache

文章目录 LRUCache LRUCache 1. LRUCache是一种缓存的替换技术&#xff0c;在CPU和main memory之间根据计算机的局部性原理&#xff0c;往往会采用SRAM技术来构建CPU和主存之间的高速缓存&#xff0c;DRAM(dynamic random access memory)用于构建主存&#xff0c;LRUCache这种…

WEB APIs(1)

变量声明const&#xff08;修饰常量&#xff09; const优先&#xff0c;如react&#xff0c;基本const&#xff0c; 对于引用数据类型&#xff0c;可用const声明&#xff0c;因为储存的是地址 何为APIs 可以使用js操作HTML和浏览器 分类&#xff1a;DOM&#xff08;文档对象…

Golang快速入门到实践学习笔记

Go学习笔记 1.基础 Go程序设计的一些规则 Go之所以会那么简洁&#xff0c;是因为它有一些默认的行为&#xff1a; 大写字母开头的变量是可导出的&#xff0c;也就是其它包可以读取 的&#xff0c;是公用变量&#xff1b;小写字母开头的就是不可导出的&#xff0c;是私有变量…

[WinForm开源]概率计算器 - Genshin Impact(V1.0)

创作目的&#xff1a;为方便旅行者估算自己拥有的纠缠之缘能否达到自己的目的&#xff0c;作者使用C#开发了一款小型软件供旅行者参考使用。 创作说明&#xff1a;此软件所涉及到的一切概率与规则完全按照游戏《原神》(V4.4.0)内公示的概率与规则&#xff08;包括保底机制&…

Virt a Mate(VAM)游戏折腾记录

如有更新见原文&#xff1a;https://blog.iyatt.com/?p13283 1 前言 如果在网上看到有些视频名字带有 VAM 的&#xff0c;可能就是玩这个游戏录屏的。这个游戏可以建模、操作模型动作、构建场景等等。之前大致知道有这么个东西&#xff0c;只是电脑配置太差了&#xff0c;新…

文件上传-第三方服务阿里云OSS

JAVA后端实现文件上传,比如图片上床功能,有很多实现方案,可以将图片保存到服务器的硬盘上。也可以建立分布式集群,专门的微服务来存储文件常见的技术比如Minio。对于中小型公司&#xff0c;并且上传文件私密性不高的话可以使用第三方的存储服务&#xff0c;比如阿里云、华为云等…

投资银行在网络安全生态中的作用

文章目录 一、投资银行的含义(一)并购买方。(二)并购卖方。(三)IPO辅助。(四)投资银行业务的另一方面是帮助这些交易融资。二、从投资银行角度看网络安全产业(一)行业的短期前景三、复杂的网络安全并购(一)行业知识对投资银行业务很重要(二)在网络安全领域,技术…

文案馆头像壁纸微信小程序源码【支持流量主】

文案馆头像壁纸微信小程序源码【支持流量主】 源码介绍&#xff1a;文案馆头像壁纸微信小程序源码是一款可以获取套图、头像、壁纸的小程序。小程序源码内置流量主功能 需求环境&#xff1a;微信小程序phpmysql 下载地址&#xff1a; https://www.changyouzuhao.cn/13453.ht…

分析一个项目(微信小程序篇)二

目录 首页&#xff1a; 发现&#xff1a; 购物车&#xff1a; 我的&#xff1a; 分析一个项目讲究的是如何进行对项目的解析分解&#xff0c;进一步了解项目的整体结构&#xff0c;熟悉项目的结构&#xff0c;能够知道每个组件所处在哪个位置&#xff0c;发挥什么作用。 接…

(三十八)大数据实战——Atlas元数据管理平台的部署安装

前言 Apache Atlas 是一个开源的数据治理和元数据管理平台&#xff0c;旨在帮助组织有效管理和利用其数据资产。为组织提供开放式元数据管理和治理功能 &#xff0c;用以构建其数据资产目录&#xff0c;对这些资产进行分类和管理&#xff0c;形成数据字典 。并为数据分析师和数…

JVM(4)原理篇

1 栈上的数据存储 在Java中有8大基本数据类型&#xff1a; 这里的内存占用&#xff0c;指的是堆上或者数组中内存分配的空间大小&#xff0c;栈上的实现更加复杂。 以基础篇的这段代码为例&#xff1a; Java中的8大数据类型在虚拟机中的实现&#xff1a; boolean、byte、char…

基于LightGBM的回归任务案例

在本文中&#xff0c;我们将学习先进的机器学习模型之一&#xff1a;Lightgbm。在对XGB模型进行了越来越多的改进以获得更好的性能之后&#xff0c;XGBoost是一种极限梯度提升机器&#xff0c;但通过lightgbm&#xff0c;我们可以在没有太多计算的情况下实现类似或更好的结果&a…

洛谷C++简单题小练习day11—字母转换,分可乐两个小程序

day11--字母转换--2.14 习题概述 题目描述 输入一个小写字母&#xff0c;输出其对应的大写字母。例如输入 q[回车] 时&#xff0c;会输出 Q。 代码部分 #include<bits/stdc.h> using namespace std; int main() { char n;cin>>n;cout<<char(n-32)<…

为自监督学习重构去噪扩散模型

在这项研究中&#xff0c;作者检验了最初用于图像生成的去噪扩散模型&#xff08;DDM&#xff09;的表示学习能力。其理念是解构DDM&#xff0c;逐渐将其转化为经典的去噪自动编码器&#xff08;DAE&#xff09;。这一解构过程让大家能够探索现代DDM的各个组成部分如何影响自监…

18 19 SPI接口的74HC595驱动数码管实验

1. 串行移位寄存器原理&#xff08;以四个移位寄存器为例&#xff09; 1. 通过移位寄存器实现串转并&#xff1a;一个数据输入端口可得到四位并行数据。 通过给data输送0101数据&#xff0c;那么在经过四个时钟周期后&#xff0c;与data相连的四个寄存器的输出端口得到了0101…