为什么协程能让程序不再卡顿?——从同步、异步到 C++ 实战

1 引言

在图形界面(GUI)应用中,"卡顿"几乎是所有开发者都会遇到的老问题。一次复杂的计算、一次网络请求、一次磁盘读取,甚至一次大循环,都可能让界面在几百毫秒内完全失去响应,用户看到的就是------窗口半透明、按钮点不动、程序像"假死"了一样。

过去,在 C++ 程序中解决卡顿最常见的方法是:加一个线程。再加一个线程。然后用锁把它们绑在一起 。但随着项目复杂度提升,多线程的调度开销、锁竞争、死锁风险,也让不少开发者叫苦不迭。而在另一些语言里------比如 JavaScript、C#、Python------同样的问题却可以用更轻量、更优雅的方式解决:异步 I/O + 协程(Coroutine)

协程并不是线程的替代品,而是一种更贴近业务逻辑的结构化异步方式。得益于协程,你可以写出"看起来同步、实际上异步"的代码;你可以在单线程中实现并发;你可以让 GUI 始终流畅响应,而复杂任务在后台悄然推进。

本文将从最基础的概念------同步与异步、线程与协程------逐步展开,解释为什么协程能让程序不再卡顿,并通过完整的 C++ 协程/Boost.Coroutine 实战展示其内部原理与适用场景。

2 基础

在进行实战之前,先学习一些比较基础的知识。

2.1 同步 VS 异步

所谓同步(Synchronous) ,是指调用一个函数时,必须等它执行完才能继续执行下一行。例如:

cpp 复制代码
auto img = LoadImage(); // 在这里等待
Render(img);

因此同步特点是当前线程会被阻塞,调用者需要等待才能继续执行。

所谓异步(Asynchronous) ,是指发起任务后不等待,任务完成后再通过回调、future、信号等方式告诉调用者。例如:

cpp 复制代码
LoadImageAsync([](Image img){
    Render(img);  // 回调在之后执行
});

因此异步的特点是当前线程不会被阻塞,调用者不需要等待就能继续执行。

2.2 协程的本质

协程(Coroutine)是一种可以主动让出执行权的"轻量级函数"。它允许函数在中途暂停(yield),稍后继续执行。通常具备如下几点要素:

  1. 可挂起(Suspend)
  2. 可恢复(Resume)
  3. 保持自己的栈和上下文(但非常轻量)
  4. 不需要线程切换(协程在当前线程运行)

线程是真实由 CPU 执行的实体,是硬件资源调度的最小单位。在一般情况下,一款 CPU 产品上可以存在多个 核心(core) ,一个 CPU 核心一次只能运行一个线程的指令流(instruction stream),多个核心可以 同时 执行多个线程上的任务。

但是对于协程来说,是完全没有物理上的基础映射的------协程是纯软件层的概念。硬件上的 CPU、寄存器、调度器甚至硬件指令都不是为协程设计的,操作系统(OS)层面也不知道协程的存在。协程本质上就是用户态下的可让出/恢复执行点的函数。调度协程的不是 CPU,不是 OS,而是程序员 / 语言运行时 / 框架。

2.3 协程 VS 线程

既然已经有了多线程,那么为什么还需要协程?因为在某些情况下,线程太"重"了,如下表所示:

特点 线程 协程
调度 由操作系统(OS)负责 由程序员/框架负责
切换成本 高(微秒级) 极低(纳秒级)
栈大小 MB 级 KB 级
最大数量 很有限(1 千级) 很多(百万级)
上下文切换
适用场景 CPU 密集 I/O 密集、高并发

线程适合 CPU 密集的任务如图像处理、AI、压缩、矩阵运算等;协程适合 I/O 密集、高并发的任务,比如爬虫、网络请求、数据库访问、web 服务等。

以 I/O 密集的高并发任务来说,使用协程非常合适:

  • 栈小(4K~64K)
  • 切换快(~100 ns)
  • 单线程也能跑几十万协程
  • I/O 等待不阻塞线程

所以很多服务器框架(Go、Rust tokio、Python asyncio、C++20 coroutine)都推荐协程,而不是大量线程。当然也不是绝对,使用线程池+任务队列的方式也可以达到同样的效果,不过实现起来复杂度较高,也不如使用协程的方案稳健,性能提升也有限。

2.4 协程和异步

很显然,多线程是实现异步的一种方式。不过,多线程的问题就是太异步了,两个线程被创建之后就如同两条平行线,相互之间不再有任何关联。但在实际的程序开发中,相互之间不进行联系的情况比较少,一般需要在关键的节点进行线程同步。

单线程同样可以实现异步。具体来说,通过 单线程 + 异步 I/O + 协程 方案,也可以实现满足超高并发需求的异步,这种方案在JavaScript、Python等环境中非常常见。正如前文中提到的,协程是一种"可以暂停/恢复"的轻量函数,因此,协程可以写出像同步代码一样的结构,通过"暂停---等结果回来---继续"的机制来实现异步。这种机制通常通过类似 yield() 的语法糖来控制。当然,如果协程体中从不 yield() ,或者没有异步 I/O 环境的支持,这个异步函数实现就会退化成同步函数。

协程本身不是为 GUI 开发设计的,但在 GUI(Qt、Unity、JavaScript)中常用协程解决卡顿,因为:

  • GUI 主线程不能长时间执行耗时操作
  • 协程能把耗时任务拆成小碎片
  • 每片执行后 yield,交回主线程让 UI 刷新

3 实现

异步实现在 JavaScript 中几乎随处可见,可以说 JavaScript 就是一门建立在异步实现上的编程语言。在 JavaScript 中,常见的异步实现有:

技术 是否异步 本质
回调 callback 异步通知函数
Promise 异步状态机
async/await 异步协程(基于 Promise)
DOM 事件、定时器 事件循环驱动的异步任务

虽然以上实现的异步机制实现本质不同,但是最终都依赖于 event loop(事件循环)调度,而不是线程。

接下来具体探究一些协程或者异步的实现,不局限于 JavaScript :

3.1 JS 的 async/await

JS 的 async/await 是协程的一种"语法级实现",严格来说是协程的语法糖。因为 async/await 做的事情是让函数可以 挂起(暂停)

js 复制代码
let x = await fetch(url); // 在这里挂起

然后 恢复执行

js 复制代码
console.log(x); // 恢复后继续

这个行为就是"协程的本质":可挂起、可恢复、用户态调度,当然最终是通过 JS runtime 事件循环调度来实现。虽然 JS 语法没有暴露"yield control to scheduler"这样的命令,但其行为确实和协程一致。因此,JS 的 async/await 是异步协程(asynchronous coroutine)的一种形式。

3.2 JS 的 Promise

JS 的 Promise 不能暂停函数,不能恢复执行点,也没有栈帧保存能力,因此并不是协程。Promise 是一种 异步状态机 ,能够表达 3 个状态:pending → fulfilled / rejected 。是用于管理异步结果的 数据结构(异步容器) 。当然,async/await 本身是用 Promise 作为底层机制 实现的。

3.3 Qt 的信号/槽

Qt 的信号槽 "有时异步,有时同步",取决于连接类型:

Qt::ConnectionType 同步/异步?
DirectConnection 同步(立即调用)
QueuedConnection 异步(事件队列中排队执行)
AutoConnection 取决于接收者是否在另一个线程

如果是跨线程或 QueuedConnection,就是异步模型,使用事件队列 + 调度器来执行槽函数。但是 Qt 的异步不是协程,它是事件驱动,不会"暂停函数并恢复"。

3.4 C 的回调函数

回调本身不是异步。回调只是一个函数指针,什么时候执行取决于调用者;是否异步由调用者决定:

  • 如果驱动/库是异步的,那么回调变成异步回调。
  • 如果是同步调用,那么回调就是普通函数调用。

例如同步回调:

c 复制代码
int process(int x, int(*cb)(int)) {
    return cb(x); // 立即调用
}

异步回调:

c 复制代码
void read_async(int fd, void(*on_complete)(int result));

如果回调函数什么时候调用,使用者无法控制,这种编程模型才叫做异步。

3.5 C# 的 async/await

和 JavaScript 类似,C# 的 async/await 也是一种 基于状态机的协程实现,具有以下特点:

  • 函数在 await挂起(suspend)
  • 当被 await 的任务(Task)完成时,自动恢复执行
  • 编译器将 async 方法重写为一个 状态机类(state machine),保存局部变量、执行位置等上下文
  • 默认在 原始上下文(如 UI 线程)恢复执行 (通过 SynchronizationContext
csharp 复制代码
async Task<string> FetchDataAsync()
{
    var client = new HttpClient();
    string data = await client.GetStringAsync("https://api.example.com"); // 挂起
    Console.WriteLine(data); // 恢复
    return data;
}

这完全符合"协程"定义:可挂起、可恢复、用户态调度(由 .NET Task Scheduler 驱动)

因此,C# 的 async/await真正的轻量级协程(asynchronous coroutine),比 JS 更接近系统级协程(如 Go 的 goroutine),尽管仍基于回调和状态机而非独立栈。

3.6 Unity 的 IEnumerator

Unity也可以使用 C# 的 async/await ,但是 Unity 底层可能没有真正的异步 I/O 支持(尤其在 WebGL 或移动平台),从而造成主线程阻塞。

Unity 还引入了另外一种伪协程(pseudo-coroutine)机制 ------IEnumerator,用于在单线程游戏主循环 中实现"看似并发"的逻辑控制。它不是真正意义上的协程(没有独立栈、不能跨线程、不基于异步 I/O),但通过 迭代器(iterator) + 主循环调度 模拟了挂起与恢复的行为。

例如:

csharp 复制代码
IEnumerator CountDown()
{
    for (int i = 3; i > 0; i--)
    {
        Debug.Log(i);
        yield return new WaitForSeconds(1); // 挂起 1 秒
    }
    Debug.Log("Go!");
}

// 启动协程
StartCoroutine(CountDown());

其中:

  • yield return 是挂起点:函数在此处暂停,控制权交还给 Unity 引擎。
  • Unity 主循环每帧检查协程状态,当条件满足(如时间到、帧结束等),从挂起点继续执行

Unity 的 IEnumerator 更准确地说是一种 基于帧的协作式任务调度器,而非语言级协程。

4. 实例

4.1 无栈协程

C++20 已经提供了一种原生的协程方案 co_await/co_yield,新项目如果能使用 C++20 推荐使用。不过 C++20 相对于 C++17 的变动还是不小,笔者使用的还是 C++17,那么就可以使用 boost 的协程方案 boost::coroutines2 。

无论是 boost::coroutines2 还是 co_await/co_yield,都是无栈协程 的一种实现。所谓无栈协程,指的是协程没有独立的调用栈,其局部变量和执行状态由编译器或库通过状态机保存在堆上。与之相对的是有栈协程,拥有自己的栈空间,可任意挂起点(包括深层函数调用中)。应该来说,主流语言倾向无栈协程,JS、C#、Python、Rust、C++20 都选择了无栈模型,因其与事件循环、Future/Promise 模型天然契合,且内存效率极高。

一个使用 boost::coroutines2 的协程实现代码如下所示:

cpp 复制代码
#include <boost/coroutine2/all.hpp>

using namespace std;

// 模拟"耗时任务"
void long_running_task(boost::coroutines2::coroutine<void>::push_type& yield) {
  for (int i = 0; i < 10; ++i) {
    std::cout << "Task: Processing iteration " << i << std::endl;

    // 模拟耗时操作(例如计算或 I/O)
    std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 让出执行权,允许主线程更新 UI
    yield();
  }
  std::cout << "Task: Completed!" << std::endl;
}

// 模拟"UI 更新"
void update_ui() {
  static int ui_update_count = 0;
  std::cout << "UI: Updating UI, count = " << ++ui_update_count << std::endl;
}

int main() {
  using namespace boost::coroutines2;

  // 创建协程,绑定耗时任务
  coroutine<void>::pull_type task_source(
      [&](coroutine<void>::push_type& yield) { long_running_task(yield); });

  // 主循环:交替执行协程和 UI 更新
  while (task_source) {
    // 执行协程的一部分
    task_source();

    // 更新 UI
    update_ui();

    // 模拟 UI 处理时间
    std::this_thread::sleep_for(std::chrono::milliseconds(300));
  }

  return 0;
}

这段代码的关键在于理解协程核心组件 boost::coroutines2 :

cpp 复制代码
using namespace boost::coroutines2;

coroutine<void>::pull_type task_source(
    [&](coroutine<void>::push_type& yield) {
        long_running_task(yield);
    });

其中:

  • coroutine :定义一个协程,不传递值(若需传值,可用 coroutine 等)。
  • pull_type :协程的"拉取端",代表主控上下文,用于启动和恢复协程。
  • push_type:协程的"推送端",在协程体内用于挂起并交出控制权。
  • yield:是一个函数对象,调用 yield() 即挂起当前协程,返回主控上下文。

形象的说,pull_type 是"消费者"(主控方),push_type 是"生产者"(协程体),而协程通过 yield 返回控制权给 pull 端,这其实是一种非对称协程的设计。

运行结果如下:

bash 复制代码
Task: Processing iteration 0
Task: Processing iteration 1
UI: Updating UI, count = 1
Task: Processing iteration 2
UI: Updating UI, count = 2
Task: Processing iteration 3
UI: Updating UI, count = 3
Task: Processing iteration 4
UI: Updating UI, count = 4
Task: Processing iteration 5
UI: Updating UI, count = 5
Task: Processing iteration 6
UI: Updating UI, count = 6
Task: Processing iteration 7
UI: Updating UI, count = 7
Task: Processing iteration 8
UI: Updating UI, count = 8
Task: Processing iteration 9
UI: Updating UI, count = 9
Task: Completed!
UI: Updating UI, count = 10

4.2 异步框架

在 C/C++ 程序开发中,由于应用方向偏底层,使用协程的情况比较少。即使是需要使用到协程的场景,使用线程池+任务队列的方案就可以平替掉。这其中还存在一个问题,那就是 C/C++ 是 Native 环境,没有语言运行时或者框架来帮助你构建一个异步的环境,提供异步 I/O 的接口------那么使用协程的意义也不大:因为协程往往需要异步机制的支持,否则就会退化为同步代码。

比如说,笔者这里使用 C++ 的 Qt 环境进行 GUI 界面开发,如果将 4.1 节的示例代码直接 移植到 Qt 的主线程中运行(例如在某个按钮点击槽函数中执行这个 while 循环),协程就可以正常运行并且界面不卡吗?答案肯定是不行的,即使笔者把 update_ui() 改成更新 QProgressBar,Qt 的界面仍然会卡死,直到任务最终完成。这是因为代码中的 while 循环会完全占据 Qt 主线程,不会返回控制权给 Qt 事件循环(QEventLoop),导致界面无法重绘(进度条不动、窗口白屏),即使调用了 progressBar->setValue(50),也不会立即显示,因为重绘事件被阻塞了。

Qt 的 UI 更新(包括 setValue、repaint、update)只是将重绘请求放入事件队列,必须等当前函数退出、回到 QApplication::exec() 的事件循环后才会真正绘制。

在 Web 的浏览器环境中,协程通常配合异步 I/O 接口来实现;那么在 Qt 环境中,就应该配合 Qt 的异步环境,也就是 Qt 事件循环。更为具体的说,可以使用定时器组件QTimer的信号槽。具体实例如下所示:

cpp 复制代码
#include <QApplication>
#include <QProgressBar>
#include <QThread>
#include <QTimer>
#include <QVBoxLayout>
#include <QWidget>
#include <boost/coroutine2/all.hpp>
#include <iostream>

// 声明协程类型
using coroutine_t = boost::coroutines2::coroutine<void>;

// 模拟耗时的子函数,支持 yield
void do_heavy_subtask(int outer_i, coroutine_t::push_type& yield) {
  for (int j = 1; j <= 20; ++j) {
    QThread::msleep(200);  // 模拟子任务耗时
    std::cout << "    Subtask: " << outer_i << "." << j << std::endl;
    yield();  // 子任务也可以让出执行权
  }
}

class CoroutineWidget : public QWidget {
  Q_OBJECT

 public:
  CoroutineWidget(QWidget* parent = nullptr)
      : QWidget(parent), progress(0), timer(new QTimer(this)) {
    progressBar = new QProgressBar(this);
    progressBar->setRange(0, 100);
    progressBar->setValue(0);

    auto* layout = new QVBoxLayout(this);
    layout->addWidget(progressBar);

    // 外层协程体
    coroutine = std::make_unique<coroutine_t::pull_type>(
        [this](coroutine_t::push_type& yield) {
          for (int i = 1; i <= 10; ++i) {
            QThread::msleep(300);  // 模拟主任务耗时
            this->progress = i * 10;
            std::cout << "Main loop i = " << i << std::endl;

            // 调用耗时子任务函数,并传递 yield
            do_heavy_subtask(i, yield);

            yield();  // 主任务也可以选择挂起
          }
        });

    connect(timer, &QTimer::timeout, this, &CoroutineWidget::updateTask);
    timer->start(100);  // 每100ms调用一次
  }

 private slots:
  void updateTask() {
    if (*coroutine) {
      (*coroutine)();  // 执行协程一小段
      std::cout << progress << std::endl;
      progressBar->setValue(progress);  // 更新 UI
    } else {
      timer->stop();
    }
  }

 private:
  using coroutine_t = boost::coroutines2::coroutine<void>;

  int progress = 0;
  QProgressBar* progressBar;
  QTimer* timer;
  std::unique_ptr<coroutine_t::pull_type> coroutine;
};

#include "main.moc"

int main(int argc, char* argv[]) {
  QApplication app(argc, argv);

  CoroutineWidget w;
  w.show();

  return app.exec();
}

在这段代码实现中,不仅实现了在协程体的顶层函数中yield

cpp 复制代码
// 外层协程体
coroutine = std::make_unique<coroutine_t::pull_type>(
    [this](coroutine_t::push_type& yield) {
      for (int i = 1; i <= 10; ++i) {
        QThread::msleep(300);  // 模拟主任务耗时
        this->progress = i * 10;
        std::cout << "Main loop i = " << i << std::endl;

        // 调用耗时子任务函数,并传递 yield
        do_heavy_subtask(i, yield);

        yield();  // 主任务也可以选择挂起
      }
    });

还实现了在顶层函数的子函数中yield

cpp 复制代码
// 模拟耗时的子函数,支持 yield
void do_heavy_subtask(int outer_i, coroutine_t::push_type& yield) {
  for (int j = 1; j <= 20; ++j) {
    QThread::msleep(200);  // 模拟子任务耗时
    std::cout << "    Subtask: " << outer_i << "." << j << std::endl;
    yield();  // 子任务也可以让出执行权
  }
}

这是因为虽然无栈协程并不支持任意挂起,但是可以把 yield(即 push_type&)作为参数传递给子函数,从而实现了细粒度的让出控制。

另一方面,通过定时器 QTimer 来恢复控制权:

cpp 复制代码
connect(timer, &QTimer::timeout, this, &CoroutineWidget::updateTask);
timer->start(100);  // 每100ms调用一次

在响应函数而不是 while 循环中执行协程和 UI 更新:

cpp 复制代码
void updateTask() {
  if (*coroutine) {
    (*coroutine)();  // 执行协程一小段
    std::cout << progress << std::endl;
    progressBar->setValue(progress);  // 更新 UI
  } else {
    timer->stop();
  }
}

从而实现了协程与 Qt 异步模型(事件循环 + 信号槽)的融合,改善了界面交互。

4.3 深入认识

从上述两个实例中可以看到,协程有个很重要的优势,就是可以写出看起来像同步顺序执行,但实际上可以挂起/恢复的异步代码。换句话说,协程只是 让异步代码长得像同步的语法工具。协程本身不会加速单个任务的执行,也不会带来真正的并行计算(CPU 并发),但是可以极大提升 I/O 密集型应用的吞吐量。此外还具有另外两点优势:

第1点是解决了异步代码难以维护的问题。比如说回调函数,它将"时间上的先后顺序"强行转换为"空间上的嵌套结构",破坏了代码的线性逻辑,导致可读性、可维护性、可扩展性全面下降:

js 复制代码
readFile(a, (x)=>{
  readFile(b, (y)=>{
    db.query(z, ()=>{
      ...
    });
  });
});

而协程将异步操作"拉回"线性执行流,使异步代码在语法和逻辑上都保持同步风格的清晰结构,从而在不阻塞线程的前提下,显著提升了可读性、可维护性和开发体验:

js 复制代码
let x = await readFile(a);
let y = await readFile(b);
await db.query(z);

第2点则是显著改善了用户的 GUI 交互体验,有效避免后台任务导致界面卡顿或无响应。以 Qt 环境中来说,虽然 Qt 更加推荐使用 QThread + 信号槽的方式来解决卡顿的问题,但是其实不是所有的任务都可以放在后台的线程中执行的。比如渲染绘制任务,通过 QPainter 绘制二维图形只允许在主线程中进行,这个时候就可以通过协程来分担比较繁重的绘制任务。

5 优化

笔者使用协程是想通过协程改进地图的绘制。绝大多数情况下,二维/三维图形的绘制都是只能在主线程中进行,但是地图的绘制任务可能会随着图层的增加而更加繁重,造成 GUI 页面卡顿。在这种情况下,协程可以将多个图层的绘制任务拆碎,每绘制一个图层,就通过 yield 让出控制权;配合定时器QTimer的信号槽机制,在 GUI 空闲的时候恢复控制权进行持续绘制,从而改善地图 GUI 的绘制交互体验。

更进一步的,如果单个图层内部的绘制也很耗时,那么可以在单个图层内部通过协程进一步拆分任务,实现颗粒度更细的让出控制。比如绘制瓦片地图,如果等所有的地图瓦片都经过远端读取->合并处理->可视化绘制的流程,那么地图 GUI 界面一定很卡,因为涉及到的地图瓦片可能非常多。因此,合理的处理方法就是每绘制一个地图瓦片就 yield 让出控制权;这样界面就能在绘制过程中持续响应用户的操作------比如平移、缩放或点击------从而让用户获得更流畅、更即时的交互体验。

另一个典型的例子是 QGIS/ArcGIS 等软件加载矢量地图。如果数据量很大,矢量地图往往是分批加载,地图是分批可视化出来的,并且GUI界面完全不卡。这种技术完全可以通过协程来实现。

不过,即使采取上述协程的操作也不是完全就没有问题了。关键的地方就在于远端访问并不是一个确定就能成功的操作,访问失败,访问超时,访问成功但是耗时很长都是很常见的现象。在这种情况下,即使每次只绘制一个瓦片,也很可能会造成 GUI 界面卡顿的问题。问题的关键就在于,C++ 环境下大多数远端访问的接口都是同步操作,没有异步 I/O 接口的支持,协程就不能最大程度的发挥价值。

据说 C++ 还是有一些远端访问的异步接口实现,但是笔者没有尝试去找。说到底要提升速度确实也只能依靠多线程并行,远端访问的操作确实也很适合放到多线程中。不过使用线程也有问题,因为线程的代价很高,不能需要获取一个瓦片就启动一个线程,那么就需要使用线程池。另一方面,也要考虑到获取瓦片的线程与主线程如何通信的问题,可以使用线程异步 std::future。伪代码如下所示:

cpp 复制代码
std::future<std::shared_ptr<cv::Mat>> GetTileAsync(
    const std::string& remoteAddress,
    const std::filesystem::path& localFilePath) {
  return threadPool.Submit([this, remoteAddress, localFilePath]() {
    return GetTile(remoteAddress, localFilePath);
  });
}

void Read(){
  //...

  //异步下载瓦片
  auto future = GetTileAsync(tileAddress, cachePath);

  // 主动轮询 future 状态,未完成时 yield 出去
  while (future.wait_for(std::chrono::milliseconds(0)) !=
        std::future_status::ready) {
    if (yield) yield();
  }

  std::shared_ptr<cv::Mat> img = future.get();
  if (!img) continue;

  //...
}

这段代码的意思是,主动轮询 future 状态:如果线程池中的任务完成,就继续往下执行;如果线程池中的任务没有完成,就 yield 让出。这样既不会堵塞住主线程,也可以让获取远端瓦片的任务放在主线程,同时保证用户的体验和性能效率。

在这里,笔者想说的是协程并不是万能的,尤其是在 C/C++ 环境中可能缺少的异步机制的支持,可能还是需要多线程的支持。另外,笔者的实践可能也不是最好的,比如说 while 轮询,间隔时间长了会造成堵塞;间隔时间短了又会造成 CPU 空转。有时间的话再进行进一步改进。

相关推荐
用户805533698031 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner1 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz6 天前
QML Hello World 入门示例
qt
xcyxiner9 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner10 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能13 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G13 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt