Qt之条件变量QWaitCondition详解(从使用到原理分析全)
QWaitCondition内部实现结构图:
相关系列文章
C++之Pimpl惯用法
目录
1.简介
2.示例
2.1.全局配置
2.2.生产者Producer
2.3.消费者Consumer
2.4.测试例子
3.原理分析
3.1.源码介绍
3.2.辅助函数CreateEvent
3.3.辅助函数WaitForSingleObject
3.4.QWaitConditionEvent
3.5.QWaitConditionEventPrivate
3.6.流程分析
4.总结
1.简介
QWaitCondition
是用来同步线程的条件变量,头文件<QWaitCondition>
,类中的所有函数都是线程安全的。主要的公共函数(以Qt5.12.12为例)如下表:
返回类型 | 函数名称 | 含义 |
QWaitCondition () | 构造函数 | |
---|---|---|
~QWaitCondition () | 析构函数 | |
bool | wait ( QMutex * mutex, unsigned long time = ULONG_MAX ) | mutex将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来: (1)另一个线程使用wakeOne()或wakeAll()传输给它; (2)time毫秒过去。 |
bool | wait(QMutex *lockedMutex, QDeadlineTimer deadline) | 同上 |
bool | wait ( QReadWriteLock * readWriteLock, unsigned long time = ULONG_MAX ) | readWriteLock将被解锁,并且调用线程将会阻塞,直到下列条件之一满足才想来: (1)另一个线程使用wakeOne()或wakeAll()传输给它; (2)time毫秒过去。 |
bool | wait(QReadWriteLock *lockedReadWriteLock, QDeadlineTimer deadline) | 同上 |
void | wakeAll () | 唤醒所有等待的线程,线程唤醒的顺序不确定,由操作系统的调度策略决定 |
void | wakeOne() | 唤醒等待QWaitCondition的线程中的一个线程,线程唤醒的顺序不确定,由操作系统的调度策略决定 |
void | notify_all() | 同wakeAll() |
void | notify_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.源码介绍
序号 | 文件 | 说明 | 源码位置 |
---|---|---|---|
1 | qwaitcondition.h | 声明QWaitCondition | .\5.12.12\Src\qtbase\src\corelib\thread |
2 | qwaitcondition_win.cpp | 定义QWaitConditionEvent,QWaitConditionPrivate,QWaitCondition的windows实现 | .\5.12.12\Src\qtbase\src\corelib\thread |
3 | qwaitcondition_unix.cpp | 定义QWaitConditionEvent,QWaitConditionPrivate,QWaitCondition的unix实现 | .\5.12.12\Src\qtbase\src\corelib\thread |
4 | qwaitcondition.h | 对外访问包含的头文件 | .\5.12.12\Src\qtbase\include\QtCore |
3.2.辅助函数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.3.辅助函数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,等待成功,拥有该Mutex | HANDLE |
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.4.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.5.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.6.流程分析
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
相关文章:

Qt之条件变量QWaitCondition详解(从使用到原理分析全)
QWaitCondition内部实现结构图: 相关系列文章 C之Pimpl惯用法 目录 1.简介 2.示例 2.1.全局配置 2.2.生产者Producer 2.3.消费者Consumer 2.4.测试例子 3.原理分析 3.1.源码介绍 3.2.辅助函数CreateEvent 3.3.辅助函数WaitForSingleObject 3.4.QWaitCo…...

OpenSource - 一站式自动化运维及自动化部署平台
文章目录 orion-ops 是什么重构特性快速开始技术栈功能预览添砖加瓦License orion-ops 是什么 orion-ops 一站式自动化运维及自动化部署平台, 使用多环境的概念, 提供了机器管理、机器监控报警、Web终端、WebSftp、机器批量执行、机器批量上传、在线查看日志、定时调度任务、应…...

【后端高频面试题--设计模式下篇】
🚀 作者 :“码上有前” 🚀 文章简介 :后端高频面试题 🚀 欢迎小伙伴们 点赞👍、收藏⭐、留言💬 后端高频面试题--设计模式下篇 往期精彩内容设计模式总览模板方法模式怎么理解模板方法模式模板方…...

这才是大学生该做的副业,别再痴迷于游戏了!
感谢大家一直以来的支持和关注,尤其是在我的上一个公众号被关闭后,仍然选择跟随我的老粉丝们,你们的支持是我继续前行的动力。为了回馈大家长期以来的陪伴,我决定分享一些实用的干货,这些都是我亲身实践并且取得成功的…...

Ubuntu20.04 安装jekyll
首先使根据官方文档安装:Jekyll on Ubuntu | Jekyll • Simple, blog-aware, static sites 如果没有报错,就不用再继续看下去了。 我这边在执行gem install jekyll bundler时报错,所以安装了rvm,安装rvm可以参考这篇文章Ubuntu …...

AWK语言
一. awk awk:报告生成器,格式化输出。 在 Linux/UNIX 系统中,awk 是一个功能强大的编辑工具,逐行读取输入文本,默认以空格或tab键作为分隔符作为分隔,并按模式或者条件执行编辑命令。而awk比较倾向于将一行…...

精通Nmap:网络扫描与安全的终极武器
一、引言 Nmap,即NetworkMapper,是一款开源的网络探测和安全审计工具。它能帮助您发现网络中的设备,并识别潜在的安全风险。在这个教程中,我们将一步步引导您如何有效地使用Nmap,让您的网络更加安全。 因为Nmap还有图…...

Java 学习和实践笔记(11)
三大神器: 官方网址: http://www.jetbrains.com/idea/ 官方网址: https://code.visualstudio.com/ 官方网址: http://www.eclipse.org 装好了idea社区版,并试运行以下代码,OK! //TIP To <b>Run</b> code, press &l…...

开发实体类
开发实体类之间先在pom文件中加入该依赖 <!-- 开发实体类--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope></dependency>我们在实体类中声明各个属…...

人工智能学习与实训笔记(十五):Scikit-learn库的基础与使用
人工智能专栏文章汇总:人工智能学习专栏文章汇总-CSDN博客 本篇目录 一、介绍 1. 1 Scikit-learn的发展历程及定义 1.2 理解算法包、算法库及算法框架之间的区别和联系 二、Scikit-learn官网结构 三、安装与设置 3.1 Python环境的安装与配置 3.2 Scikit-lea…...

插值与拟合算法介绍
在数据处理和科学计算领域,插值与拟合是两种极为重要的数据分析方法。它们被广泛应用于信号处理、图像处理、机器学习、金融分析等多个领域,对于理解和预测数据趋势具有至关重要的作用。本文将深入浅出地介绍这两种算法的基本原理,并结合C语言编程环境探讨如何在CSDN开发者社…...

下一代Windows系统曝光:基于GPT-4V,Agent跨应用调度,代号UFO
下一代Windows操作系统提前曝光了?? 微软首个为Windows而设的智能体(Agent) 亮相: 基于GPT-4V,一句话就可以在多个应用中无缝切换,完成复杂任务。整个过程无需人为干预,其执行成功…...

二.自定义头文件
一.Worker.h 1.1概述 - 类名:Worker - 继承关系:所有其他类(Employee、Manager、Boss)都继承自该抽象类 - 头文件保护:使用 pragma once 防止头文件重复包含 - 引入标准库:包含 <iostream> 和 <st…...

【AIGC】Stable Diffusion之模型微调工具
推荐一款好用的模型微调工具,cybertron furnace 是一个lora训练整合包,提供训练 lora 模型的工具集或环境。集成环境包括必要的依赖项和配置文件、预训练脚本,支持人物、二次元、画风、自定义lora的训练,以简化用户训练 lora 模型…...

探索未来科技前沿:深度学习的进展与应用
深度学习的进展 摘要:深度学习作为人工智能领域的重要分支,近年来取得了巨大的进展,并在各个领域展现出惊人的应用潜力。本文将介绍深度学习的发展历程、技术原理以及在图像识别、自然语言处理等领域的应用,展望深度学习在未来的…...

PTA | Wifi密码
下面是微博上流传的一张照片:“各位亲爱的同学们,鉴于大家有时需要使用 wifi,又怕耽误亲们的学习,现将 wifi 密码设置为下列数学题答案:A-1;B-2;C-3;D-4;请同学们自己作答…...

Linux中gdb使用说明书
首先我们要使用gdb,必须明白gdb使用范围: 要使用gdb调试,必须在源代码生成二进制程序的时候, 加上 -g 选项(gcc/g) 其次,我们就要来学习gdb使用的一些命令了: list/l 行号:显…...

LInux——开发工具的使用
目录 Linux软件包管理器 yum rzsz Linux编辑器——vim vim的使用 vim的基本操作 命令模式的常见命令 底行模式的常见命令 vim是需要配置的 Linux编译器——gcc/g 预处理 编译 汇编 链接 函数库 Linux项目自动化构建工具 make/makefile make原理 项目清理 Linux调试器g…...

沁恒CH32V30X学习笔记03--64位systick
systick CH32F2x 系列产品Cortex-M3 内核自带了一个 24 位自减型计数器(SysTick timer)。支持 HCLK 或 HCLK/8 作为时基,具有较高优先级别(6)。一般可用于操作系统的时基。 CH32V3x 系列产品内核自带了一个 64 位加减计数器(SysTick),支持 HCLK 或者 HCLK/8 作为时基,…...

【JavaEE】IP协议
作者主页:paper jie_博客 本文作者:大家好,我是paper jie,感谢你阅读本文,欢迎一建三连哦。 本文于《JavaEE》专栏,本专栏是针对于大学生,编程小白精心打造的。笔者用重金(时间和精力)打造&…...
计算机网络-数据通信基础
目录 前言 一、数据通信基本概念 二、数据通信相关知识1 总结 前言 正在学习计算机网络体系,把每日所学的知识梳理出来,既能够当作读书笔记,又能分享出来和大家一同学习讨论。 一、数据通信基本概念 基本概念:信源、信道、信宿&…...

【lesson53】线程控制
文章目录 线程控制 线程控制 线程创建 代码: 运行代码: 强调一点,线程和进程不一样,进程有父进程的概念,但在线程组里面,所有的线程都是对等关系。 错误检查: 传统的一些函数是,成功返回0&…...

TypeScript(一):TypeScript基本理解
TypeScript基本理解 为什么使用TS JavaScript发展至今,没有进行数据类型的验证而我们知道,在编程阶段,错误发现的越早越好而TS就解决了JS的这个问题 认识TypeScript TypeScript是拥有类型的JavaScript超级,它可以编译成普通、…...

C语言—指针
碎碎念:做指针题的时候我仿佛回到了原点,总觉得目的是为了把框架搭建起来,我胡说的哈31 1.利用指针变量将一个数组中的数据反向输出。 /*1.利用指针变量将一个数组中的数据反向输出。*/#include <stdio.h> #include <time.h> #include <…...

c++作业
Shell中的函数(先调用后使用的原则)(没有申明) (Function) 函数名(有没有参数根据调用格式)(不能写任何内容) { 函数体 Return 返回值 } 函数名 ----》…...

什么是tomcat?tomcat是干什么用的?
前言 Tomcat是一个开源的、轻量级的应用服务器,是Apache软件基金会的一个项目。它实现了Java Servlet、JavaServer Pages(JSP)和Java Expression Language(EL)等Java技术,用于支持在Java平台上运行的动态W…...

中科院一区论文复现,改进蜣螂算法,Fuch映射+反向学习+自适应步长+随机差分变异,MATLAB代码...
本期文章复现一篇发表于2024年来自中科院一区TOP顶刊《Energy》的改进蜣螂算法。 论文引用如下: Li Y, Sun K, Yao Q, et al. A dual-optimization wind speed forecasting model based on deep learning and improved dung beetle optimization algorithm[J]. Ener…...

C# 如何实现一个事件总线
EventBus(事件总线)是一种用于在应用程序内部或跨应用程序组件之间进行事件通信的机制。 它允许不同的组件通过发布和订阅事件来进行解耦和通信。在给定的代码片段中,我们可以看到一个使用C#实现的Event Bus。它定义了一些接口和类来实现事件…...

Python学习路线图
防止忘记,温故知新 进阶路线...

作业2.14
chgrp: 只能修改文件的所属组 chgrp 新的组 文件名 要求:修改的目标组已经存在 chown: chown 新的用户名 文件名 sudo chown root :1 将文件1的所属组用户和所属组用户都改为root sudo chown root:ubuntu 1 将文件1的所属用户…...