lee-romantic 's Blog
Learning is a journey, not a destination.
Toggle navigation
lee-romantic 's Blog
主页
About Me
归档
标签
部分Qt+OpenGL+CUDA+多线程问题总结
2025-11-12 12:34:59
2
0
0
lee-romantic
# XdmaDriver - 简介 XDMA Driver(Xilinx DMA Driver)是赛灵思(Xilinx)公司提供的一种PCI Express(PCIe)DMA(直接内存访问)驱动程序,主要用于在FPGA(现场可编程门阵列)和主机(PC/服务器)之间实现高速数据传输。它支持Xilinx FPGA上的XDMA IP核,提供用户空间和内核空间的DMA访问能力,适用于高性能计算、数据采集、网络加速等应用场景。 - 主要功能: - PCIe 通信支持 - DMA直接访问 - 多进程/多用户支持(高级功能) - 用户空间零拷贝优化(如 DPDK 集成) # CVI - CVI(LabWindows/CVI)是 National Instruments(NI) 公司推出的一款 ANSI C 集成开发环境(IDE),专为 测试测量、自动化控制和仪器通信 设计。它结合了 C 语言的高效性和图形化编程的便捷性,广泛应用于工业自动化、数据采集、硬件控制等领域。 - CVI 的核心特点 - 基于 ANSI C,但扩展了仪器控制功能,支持标准 C 语法,但增加了专门的库(如 VISA、GPIB、DAQmx)用于仪器通信和数据采集。适用于需要高性能、低延迟的嵌入式或实时系统开发。 - 图形化用户界面(GUI)设计,提供拖拽式 UI 编辑器,可快速创建 面板(Panel)、按钮、图表 等控件,类似 LabVIEW 但基于代码。 适合开发 仪器控制软件、数据监控系统 等交互式应用。 - 与 NI 硬件深度集成,支持 NI 的数据采集卡(如 PCIe/PXI DAQ)、GPIB 设备、PXI 模块等。可通过 NI-DAQmx、VISA 等驱动直接控制硬件。跨平台兼容性(Windows/Linux),主要运行在 Windows,但部分功能支持 Linux(需 NI Linux Real-Time 系统)。 - 调试与性能分析工具,内置调试器、内存检查工具,适合开发高可靠性应用。 - CVI 的典型应用场景 - 自动化测试系统(如生产线上的仪器控制) - 数据采集与信号处理(如传感器数据实时分析) - 工业控制与监控(如 PLC 通信、HMI 开发) - 科研仪器开发(如光谱仪、示波器控制软件) # OpenGL中,基于纹理、PBO,CUDA实现高速图像渲染的基本处理流程 - 1、在OpenGL中结合纹理(Texture)、PBO(Pixel Buffer Object)和CUDA实现高速图像渲染,可以充分利用GPU的并行计算和内存零拷贝优势。 - 2、基本流程: `图像数据源->CUDA处理->PBO映射->纹理绑定->OpenGL渲染` - CUDA: 负责图像处理(如滤波、增强、AI推理)。 - PBO:作为OpenGL与CUDA共享的内存桥梁,避免数据拷贝。 - 纹理:最终绑定到OpenGL用于渲染。 - 3、详细处理流程 - 步骤1:初始化OpenGL资源 ``` // 创建纹理 GLuint textureID; glGenTextures(1, &textureID); glBindTexture(GL_TEXTURE_2D, textureID); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // 创建PBO(双缓冲避免等待) GLuint pboIDs[2]; glGenBuffers(2, pboIDs); for (int i = 0; i < 2; i++) { glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIDs[i]); glBufferData(GL_PIXEL_UNPACK_BUFFER, width * height * 4, NULL, GL_STREAM_DRAW); } ``` - 步骤2:注册PBO为CUDA资源 ``` cudaGraphicsResource* cudaPBOResources[2]; for (int i = 0; i < 2; i++) { cudaGraphicsGLRegisterBuffer(&cudaPBOResources[i], pboIDs[i], cudaGraphicsMapFlagsWriteDiscard); } ``` - 步骤3:CUDA处理与数据传递 ``` // 映射当前PBO到CUDA cudaGraphicsMapResources(1, &cudaPBOResources[currentPBO], 0); void* d_ptr; size_t size; cudaGraphicsResourceGetMappedPointer(&d_ptr, &size, cudaPBOResources[currentPBO]); // CUDA核函数处理图像(如滤波、格式转换) myCudaKernel<<<blocks, threads>>>(d_ptr, width, height, ...); // 解映射 cudaGraphicsUnmapResources(1, &cudaPBOResources[currentPBO], 0); ``` - 步骤4:PBO数据上传并更新纹理 ``` glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pboIDs[currentPBO]); glBindTexture(GL_TEXTURE_2D, textureID); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, NULL); // NULL表示从PBO中渲染 // 当 GL_PIXEL_UNPACK_BUFFER 绑定 PBO 时,glTexSubImage2D 的 pixels 参数会被忽略,数据直接从 PBO 读取。 //如果 pixels 不是 NULL,OpenGL 会从 CPU 内存读取数据,而不是 PBO。 ``` - 步骤5:OpenGL渲染 ``` // 绑定纹理并渲染(下面这样是旧版本OpenGL传统/立即模式下的代码,OpenGL3.0已废弃,现代OpenGL采用可编程管线,通过VBO,VAO和着色器来渲染图形) glBindTexture(GL_TEXTURE_2D, textureID); glBegin(GL_QUADS); glTexCoord2f(0, 0); glVertex2f(-1, -1); glTexCoord2f(1, 0); glVertex2f(1, -1); glTexCoord2f(1, 1); glVertex2f(1, 1); glTexCoord2f(0, 1); glVertex2f(-1, 1); glEnd(); // 交换PBO双缓冲 currentPBO = (currentPBO + 1) % 2; ``` - 3、关键优化技术 - 双PBO缓冲: - 异步传输:一个PBO用于CUDA写入时,另一个PBO用于OpenGL读取。 - CUDA-OpenGL互操作 : - 通过cudaGraphicsGLRegisterBuffer避免内存拷贝,实现零拷贝。 - 纹理压缩格式: - 使用GL_RGBA8或GL_BGRA等格式匹配CUDA输出,减少转换开销。 - 流式并行: - CUDA流(cudaStream_t)与OpenGL Fence同步,避免GPU空闲。 - 4、性能对比(传统vsCUDA-PBO) | 方法 | 数据传输延迟 | CPU占用率 | 适用场景 | | :------------------- | :---------: | :-------: | :----------------------: | | CPU处理+glTexImage2D | 高延迟 | 高占用 | 简单应用场景、低分辨率图像 | | PBO异步上传 | 中 | 中 | 中等实时性场景需求 |CUDA | CUDA+PBO零拷贝 | 低 | 低 | 高频图像处理(如4K@60fps) | - 5、完整代码示例(伪代码) ``` // 初始化 initGLTexture(); initPBO(); initCUDA(); while (rendering) { // CUDA处理 cudaProcessImage(pboIDs[currentPBO]); // 更新纹理,下面渲染时就会使用到textureID绑定的纹理 glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, pboIDs[currentPBO]); glBindTexture(GL_TEXTURE_2D, textureID); glGenerateMipmap(GL_TEXTURE_2D); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, image_width, image_height, GL_RGBA, GL_UNSIGNED_BYTE, NULL); glBindBuffer(GL_PIXEL_UNPACK_BUFFER_ARB, 0); // 直接像这样使用立即渲染模式(旧版OpenGL做法,已经不推荐) //glBindTexture(GL_TEXTURE_2D, textureID); //glBegin(GL_QUADS); // glTexCoord2f(0, 0); glVertex2f(-1, -1); // glTexCoord2f(1, 0); glVertex2f(1, -1); // glTexCoord2f(1, 1); glVertex2f(1, 1); // glTexCoord2f(0, 1); glVertex2f(-1, 1); //glEnd(); // 或者像这样使用VBO,VAO+着色器进行渲染(现代OpenGL的做法) ourShader.use(); glBindVertexArray(VAO); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 交换PBO swapPBOBuffers(); } ``` - 事实上,不只是PBO可以通过CUDA修改,VBO也是一样的思路。下面是一个OpenGL+CUDA互操作的完整例子: ``` #include <cuda_runtime.h> #include <GL/glew.h> int main() { // 1. 初始化OpenGL和CUDA GLuint vbo; glGenBuffers(1, &vbo); glBindBuffer(GL_ARRAY_BUFFER, vbo); glBufferData(GL_ARRAY_BUFFER, 1024, NULL, GL_DYNAMIC_DRAW); // 2. 注册VBO到CUDA cudaGraphicsResource_t cudaResource; cudaGraphicsGLRegisterBuffer(&cudaResource, vbo, cudaGraphicsMapFlagsWriteDiscard); // 3. 映射资源 cudaGraphicsMapResources(1, &cudaResource, 0); // 资源数量为1 // 4. 获取设备指针 float* devPtr; size_t size; cudaGraphicsResourceGetMappedPointer((void**)&devPtr, &size, cudaResource); // 5. CUDA内核修改数据 dim3 blocks(16, 16); dim3 threads(64); myKernel<<<blocks, threads>>>(devPtr, size); // 6. 取消映射 cudaGraphicsUnmapResources(1, &cudaResource, 0); // 7. 清理 cudaGraphicsUnregisterResource(cudaResource); glDeleteBuffers(1, &vbo); return 0; } ``` - 6、典型应用场景 - 实时视频处理:4K视频去噪、超分辨率。 - 深度学习渲染:AI生成图像实时显示(如Stable Diffusion)。 - 医学成像:超声/CT数据实时增强。 - 虚拟现实:高帧率环境下的动态纹理更新。 - 7、总结 - 通过CUDA+PBO+纹理的协同: - CUDA处理图像数据(并行计算高效)。 - PBO实现设备间零拷贝传输。 - 纹理绑定到OpenGL完成最终渲染。 - 此方案适合需要超低延迟和高吞吐量的图像处理场景,性能远超传统CPU上传方式。 # OpenGL中的双PBO缓冲优化 ## 简介 - PBO 是一种缓冲对象,用于高效处理像素数据的上传(从CPU到GPU)或回读(从GPU到CPU)。而在OpenGL中,双PBO(Pixel Buffer Object)缓冲是一种通过异步数据传输优化像素操作(如屏幕截图、纹理更新或GPU计算结果的回读)的技术。其核心思想是重叠数据传输与计算,减少CPU等待时间,提升性能。 - 传统方式存在的问题: 使用 glReadPixels 或 glTexImage2D 直接操作像素时,CPU 必须等待 GPU 完成渲染并同步传输数据,导致性能瓶颈。 - PBO 的改进: 将数据传输任务交给 DMA(直接内存访问),CPU 和 GPU 可并行工作。 ## 乒乓切换 - 双PBO通过乒乓切换(Ping-Pong)实现并行化。例如在从帧缓冲区回读像素(如截图)的情况下,具体流程为: - 1、初始化:创建两个PBO(PBO1 和 PBO2),分别用于交替传输数据。 ``` GLuint pbo[2]; glGenBuffers(2, pbo); glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[0]); glBufferData(GL_PIXEL_PACK_BUFFER, size, NULL, GL_STREAM_READ); glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[1]); glBufferData(GL_PIXEL_PACK_BUFFER, size, NULL, GL_STREAM_READ); ``` - 2、第一帧 - 绑定PBO1为GL_PIXEL_PACK_BUFFER; - 调用 glReadPixels 将像素数据异步写入 PBO1(GPU→PBO1,无需CPU等待)。 - 同时:CPU 处理 PBO2 中上一帧的数据(如保存到文件)。 - 3、第二帧 - 切换绑定到 PBO2 接收新数据,同时处理 PBO1 的数据。 - 将下面的代码加上while循环,如此循环交替,实现并行化处理。 ``` // 绑定pbo[0]接收新数据 glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[0]); glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0); // 映射pbo[1]读取旧数据(上一帧) glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo[1]); void* data = glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY); if (data) { saveToFile(data); // CPU处理数据 glUnmapBuffer(GL_PIXEL_PACK_BUFFER); } ``` ## 关键优化 - 异步传输: glReadPixels 或 glTexSubImage2D 通过PBO传输时,立即返回,不阻塞CPU。 - 内存映射(Mapping)延迟: 通过 glMapBuffer 访问PBO数据时,才需要同步,但可通过双缓冲隐藏延迟。 - DMA 加速: PBO 使用显存的DMA通道,绕过CPU直接传输数据。 - 注意事项: - 同步点:glMapBuffer 会隐式同步,确保前一帧的传输已完成。 可通过 glMapBufferRange 配合 GL_MAP_UNSYNCHRONIZED_BIT 减少阻塞(需手动同步)。 - PBO大小:需匹配像素数据尺寸(如 width × height × 4 对于RGBA8格式)。 - 驱动程序兼容性:部分旧驱动可能对PBO的异步支持不完善。 ## 其它应用场景 - 纹理上传优化 - 双PBO交替上传纹理数据(如GPU将视频流数据处理后,更新纹理): ``` glBindBuffer(GL_PIXEL_UNPACK_BUFFER, pbo[pboIndex]); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, 0); ``` - GPU计算结果的回读: 将CUDA/OpenCL计算结果通过PBO快速传回OpenGL渲染。 ## 总结 双PBO通过乒乓缓冲和异步传输,最大化重叠CPU与GPU的工作,显著提升像素操作的吞吐量。适用于需要高频数据交换的场景(如实时截图、视频处理或GPU计算可视化)。 # OpenGL为什么通过地址查找方式加载函数: OpenGL之所以采用运行时动态获取函数指针(如 `wglGetProcAddress`)的方式,而非传统的头文件+静态/动态链接库模式,主要是基于一下考量: ## 1.跨驱动兼容性 - 硬件厂商自主实现: OpenGL 函数的具体实现由显卡驱动(如 NVIDIA/AMD/Intel 的 ICD 驱动)提供,不同厂商甚至不同驱动版本的函数实现地址可能完全不同。 例如:glGenBuffers 在 NVIDIA 驱动和 AMD驱动中的内存地址不同,无法通过统一的库文件导出。 - 避免绑定特定库: 若通过静态/动态链接库直接调用,需为每个厂商编译不同版本的库,导致维护灾难。 ## 2. 扩展机制的灵活性 - 动态扩展支持: - OpenGL 通过扩展(如 GL_ARB_vertex_buffer_object)不断引入新功能,这些扩展可能仅部分驱动支持。 - 运行时查询允许开发者检查扩展是否存在(如 glewIsSupported),再安全调用相关函数。 - 避免头文件爆炸: - 若所有扩展函数都通过头文件公开,头文件将变得极其庞大(OpenGL 有数百个扩展),且大部分函数对用户无用。 ## 3. 版本碎片化问题 - 多版本共存: - 同一台机器上可能同时存在支持 OpenGL 3.3 和 4.6 的驱动,但系统自带的 opengl32.dll(Windows)仅实现 1.1 版核心函数。高版本函数必须通过运行时获取,否则无法兼容旧系统。 - 渐进式加载: - 应用可先检查版本(glGetString(GL_VERSION)),再按需加载高版本函数,避免强行依赖新特性导致崩溃。 ## 4. 平台差异性处理 - 统一接口,不同实现: - Windows 用 wglGetProcAddress,Linux 用 glXGetProcAddress,macOS 用 NSOpenGLGetProcAddress,但函数行为一致。 - 通过抽象层(如 GLEW/GLAD)隐藏平台差异,开发者无需关心底层细节。 - 绕过系统库限制: - Windows 的 opengl32.dll 停滞在 OpenGL 1.1,动态加载是唯一支持现代 OpenGL 的途径。 ## 5. 性能与资源优化 - 按需加载: - 应用只需加载实际使用的函数,减少内存占用(如旧版兼容模式无需加载着色器相关函数)。 - 驱动优化空间: - 驱动厂商可根据硬件特性动态调整函数实现(如某些函数在 AMD显卡上可能走优化后的私有路径)。 ## 对比:传统库链接的局限性 |动态获取函数指针| 传统库链接| |:--:|:--:| |支持任意扩展和版本 |仅支持库编译时固定的函数集| |兼容所有厂商驱动 |需为每个驱动编译不同库| |运行时灵活检查功能可用性| 编译时即需确定所有依赖| |跨平台接口统一 |需为每个平台维护不同库| ## 总结 OpenGL 的动态函数加载机制是 对图形硬件生态碎片化的工程妥协,它牺牲了部分易用性,换来了无与伦比的跨驱动、跨版本、跨平台兼容性。尽管牺牲了易用性,需要手动管理函数指针,现代工具链(如 GLAD)已极大简化了这一过程,开发者只需关注业务逻辑即可。 # CUDA相关 一般的CUDA程序的基本结构: ``` 头文件包含 常量定义(或者宏定义) C++自定义函数和CUDA核函数的户明(原型) int main(void) { 分配主机与设备内存; 初始化主机中的数据; 将某些数据从主机复制到设备; 调用核函数在设备中进行计算; 将某些数据从设备复制到主机; 释放主机与设备内存; } C++自定义函数和CUDA核函数的定义(实现) ``` 使用CUDA计算两个数组之和(尤其是很大的数组): ``` #include <math.h> #include <stdio.h> const double EPSILON = 1.0e-15; const double a = 1.23; const double b = 2.34; const double c = 3.57; void __global__ add(const double *x, const double *y, double *z); void check(const double *z, const int N); int main(void) { const int N = 100000000; const int M = sizeof(double) * N; double *h_x = (double*) malloc(M); double *h_y = (double*) malloc(M); double *h_z = (double*) malloc(M); for (int n = 0; n < N; ++n) { h_x[n] = a; h_y[n] = b; } double *d_x, *d_y, *d_z; cudaMalloc((void **)&d_x, M); cudaMalloc((void **)&d_y, M); cudaMalloc((void **)&d_z, M); cudaMemcpy(d_x, h_x, M, cudaMemcpyHostToDevice); cudaMemcpy(d_y, h_y, M, cudaMemcpyHostToDevice); const int block_size = 128; const int grid_size = (N + block_size - 1) / block_size; add<<<grid_size, block_size>>>(d_x, d_y, d_z, N); cudaMemcpy(h_z, d_z, M, cudaMemcpyDeviceToHost); check(h_z, N); free(h_x); free(h_y); free(h_z); cudaFree(d_x); cudaFree(d_y); cudaFree(d_z); return 0; } void __global__ add(const double *x, const double *y, double *z, const int N) { const int n = blockDim.x * blockIdx.x + threadIdx.x; if (n < N) { z[n] = x[n] + y[n]; } } void check(const double *z, const int N) { bool has_error = false; for (int n = 0; n < N; ++n) { if (fabs(z[n] - c) > EPSILON) { has_error = true; } } printf("%s\n", has_error ? "Has errors" : "No errors"); ``` # 大文件分块下载 ## 核心原理 - 针对大文件(1GB以上)的下载,通常来说磁盘IO速度会比网络IO更快(不绝对),多线程分块下载通常能显著提升速度,但实际效果受网络环境、服务器策略和硬件条件等因素影响。其核心原理为: - 突破单线程的带宽限制(主要原因):单线程下载时,TCP连接的吞吐量受限于窗口大小、延迟(RTT)和拥塞控制算法。即使带宽充足,单线程可能无法完全利用可用带宽(例如:高延迟网络下单线程速度远低于理论值)。多线程通过并行TCP连接,可以叠加多个连接的吞吐量,更充分地利用带宽。 - 规避TCP的慢启动机制: 每个TCP连接初始阶段会逐步提升速度(慢启动)。多线程分块下载使得多个连接同时进行慢启动,整体更快达到稳定高速状态。 - 服务器限速规避: 部分服务器会对单个IP的连接限速。多线程分块下载(尤其是通过多IP或CDN节点)可能绕过此类限制。当然快手不存在这个问题。 ## 关键条件 - 服务器支持分块请求(HTTP Range头): 服务器需支持Accept-Ranges: bytes,允许客户端通过Range: bytes=0-999等请求指定文件块。否则多线程无法实现。 - 网络带宽未被单线程占满: 若单线程已跑满带宽(例如:千兆局域网下载高速服务器文件),多线程可能无提升,甚至因调度开销稍慢。 - 磁盘I/O性能足够: 多线程下载需并行写入文件块,机械硬盘可能因随机写入成为瓶颈(SSD影响较小)。合并分块时也可能有短暂延迟。 - 线程数合理配置: 线程过多会增加连接管理开销(如TCP握手、上下文切换),反而不利于速度。通常建议 4-16个线程,具体需测试调整。 ## 潜在瓶颈与注意事项 - 服务器并发限制: 部分服务器会限制单个IP的连接数或请求频率,过多线程可能触发封禁(如429错误)。 - 分块不均的尾块问题: 若文件大小不能被线程数整除,最后一个分块可能远小于其他块,导致线程闲置。 - 协议差异: FTP协议天然支持分块下载,而HTTP依赖服务器支持。某些私有协议可能禁止多线程。 ## 结论 - 推荐使用多线程分块下载,尤其适用于: - 大文件(1GB以上) - 高延迟网络(如4G/跨国下载) - 服务器未对单IP限速 - 不适用场景: - 服务器禁用分块请求 - 本地带宽已被单线程占满 - 磁盘I/O或CPU性能不足 - 工具建议: - 支持多线程的下载工具(如IDM、Aria2、wget2)可自动优化分块和线程数。 # QThread和moveToThread ## 1.简介 - QThread对象本身并不运行在新线程中,只有run()中的代码在新线程中执行,QThread的信号槽在其创建线程中执行,而非管理线程。QThread的双重身份: - 线程管理器:其作为QObject的派生类,其作用就是用来管理底层线程的; - 线程接口:同时也提供对底层线程的控制接口。 - 实现多线程的两种主要有两种方式(继承QThread和moveToThread)。 ## 2.继承自QThread的方式 - 工作原理 - 重写QThread的run()方法,在该方法内执行耗时操作。 - run()中的代码会在新线程中运行,而QThread对象本身属于创建它的线程(通常是主线程)。 - 代码实例 ``` class WorkerThread : public QThread { protected: void run() override { // 耗时操作(在新线程中执行) doHeavyTask(); } }; // 使用方式 WorkerThread *thread = new WorkerThread; thread->start(); // 启动新线程 ``` - 优点 - 直观简单,适合快速实现独立任务。 - 对线程生命周期有完全控制(如start()/wait())。 - 缺点 - 违反了Qt的“事件驱动”设计理念(QThread本身不是任务容器)。 - 容易误用:在子类中直接添加成员变量或方法时,可能意外在主线程中访问它们(导致竞态条件)。 ## 3.moveToThread的方式 - 工作原理 - 创建一个普通的QObject派生类(包含槽函数),然后通过moveToThread()将其移动到新线程。本质上这个QObject派生类就是一个工作任务类,包含具体的一些需要执行在工作线程中的槽函数。 - 通过信号槽触发任务,所有槽函数会在目标线程中执行。 - 注意,所有的内部成员都必须以该QObject为parent, 否则通过moveToThread移动到新的线程时,会有部分内部成员不会移到新的工作线程,它们可能还是属于原来的主线程。例如:SerialAssistant的构造函数中: ``` m_serialPort = new QSerialPort(this);// 如果漏掉了this, 那么在串口传输时就会有警告m_serialPort和SerialAssistant不在同一个线程 ``` - 代码实例: ``` class Worker : public QObject { Q_OBJECT public slots: void doWork() { qDebug() << "Worker thread:" << QThread::currentThread(); // 执行耗时操作... emit resultReady(); } signals: void resultReady(); }; // 在主线程中使用 QThread *thread = new QThread; Worker *worker = new Worker; worker->moveToThread(thread); // 线程启动,则执行doWorkd connect(thread, &QThread::started, worker, &Worker::doWork); // doWork完成,则退出,线程正常quit退出会触发finished connect(worker, &Worker::resultReady, thread, &QThread::quit); // 线程finished,则触发worker析构 connect(thread, &QThread::finished, worker, &Worker::deleteLater); // 线程finished,则触发线程thread析构 connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); ``` - 优点: - 符合Qt的事件驱动模型,线程通过事件循环管理任务。 - 更安全:对象的所有槽函数自动在目标线程中执行,无需手动同步。 - 灵活性高:一个线程可托管多个QObject对象。 - 缺点: - 需要显式管理线程和对象的生命周期(如避免跨线程删除对象)。 - 必须使用信号槽触发任务,不能直接调用方法。 ## 4.关键区别对比 |特性 |继承QThread |moveToThread| |:------------------- | :---------: | :-------: | |线程模型 |重写run(),类似传统线程| 事件驱动,与Qt信号槽深度集成| |代码执行位置| run()在新线程,其他方法在主线程 |所有槽函数在目标线程中执行| |资源管理| 需手动控制线程生命周期| 需注意对象跨线程删除(用deleteLater)| |适用场景| 独立、一次性任务| 长期运行、需事件交互的任务 ## 5.建议使用哪种? - 推荐moveToThread(Qt官方推荐方式): - 更符合Qt的设计哲学,避免直接操作线程。 - 天然支持信号槽通信,减少竞态条件风险。 - 适合需要与主线程交互的场景(如进度更新)。 - 仅在以下情况考虑继承QThread: - 需要完全控制线程的执行流程(如实现自定义线程调度)。 - 任务完全独立,无需与主线程通信。 ## 6. 注意事项 - 线程安全: - 使用moveToThread时,确保对象的构造函数在主线程调用,否则需手动设置QObject::moveToThread(nullptr)。 - 跨线程访问资源时,需用互斥锁(QMutex)或信号槽。 - 对象生命周期: - 使用deleteLater()安全删除跨线程对象。 - 避免在线程运行期间直接析构QThread对象。 - 事件循环: - moveToThread要求目标线程运行事件循环(exec()),否则槽函数无法执行。 ## 7.moveToThread(this)争议用法 - 问题实例: ``` class ControversialThread : public QThread { Q_OBJECT public: ControversialThread() { moveToThread(this); // 危险操作! } protected: void run() override { qDebug() << "Running in thread:" << QThread::currentThread(); } }; // 使用 ControversialThread *thread = new ControversialThread; connect(thread, &ControversialThread::someSignal, thread, &ControversialThread::someSlot); thread->start(); ``` - 这种用法的问题: - 线程生命周期问题:线程退出后,事件处理不确定; - 资源管理复杂:可能导致内存泄漏或者崩溃; - 违反设计原则:混淆了线程管理器和线程本身的职责; - 事件循环冲突:QThread本身的时间循环与工作线程的事件循环可能冲突; ## 8.高级线程用法QtConcurrent: 可以基于QtConcurrent和QEventLoop实现线程池: ``` include <QtConcurrent> void threadedProcessing(const QList<QString> &data) { // 使用线程池并行处理 QFuture<void> future = QtConcurrent::map(data, [](const QString &item) { qDebug() << "Processing" << item << "in thread" << QThread::currentThread(); // 处理每个数据项... }); // 等待完成 QFutureWatcher<void> watcher; watcher.setFuture(future); QEventLoop loop; QObject::connect(&watcher, &QFutureWatcher<void>::finished, &loop, &QEventLoop::quit); loop.exec(); } ``` ## 9.总结 - 优先选择moveToThread:适用于大多数场景,尤其是需要与GUI线程交互的任务。 - 不要使用moveToThread(this),这种模式会带来更多问题而非解决方案; - 谨慎使用继承QThread:仅用于简单、独立的任务,注意避免误用。 - 更复杂的需求,可以使用QtConcurrent等高级API而非直接使用QThread。 ## 参考链接 - https://mp.weixin.qq.com/s/2Yhl2Cz_LDdxioq_ioZ4oQ - deepseek # 自定义控件 - 自定义控件的方法: - 从外观上:QSS、继承paintEvent绘制函数进行重绘、继承QStyle相关类重绘、组合拼装等; - 从功能上:重写相关的事件函数、添加或者修改信号、槽函数等等; # QPixmap和QImage的区别 - QPixmap主要用于绘图,针对屏幕显示而最佳化设计,QImage主要是为图像I/O,图片访问和像素修改而设计的。 # Qt的事件系统: ## 1、基本概念 - 事件(Event):表示发生在应用程序中的事情,如鼠标点击、键盘输入、窗口重绘等 - 事件源:产生事件的源头(如鼠标、键盘、定时器等) - 事件目标:接收并处理事件的对象(通常是 QWidget 或其子类) - Qt 的事件处理遵循以下基本流程: - 事件产生:由系统或 Qt 内部生成 QEvent 对象 - 事件投递:通过事件队列(event queue)投递到目标对象 - 事件分发:QCoreApplication 从队列中取出事件并分发给相应对象 - 事件处理:对象通过 event() 方法接收并处理事件 - 事件循环:Qt 应用程序的核心是事件循环, 事件循环不断检查事件队列,分发事件给相应的对象处理。事件循环由 QCoreApplication::exec() 启动: ``` int main(int argc, char *argv[]) { QApplication app(argc, argv); // 创建窗口和其他对象 return app.exec(); // 进入主事件循环 } ``` ## 2、事件的类型: - Qt 定义了多种事件类型,主要包括: - 输入事件:鼠标、键盘、触摸等 - 窗口事件:显示、隐藏、移动、调整大小等 - 绘图事件:重绘请求 - 定时器事件:定时器超时 - 自定义事件:用户定义的事件类型 ## 3、事件的传播机制: ###向上传播 如果子控件不处理事件,事件会传递给父控件; ###事件过滤器 可以在事件到达目标对象前拦截和处理事件; ###事件接受/忽略 通过 accept() 和 ignore() 方法可以控制事件传播 ## 4、事件的过滤级别: 根据对Qt事件机制的分析, 我们可以得到5种级别的事件过滤,Qt中每一种过滤级别都相当于一种处理事件的方式,以功能从弱到强, 排列如下: ### 4.1、重载特定事件处理函数 最常见的事件处理办法就是重载像mousePressEvent(), keyPressEvent(), paintEvent() 这样的特定事件处理函数,例如: ``` class MyWidget : public QWidget { protected: void mousePressEvent(QMouseEvent *event) override { // 处理鼠标按下事件 qDebug() << "Mouse pressed at:" << event->pos(); } }; ``` ### 4.2、重载event()函数 - 通过重载event()函数,我们可以在事件被特定的事件处理函数处理之前(象keyPressEvent())处理它. - 比如, 当我们想改变tab键的默认动作时,一般要重载这个函数. 在处理一些不常见的事件(比如:LayoutDirectionChange)时,evnet()也很有用,因为这些函数没有相应的特定事件处理函数. 当我们重载event()函数时, 需要调用父类的event()函数来处理我们不需要处理或是不清楚如何处理的事件. ``` bool MyWidget::event(QEvent *event) { if (event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); // 处理按键事件 return true; // 表示事件已处理 } return QWidget::event(event); // 其他事件交给父类处理 } ``` ### 4.3、在Qt对象上安装事件过滤器 - 安装事件过滤器有两个步骤: - (假设要用A来监视过滤B的事件)首先调用B的installEventFilter( const QOject *obj ), 以A的指针作为参数. 这样所有发往B的事件都将先由A的eventFilter()处理. - 然后, A要重载QObject::eventFilter()函数, 在eventFilter() 中书写对事件进行处理的代码. ``` // 在某个类中 obj->installEventFilter(this); bool MyClass::eventFilter(QObject *watched, QEvent *event) { if (event->type() == QEvent::MouseButtonPress) { // 处理事件 return true; // 不再进一步处理 } return false; // 继续事件处理 } ``` ### 4.4、给QAppliction对象安装事件过滤器 - 一旦我们给qApp(每个程序中唯一的QApplication对象)装上过滤器,那么所有的事件在发往任何其他的过滤器时,都要先经过当前这个 eventFilter(). 在debug的时候, 这个办法就非常有用. - 也常常被用来处理失效了的widget的鼠标事件,通常这些事件会被QApplication::notify()丢掉. (在QApplication::notify() 中, 是先调用qApp的过滤器, 再对事件进行分析, 以决定是否合并或丢弃) ### 4.5、继承QApplication类,并重载notify()函数 - Qt 是用QApplication::notify()函数来分发事件的. 想要在任何事件过滤器查看任何事件之前先得到这些事件,重载这个函数是唯一的办法. - 通常来说事件过滤器更好用一些, 因为不需要去继承QApplication类. 而且可以给QApplication对象安装任意个数的事件。 # Qt中的信号 - 所有的信号声明都是公有的,所以Qt规定不能在signals前面加上public,private,protected。 - 所有的信号返回值都是void。所有的信号都不需要定义。要使用信号,必须直接或者间接继承自QObject类,且开头必须私有声明包含Q_OBJECT宏定义。 - 在同一个线程中,当一个信号被emit时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码。 - 如果一个信号链接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们的链接时的顺序执行的。不同的线程中(即多线程),槽函数的执行顺序则是不确定的。 在链接信号和槽函数时,也可以通过connectType设置链接方式为:在发出信号后,不需要等待操函数执行完,而是直接执行后面的代码。 # 事件和信号的区别: - 相同点: - 都是 Qt 中对象间通信的机制。 - 都可以携带自定义的信息。 - 不同: - 发起方: - 事件:由系统(如鼠标、键盘、定时器)或 Qt 应用程序本身产生。 - 信号:由对象主动发射。 - 处理方式: - 事件:通过事件循环派发,由 event() 函数或特定事件处理函数(如 mousePressEvent())处理。可以被过滤或阻塞。 - 信号:连接到另一个对象的槽函数。一旦发射,所有连接的槽都会异步执行。无法被中途拦截。 - 使用场景: - 事件:处理与对象本身相关的、低级的交互(如按键、绘制、关闭)。 - 信号:通知其他对象某个动作已经发生(如按钮被点击、数据加载完成)。 - 使用场合与时机不同: 一般情况下,在“使用”窗口部件时,我们经常需要使用信号,并且会遵循信号与槽的机制; 而在“实现”窗口部件时,我们就不得不考虑如何处理事件了。 举个例子,当使用QPushButton时,我们对于它的clicked()信号往往更为关注,而很少关心促成发射该信号的底层的鼠标或者键盘事件。但是,如果要实现一个类似于QPushButton的类,我们就需要编写一定的处理鼠标和键盘事件的代码,而且在必要的时候,仍然需要发射和接收clicked()信号。(**事件关注动作发生前的事情,信号关注动作发生后的事情**)。 # 信号槽连接的类型 ## 自动连接(Qt::AutoConnection, 默认方式) - 原理: - 运行时自动决定使用直接连接还是队列连接 - 如果信号发射者和接收者在同一线程,使用直接连接(相当于 Qt::DirectConnection) - 如果不在同一线程,使用队列连接(相当于 Qt::QueuedConnection) ``` QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot); // 默认AutoConnection ``` ## 直接连接(Qt::DirectConnection) - 原理: - 信号发射时立即调用槽函数 - 槽函数在信号发射者的线程中执行 - 类似于直接函数调用,没有事件队列的介入 - 如果跨线程使用可能导致问题 - 适用场景: - 信号和槽在同一线程 - 需要立即执行的场景 ``` QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::DirectConnection); ``` ##队列连接 (Qt::QueuedConnection) - 其为异步执行,原理: - 使用Qt::QueuedConnection和槽函数连接的信号发射时,该信号及其参数会被封装成QMetaCallEvent(QMetaCallEvent包括接收者对象指针receiver, 槽函数方法索引method, 通过QVaariant存储的参数的拷贝),随后该事件通过QCoreApplication::postEvent被放入接收者线程的事件队列中; - 信号发射后,发射线程继续执行,不会等待槽函数完成(所以是异步执行嘛); - 接收者线程的事件循环稍后会处理这个事件,并调用槽函数; - 最终,信号参数会被拷贝并存储(因此参数类型必须可拷贝),槽函数在接收者的线程中执行; - 注意事项: - 事件循环必须运行:若接收者线程未启动事件循环,槽函数就永远不会执行; - 参数类型限制:信号参数必须是Qt元类型系统支持的类型(如基本类型、QString等),否则需要使用qRegisterMetaType()注册; - 尽量避免参数指针传递:若参数为指针,需要确保指针指向的数据生命周期长于事件处理时间; - 性能开销:事件封装和队列操作会引入额外开销,高频信号需要谨慎使用; - 适用场景: - 跨线程通信, 比如跨线程更新UI - 需要线程安全的场景,比如线程间数据传递,日志记录、任务分发等需要异步处理的场景; ``` QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::QueuedConnection); ``` ## 阻塞队列连接(Qt::BlockingQueuedConnection) - 原理: - 类似队列连接,但阻塞发送者线程直到槽函数执行完成; - 必须确保接收者线程正在运行事件循环; - 如果发送者和接收者在同一线程使用会导致死锁; - 适用场景: - 需要同步跨线程调用的场景; - 需要等待槽函数执行结果的场景; ``` QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::BlockingQueuedConnection); ``` ## 唯一连接(Qt::UniqueConnection) - 原理: - 不是独立的连接类型,而是与其他类型组合使用(如 Qt::AutoConnection | Qt::UniqueConnection) - 确保相同的信号槽对只会连接一次 - 如果相同的连接已存在,新的连接会失败 - 适用场景: - 避免重复连接相同的信号槽 ``` QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::AutoConnection | Qt::UniqueConnection); ``` ## 单次连接(5.15以上版本, Qt::SingleShotConnection) - 原理: - 槽函数只执行一次,之后连接自动断开; - 可以通过 QObject::connect() 的第五个参数指定上下文对象来实现 ``` // Qt 5 方式 QMetaObject::Connection conn = QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot); QObject::disconnect(conn); // 手动断开 // Qt 5.15+ 提供了更简单的方式 QObject::connect(sender, &Sender::signal, receiver, &Receiver::slot, Qt::SingleShotConnection); ``` # 信号槽原理 - 基础: - 元对象系统:所有连接信息通过QMetaObject系统存储和管理 - 信号发射机制:信号被emit时,Qt的moc生成的代码会调用QMetaObject::activate() - 连接查找:运行时查找与该信号连接的所有槽函数 - 参数传递: - 直接连接:直接调用,参数通过栈传递 - 队列连接:参数被拷贝并作为事件的一部分存储 - 线程安全:队列连接使用互斥锁保护共享数据; ## 详细原理: - Qt的信号槽机制是一种用于对象间通信的观察者模式实现,它通过信号(Signal)和槽(Slot)的绑定,实现了松耦合的事件通知机制。信号在事件发生时被发射(emit),而连接的槽函数会自动执行响应。 - 相比传统回调函数,信号槽有三大核心优势: - 类型安全:编译时检查信号和槽的参数兼容性; - 松耦合:通信双方不需要知道彼此的具体实现; - 线程安全:自动处理跨线程通信,无需手动同步; - 具体点讲,信号槽的实现依赖于Qt的元对象系统: - moc预编译器会处理包含Q_OBJECT宏的类,生成元信息代码; - 每个QObject派生类都有对应的QMetaObject,存储类名、信号、槽等元信息; - 信号和槽通过索引机制进行标识和管理; - 当emit一个信号时: - 实际调用的是moc生成的激活函数(QMetaObject::activate) - 系统查找该信号的所有连接记录; - 根据连接类型决定调用方式: - 直接连接:立即在当前线程同步调用槽函数; - 队列连接:将调用请求和参数打包为事件,投递到接收者线程的事件队列; - 阻塞队列连接:类似队列连接,但会阻塞发送线程直到槽执行完成; - 参数传递时: - 连接时会检查参数类型兼容性(槽的参数可以比信号少); - 直接连接直接传递参数指针; - 队列连接需要参数可拷贝,通过QMetaType系统处理类型转换" - 在实际的工作中,信号槽的典型应用场景包括: - 界面响应:按钮点击信号连接业务逻辑槽函数; - 跨线程通信:工作线程通过信号向主线程发送进度更新; - 模块解耦:不同模块通过信号槽交互而不直接依赖 ## 可能的追问: - 连接存储的数据结构(通常使用向量+哈希的组合) - Qt5的新语法(函数指针)连接与老式连接(宏)的差异(编译时检查vs运行时检查) - 信号槽与C++11的lambda结合使用的注意事项: - 连接生命周期管理:lambda捕获的对象必须保证在信号触发时仍然有效,特别注意捕获this指针时的对象生命周期问题 - 内存泄漏风险:- 使用QObject::connect的lambda连接不会自动断开,可能导致lambda及其捕获的对象无法释放 - 线程安全:确保lambda中访问的对象在接收信号的线程中是安全的,注意跨线程访问共享数据时的同步问题) ``` // 危险示例 - 可能访问已销毁的对象 connect(sender, &Sender::signal, [this]() { this->doSomething(); // 如果this已被销毁,将导致崩溃 }); // 安全做法 - 使用QPointer或弱引用 QPointer<MyClass> weakThis(this); connect(sender, &Sender::signal, [weakThis]() { if (weakThis) { weakThis->doSomething(); } }); ``` ## 简洁总结: Qt信号槽是一种基于元对象系统的松耦合通信机制。当对象状态变化时发射信号,所有连接的槽函数会自动触发。其核心实现是通过moc预编译器生成元信息代码,在QMetaObject中维护信号槽的索引和连接关系。信号发射时,系统会根据连接类型决定同步调用还是异步事件投递,自动处理线程安全问题。相比回调函数,它提供了更好的类型安全和模块解耦能力,是Qt框架最核心的特性之一。 # sendEvent和postEvent ## QCoreApplication::sendEvent() - sendEvent() 是同步事件发送方法: ``` bool QCoreApplication::sendEvent(QObject *receiver, QEvent *event); ``` - 特点: - 同步执行:事件会立即被处理,函数调用会阻塞直到事件处理完成; - 不接管事件对象所有权:调用者需要负责事件对象的生命周期; - 返回bool值:表示事件是否被接收者处理; - 线程限制:必须在接收者所在的线程中调用; ``` QMouseEvent event(QEvent::MouseButtonPress, pos, button, buttons, modifiers); QCoreApplication::sendEvent(receiver, &event); ``` ## QCoreApplication::postEvent() - postEvent() 是异步事件发送方法: ``` void QCoreApplication::postEvent(QObject *receiver, QEvent *event, int priority = Qt::NormalEventPriority); ``` - 特点: - 异步执行:事件被放入事件队列,稍后处理,函数立即返回 - 接管事件对象所有权:Qt 会负责删除事件对象 - 无返回值:因为事件是异步处理的 - 线程安全:可以从任何线程调用,事件会在接收者所在线程处理 - 支持优先级:可以通过priority参数指定事件优先级 ``` QMouseEvent *event = new QMouseEvent(QEvent::MouseButtonPress, pos, button, buttons, modifiers); QCoreApplication::postEvent(receiver, event); ``` ## 主要区别: |特性 |sendEvent()|postEvent()| |:--|:--:|:--:| |执行方式 |同步 |异步| |所有权 |调用者保留 |Qt 接管| |返回值 |有(bool) |无(void)| |线程安全| 必须在接收者线程调用| 可从任何线程调用| |事件处理| 立即处理| 进入事件队列稍后处理| |内存管理| 调用者负责删除| Qt自动删除| ## 使用场景 - 使用 sendEvent() 当: - 需要立即处理事件 - 需要知道事件是否被处理 - 事件对象是栈上分配的临时对象 - 使用 postEvent() 当: - 需要跨线程发送事件 - 不要求立即处理 - 事件对象是堆上分配的(new创建的) # QString和std::string - Unicode支持与国际化: - QString:默认采用UTF-16编码,全面支持Unicode字符(包括多语言文本、表情符号等),能正确处理字符长度(如length()返回字符数而非字节数),适合国际化应用; - std::string:本质是字节容器,默认不直接支持Unicode。若需处理多字节字符(如UTF-8),需额外转换或依赖第三方库(如ICU),且length()返回字节数,可能导致字符计数错误; - 内存管理与性能优化: - 隐式共享(Copy-on-Write):QString通过引用计数实现隐式共享,多个对象共享同一数据,仅在修改时触发深拷贝,大幅减少内存占用和复制开销,尤其适合大字符串或频繁传递的场景; - std::string:传统复制机制,每次赋值或传递均需完整复制数据。部分编译器支持SSO(短字符串优化),但对大字符串性能提升有限; - 丰富的API与功能: - QString:提供高阶接口如split()、arg()(格式化替换)、toUpper()、trimmed()(去空格)等,简化复杂操作。例如:QString str = "Hello %1"; str = str.arg("World"); // 格式化输出Hello world。 - std::string:基础功能需手动实现或依赖STL算法(如std::find),复杂操作(如格式化)需结合std::stringstream或C++20的std::format; - 与Qt框架深度集成 - Qt生态兼容性:QString无缝集成Qt信号槽、GUI组件(如QLabel)、文件IO等,避免类型转换开销。例如,Qt控件直接接受QString参数; - std::string:在Qt项目中需频繁与QString转换,增加代码复杂性和潜在编码问题; - 跨平台一致性与线程安全 - QString:Qt框架保证不同平台行为一致,尤其在Unicode处理和内存管理上;隐式共享机制在多线程中更高效(需注意线程安全约束); - std::string:依赖编译器实现,不同平台性能可能波动(如MSVC的SSO优化),且多线程传递时需显式拷贝; - 总结 - 优先使用QString的场景:Qt项目、需处理多语言文本、频繁传递或修改大字符串、需要丰富API。 - 优先使用std::string的场景:非Qt项目、对轻量级依赖有要求、或编译器优化(如SSO)显著提升性能 # Qt中为什么new QWidget后,不需要delete Qt提供了一种机制,能够自动、有效地组织和管理继承自QObject的Qt对象,这就是对象树机制。 Qt中很多类都是以QObject作为它们的基类。QObject的对象总是以树状结构组织自己。当我们创建一个QObject对象时,可以指定其父对象(或称父控件),新创建的对象将被加入到父对象的子对象(或称子控件)列表中。当父对象被析构时,这个列表中的所有子对象都会被析构。不但如此,当某个QObject对象被析构时,它会将自己从父对象的列表中删除,以避免父对象析构时再次析构自己。 当一个QObject正在接受事件队列的排队时,如果中途被手动delete掉了,就会出现问题。如果一定要delete,要使用QObject的deleteLater()函数,它会让所有事件都发送完,一切处理好以后马上清楚这片内存,而且就算调用多次的deleteLater也不会有问题。 # 描述Qt中的文件流(QTextStream)和数据流(QDataStream)的相同和区别: - 相同: 文件流和数据流都可以操作磁盘文件,也可以操作内存数据。都可以通过流对象将对象打包到内存,进行数据到传输。 - 区别: - 文本流(QTextStream):操作轻量级数据(int,double,QString),数据写入文本件后以文本的方式呈现。 - 数据流(QDataStream):通过数据流可以操作各种数据类型,包括对象,存储到文件中数据为二进制。
上一篇:
OpenGL和CUDA互操作时遇到的显卡驱动问题
下一篇:
Qt 学习问题
0
赞
2 人读过
新浪微博
微信
腾讯微博
QQ空间
人人网
提交评论
立即登录
, 发表评论.
没有帐号?
立即注册
0
条评论
More...
文档导航
没有帐号? 立即注册