当前位置: 首页 > news >正文

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

相关文章:

Qt之条件变量QWaitCondition详解

QWaitCondition内部实现结构图&#xff1a; 相关系列文章 C之Pimpl惯用法 目录 1.简介 2.示例 2.1.全局配置 2.2.生产者Producer 2.3.消费者Consumer 2.4.测试例子 3.原理分析 3.1.辅助函数CreateEvent 3.2.辅助函数WaitForSingleObject 3.3.QWaitConditionEvent …...

作为国产大模型之光的智谱AI,究竟推出了多少模型?一篇文章带你详细了解!

虽然OpenAI发布了一系列基于GPT模型的产品&#xff0c;在不同领域取得了很高的成就。但是作为LLM领域绝对的领头羊&#xff0c;OpenAI没有按照其最初的Open初衷行事。无论是ChatGPT早期采用的GPT3&#xff0c;还是后来推出的GPT3.5和GPT4模型&#xff0c;OpenAI都因为担心被滥用…...

学习转置矩阵

转置矩阵 将矩阵的行列互换得到的新矩阵称为转置矩阵 输入描述&#xff1a; 第一行包含两个整数n和m&#xff0c;表示一个矩阵包含n行m列&#xff0c;用空格分隔。 (1≤n≤10,1≤m≤10) 从2到n1行&#xff0c;每行输入m个整数&#xff08;范围-231~231-1&#xff09;&#x…...

AJAX——常用请求方法

1 请求方法 请求方法&#xff1a;对服务器资源&#xff0c;要执行的操作 2 数据提交 场景&#xff1a;当数据需要在服务器上保存 3 axios请求配置 url&#xff1a;请求的URL网址 method&#xff1a;请求的方法&#xff0c;GET可以省略&#xff08;不区分大小写&#xff09; …...

sqlserver2012 解决日志大的问题

当SQL Server 2012的事务日志变得过大时&#xff0c;这通常意味着日志备份没有被定期执行&#xff0c;或者日志文件的自动增长设置被设置得太高&#xff0c;导致它不断增长以容纳所有未备份的事务。解决日志大的问题通常涉及以下几个步骤&#xff1a; 备份事务日志&#xff1a;…...

Vue3快速上手(三)Composition组合式API及setup用法

一、Vue2的API风格 Vue2的API风格是Options API,也叫配置式API。一个功能的数据&#xff0c;交互&#xff0c;计算&#xff0c;监听等都是分别配置在data, methods&#xff0c;computed, watch等模块里的。如下&#xff1a; <template><div class"person"…...

【C++】内存五大区详解

&#x1f490; &#x1f338; &#x1f337; &#x1f340; &#x1f339; &#x1f33b; &#x1f33a; &#x1f341; &#x1f343; &#x1f342; &#x1f33f; &#x1f344;&#x1f35d; &#x1f35b; &#x1f364; &#x1f4c3;个人主页 &#xff1a;阿然成长日记 …...

Django学习笔记教程全解析:初步学习Django模型,初识API,以及Django的后台管理系统(Django全解析,保姆级教程)

把时间用在思考上是最能节省时间的事情。——[美]卡曾斯 导言 写在前面 本文部分内容引用的是Django官方文档&#xff0c;对官方文档进行了解读和理解&#xff0c;对官方文档的部分注释内容进行了翻译&#xff0c;以方便大家的阅读和理解。 概述 在上一篇文章里&#xff0…...

Python学习之路-爬虫提高:selenium

Python学习之路-爬虫提高:selenium 什么是selenium Selenium是一个Web的自动化测试工具&#xff0c;最初是为网站自动化测试而开发的&#xff0c;Selenium 可以直接运行在浏览器上&#xff0c;它支持所有主流的浏览器&#xff08;包括PhantomJS这些无界面的浏览器&#xff09…...

Rust基础拾遗--进阶

Rust基础拾遗 前言1.结构体1.1 具名字段型结构体1.2 元组型结构体1.3 单元型结构体1.4 结构体布局1.5 用impl定义方法1.5.1 以Box、Rc或Arc形式传入self1.5.2 类型关联函数 1.6 关联常量1.7 泛型结构体1.8 带生命周期参数的泛型结构体1.9 带常量参数的泛型结构体1.10 让结构体类…...

数据同步工具对比——SeaTunnel 、DataX、Sqoop、Flume、Flink CDC

在大数据时代&#xff0c;数据的采集、处理和分析变得尤为重要。业界出现了多种工具来帮助开发者和企业高效地处理数据流和数据集。本文将对比五种流行的数据处理工具&#xff1a;SeaTunnel、DataX、Sqoop、Flume和Flink CDC&#xff0c;从它们的设计理念、使用场景、优缺点等方…...

随机过程及应用学习笔记(四) 马尔可夫过程

马尔可夫过程是理论上和实际应用中都十分重要的一类随机过程。 目录 前言 一、马尔可夫过程的概念 二、离散参数马氏链 1 定义 2 齐次马尔可夫链 3 齐次马尔可夫链的性质 三、齐次马尔可夫链状态的分类 四、有限马尔可夫链 五、状态的周期性 六、极限定理 七、生灭过…...

prometheus

文章目录 一、Prometheus简介什么是Prometheus&#xff1f;Prometheus的优势Prometheus的组件、架构Prometheus适用于什么场景Prometheus不适合什么场景 二、相关概念数据模型指标名称和标签样本表示方式 指标类型Counter计数器Gauge仪表盘Histogram直方图Summary摘要 Jobs和In…...

Vi 和 Vim 编辑器

Vi 和 Vim 编辑器 vi 和 vim 的基本介绍 Linux 系统会内置 vi 文本编辑器 Vim 具有程序编辑的能力&#xff0c;可以看做是 Vi 的增强版本&#xff0c;可以主动的以字体颜色辨别语法的正确性&#xff0c;方便程序设计。 代码补完、编译及错误跳转等方便编程的功能特别丰富&…...

算法沉淀——队列+宽度优先搜索(BFS)(leetcode真题剖析)

算法沉淀——队列宽度优先搜索&#xff08;BFS&#xff09; 01.N 叉树的层序遍历02.二叉树的锯齿形层序遍历03.二叉树最大宽度04.在每个树行中找最大值 队列 宽度优先搜索算法&#xff08;Queue BFS&#xff09;是一种常用于图的遍历的算法&#xff0c;特别适用于求解最短路径…...

编辑器的新选择(基本不用配置)

Cline 不用看网上那些教程Cline几乎不用配置。 点击设置直接选择Chinese, C直接在选择就行了。 Cline是一个很好的编辑器&#xff0c;有很多懒人必备的功能。 Lightly 这是一个根本不用配置的C编辑器。 旁边有目录&#xff0c;而且配色也很好&#xff0c;语言标准可以自己…...

算法沉淀——栈(leetcode真题剖析)

算法沉淀——栈 01.删除字符串中的所有相邻重复项02.比较含退格的字符串03.基本计算器 II04.字符串解码05.验证栈序列 栈&#xff08;Stack&#xff09;是一种基于先进后出&#xff08;Last In, First Out&#xff0c;LIFO&#xff09;原则的数据结构。栈具有两个主要的操作&am…...

耳机壳UV树脂制作私模定制耳塞需要注意什么问题?

制作私模定制耳塞需要注意以下问题&#xff1a; 耳模制作&#xff1a;获取准确的耳模是制作私模定制耳塞的关键步骤。需要使用合适的材料和方法&#xff0c;确保耳模的准确性和稳定性。材料选择&#xff1a;选择合适的UV树脂和其它相关材料&#xff0c;确保它们的质量和性能符…...

easyx搭建项目-永七大作战(割草游戏)

永七大作战 游戏介绍&#xff1a; 永七大作战 游戏代码链接&#xff1a;永七大作战 提取码&#xff1a;ABCD 不想水文了&#xff0c;直接献出源码&#xff0c;表示我的诚意...

nginx命名location跳转的模块上下文继承

目录 1. 缘起2. 解决方案2.1 保留指定模块的上下文信息2.2 获取指定模块的上下文信息2.3 设置指定模块的上下文信息2.4 设置模块上下文是否需要继承标记2.5 对openrety lua代码的支持 1. 缘起 nginx提供了非常棒的功能&#xff0c;命名location&#xff0c;如文章nginx的locati…...

[2025CVPR]DeepVideo-R1:基于难度感知回归GRPO的视频强化微调框架详解

突破视频大语言模型推理瓶颈,在多个视频基准上实现SOTA性能 一、核心问题与创新亮点 1.1 GRPO在视频任务中的两大挑战 ​安全措施依赖问题​ GRPO使用min和clip函数限制策略更新幅度,导致: 梯度抑制:当新旧策略差异过大时梯度消失收敛困难:策略无法充分优化# 传统GRPO的梯…...

手游刚开服就被攻击怎么办?如何防御DDoS?

开服初期是手游最脆弱的阶段&#xff0c;极易成为DDoS攻击的目标。一旦遭遇攻击&#xff0c;可能导致服务器瘫痪、玩家流失&#xff0c;甚至造成巨大经济损失。本文为开发者提供一套简洁有效的应急与防御方案&#xff0c;帮助快速应对并构建长期防护体系。 一、遭遇攻击的紧急应…...

模型参数、模型存储精度、参数与显存

模型参数量衡量单位 M&#xff1a;百万&#xff08;Million&#xff09; B&#xff1a;十亿&#xff08;Billion&#xff09; 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的&#xff0c;但是一个参数所表示多少字节不一定&#xff0c;需要看这个参数以什么…...

《Qt C++ 与 OpenCV:解锁视频播放程序设计的奥秘》

引言:探索视频播放程序设计之旅 在当今数字化时代,多媒体应用已渗透到我们生活的方方面面,从日常的视频娱乐到专业的视频监控、视频会议系统,视频播放程序作为多媒体应用的核心组成部分,扮演着至关重要的角色。无论是在个人电脑、移动设备还是智能电视等平台上,用户都期望…...

从零实现富文本编辑器#5-编辑器选区模型的状态结构表达

先前我们总结了浏览器选区模型的交互策略&#xff0c;并且实现了基本的选区操作&#xff0c;还调研了自绘选区的实现。那么相对的&#xff0c;我们还需要设计编辑器的选区表达&#xff0c;也可以称为模型选区。编辑器中应用变更时的操作范围&#xff0c;就是以模型选区为基准来…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

C++.OpenGL (10/64)基础光照(Basic Lighting)

基础光照(Basic Lighting) 冯氏光照模型(Phong Lighting Model) #mermaid-svg-GLdskXwWINxNGHso {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-GLdskXwWINxNGHso .error-icon{fill:#552222;}#mermaid-svg-GLd…...

数据库分批入库

今天在工作中&#xff0c;遇到一个问题&#xff0c;就是分批查询的时候&#xff0c;由于批次过大导致出现了一些问题&#xff0c;一下是问题描述和解决方案&#xff1a; 示例&#xff1a; // 假设已有数据列表 dataList 和 PreparedStatement pstmt int batchSize 1000; // …...

Python ROS2【机器人中间件框架】 简介

销量过万TEEIS德国护膝夏天用薄款 优惠券冠生园 百花蜂蜜428g 挤压瓶纯蜂蜜巨奇严选 鞋子除臭剂360ml 多芬身体磨砂膏280g健70%-75%酒精消毒棉片湿巾1418cm 80片/袋3袋大包清洁食品用消毒 优惠券AIMORNY52朵红玫瑰永生香皂花同城配送非鲜花七夕情人节生日礼物送女友 热卖妙洁棉…...

GitFlow 工作模式(详解)

今天再学项目的过程中遇到使用gitflow模式管理代码&#xff0c;因此进行学习并且发布关于gitflow的一些思考 Git与GitFlow模式 我们在写代码的时候通常会进行网上保存&#xff0c;无论是github还是gittee&#xff0c;都是一种基于git去保存代码的形式&#xff0c;这样保存代码…...