尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Qt多线程互斥与同步实战:QMutex、QReadWriteLock、QSemaphore与QWaitCondition详解
1. 从一次数据错乱说起为什么线程互斥不是可选项很多人写多线程程序时第一反应是“我把任务丢进线程里各跑各的不就行了”。我刚开始接触 Qt 多线程那会儿也是这么想的结果在一个数据采集的小工具上栽了跟头。那个工具用两个线程分别往同一个缓冲区写数据逻辑上看起来完全独立——一个写传感器读数一个写时间戳。跑单次没问题跑上几分钟就开始出现时间戳和读数对不上的情况偶尔还会整个界面卡死。排查了大半天才反应过来两个线程同时操作了同一个数据结构一个线程刚写完一半另一个线程就插进来改了内存。这就是典型的竞态条件。Qt 提供了不少工具来处理这类问题但工具本身不会自动帮你避开坑你得先理解为什么需要互斥再谈怎么用。这篇文章主要聊 Qt 里线程互斥与同步的实战用法。我会从最基础的QMutex讲起一路说到QReadWriteLock、QSemaphore、QWaitCondition这些工具各自适合什么场景以及我在实际项目里踩过的坑。如果你正在写或者准备写 Qt 多线程程序尤其是涉及共享数据读写的场景这些内容应该能帮你省下不少调试时间。提示本文假设你已经了解 Qt 多线程的基本用法比如QThread的启动、moveToThread的线程亲和性等。如果这些还不熟建议先补一下基础再来看互斥部分。2. QMutex最直接的锁也是最容易用错的锁2.1 互斥锁到底在锁什么QMutex是 Qt 里最基础的互斥工具。它的逻辑很简单同一时刻只允许一个线程进入被保护的代码段。你可以把它想象成一把钥匙谁拿到钥匙谁才能进房间其他人只能在门外等着。但这里有个关键点很多人会忽略锁保护的是数据不是代码。我见过不少代码把mutex.lock()和mutex.unlock()写在某个函数的两端就以为这个函数安全了。实际上如果这个函数操作的数据在别的地方也被访问而那个地方没有加锁照样会出问题。锁的有效性取决于所有访问共享数据的地方是否都遵守了同一套加锁规则。// 一个典型的错误示范 class DataBuffer { public: void writeData(const QByteArray data) { // 这里没加锁 m_buffer.append(data); } QByteArray readData() { QMutexLocker locker(m_mutex); return m_buffer; } private: QByteArray m_buffer; QMutex m_mutex; };上面这段代码里readData加了锁但writeData没有。结果就是读的时候可能读到写了一半的数据。这种问题在单线程测试时几乎不会暴露一旦上多线程就原形毕露。2.2 QMutexLocker 的正确打开方式Qt 提供了QMutexLocker这个 RAII 风格的辅助类它的作用是在构造时加锁析构时自动解锁。这比手动调用lock()和unlock()安全得多因为即使中间发生了异常或者提前 return锁也能被正确释放。void safeOperation() { QMutexLocker locker(m_mutex); // 如果这里抛出异常locker 析构时会自动解锁 if (someCondition) { return; // 提前返回也没问题 } // 操作共享数据 m_sharedData.modify(); }我个人的习惯是只要用了 QMutex就尽量用 QMutexLocker 来管理。手动 lock/unlock 的代码十个里面有八个在某个分支上漏掉了 unlock然后程序就死锁了。死锁这种东西调试起来比数据错乱还痛苦因为程序直接卡住连日志都不一定打得出来。2.3 递归锁与非递归锁的选择QMutex默认是非递归的。也就是说同一个线程如果连续两次调用lock()第二次会直接死锁——因为第一次已经锁住了第二次在等自己释放。QMutex mutex; void funcA() { mutex.lock(); funcB(); // funcB 里又去 lock 同一个 mutex mutex.unlock(); } void funcB() { mutex.lock(); // 死锁同一个线程重复加锁 // ... mutex.unlock(); }如果你确实需要递归加锁可以把QMutex的构造参数设为QMutex::RecursiveQMutex recursiveMutex(QMutex::Recursive);但我的建议是尽量避免使用递归锁。递归锁往往意味着你的代码结构有问题——函数之间的调用关系太耦合导致锁的边界不清晰。更好的做法是把锁的粒度控制好让每个函数只负责自己那一层的加锁逻辑不要跨层传递锁的状态。2.4 tryLock 的超时机制有时候你不想让线程无限期地等锁这时候可以用tryLockif (mutex.tryLock(100)) { // 最多等 100 毫秒 // 拿到锁了 // ... mutex.unlock(); } else { // 没拿到锁做其他处理 qDebug() 获取锁超时跳过本次操作; }这个在实时性要求高的场景里很有用。比如一个 UI 线程要去读后台线程的数据如果后台线程正好在写UI 线程不能一直卡着等否则界面就无响应了。这时候用tryLock设个短超时拿不到就先用旧数据或者显示加载状态用户体验会好很多。3. QReadWriteLock读多写少场景的专属方案3.1 为什么 QMutex 在这种场景下不够用假设你有一个配置数据结构多个线程都要读它但只有偶尔才会写一次。如果用QMutex每次读的时候也要加锁这意味着多个读线程之间也会互相阻塞。但实际上读操作之间是不需要互斥的——大家只是看又不会改坏东西。QReadWriteLock就是为解决这个问题设计的。它区分了读锁和写锁读锁多个线程可以同时持有互不干扰写锁同一时刻只能有一个线程持有且持有写锁时不允许任何读锁存在QReadWriteLock rwLock; QHashQString, QVariant configData; // 读操作 QVariant readConfig(const QString key) { QReadLocker locker(rwLock); return configData.value(key); } // 写操作 void writeConfig(const QString key, const QVariant value) { QWriteLocker locker(rwLock); configData.insert(key, value); }3.2 读写锁的适用边界读写锁不是万能的。它的优势在于读操作远多于写操作的场景。如果读写比例接近或者写操作很频繁读写锁反而可能比普通互斥锁更慢——因为它内部要维护读线程计数、写线程等待队列等额外状态。我一般用这个经验法则来判断如果读操作占比超过 80%考虑用 QReadWriteLock否则直接用 QMutex 更简单。另外要注意QReadWriteLock默认也是非递归的读锁和写锁之间不能嵌套获取。还有一个容易踩的坑写锁的优先级问题。在某些实现中如果读线程持续不断地获取读锁写线程可能会被“饿死”一直拿不到写锁。Qt 的QReadWriteLock默认采用“写优先”策略也就是说当有写线程在等待时新的读请求会被阻塞直到写操作完成。这个行为在大多数场景下是合理的但如果你有特殊的公平性需求需要留意这一点。3.3 读写锁的常见误用我见过一种写法用读锁去保护一个“看起来只是读”的操作但实际上这个操作内部修改了数据QVariantMap getAndUpdateStats() { QReadLocker locker(rwLock); // 错误这里用了读锁 m_stats[accessCount]; // 但这里在修改数据 return m_stats; }这种代码在单线程下跑没问题多线程下就会出各种诡异的数据不一致。读锁只保证“不会有人同时写”但它不阻止你自己写。如果你在读锁保护下修改了数据其他读线程可能同时也在读看到的就是改了一半的状态。4. QSemaphore 与 QWaitCondition更灵活的同步手段4.1 QSemaphore 解决的是“数量”问题QMutex保护的是“一个资源”而QSemaphore保护的是“N 个资源”。最典型的例子是生产者-消费者模型里的缓冲区假设缓冲区有 10 个槽位生产者要往里面放数据消费者要从里面取数据。QSemaphore freeSlots(10); // 10 个空闲槽位 QSemaphore usedSlots(0); // 初始没有已用槽位 QVectorint buffer(10); // 生产者线程 void producer() { for (int i 0; i 100; i) { freeSlots.acquire(); // 等待空闲槽位 buffer[i % 10] i; usedSlots.release(); // 通知消费者有数据了 } } // 消费者线程 void consumer() { for (int i 0; i 100; i) { usedSlots.acquire(); // 等待有数据 int value buffer[i % 10]; freeSlots.release(); // 释放一个空闲槽位 // 处理 value } }这里freeSlots和usedSlots配合使用天然地实现了“缓冲区满时生产者等待缓冲区空时消费者等待”的逻辑。比用互斥锁加条件变量手动判断要简洁得多。4.2 QWaitCondition 的使用要点QWaitCondition允许一个线程在某个条件不满足时主动等待直到另一个线程通知它条件可能已经满足。它必须和QMutex配合使用。QMutex mutex; QWaitCondition condition; bool ready false; // 等待线程 void waitThread() { QMutexLocker locker(mutex); while (!ready) { condition.wait(mutex); // 等待时会释放 mutex被唤醒时重新获取 } // 条件满足继续执行 } // 通知线程 void notifyThread() { QMutexLocker locker(mutex); ready true; condition.wakeAll(); // 或 wakeOne() }这里有几个关键点第一wait 必须在循环里调用。因为wakeAll唤醒后线程需要重新竞争 mutex等它拿到锁的时候条件可能又被别的线程改回去了。所以必须用while而不是if来检查条件。第二wait 会释放 mutex。这是很多人困惑的地方——如果 wait 不释放锁那通知线程永远拿不到锁也就永远无法发出通知程序就死锁了。Qt 的QWaitCondition::wait在等待期间会原子地释放 mutex被唤醒时再重新获取。第三wakeOne 和 wakeAll 的选择。如果多个线程在等同一个条件wakeOne只唤醒一个wakeAll唤醒全部。一般来说如果条件满足后所有等待线程都能继续工作用wakeAll如果只有一个线程能继续比如只有一个资源可用用wakeOne更高效。4.3 信号量 vs 条件变量怎么选这两个工具功能上有重叠但适用场景不同。我的经验是场景特征推荐工具资源数量固定需要计数QSemaphore等待某个状态变化QWaitCondition生产者-消费者缓冲区QSemaphore 更简洁复杂条件判断QWaitCondition 更灵活需要超时等待两者都支持但 QWaitCondition 的接口更直接QSemaphore的acquire和release语义非常清晰适合“我有 N 个东西可以用”这种模型。QWaitCondition则更适合“等到某个布尔条件为真”的场景。实际项目里两者经常混用比如用信号量控制资源数量用条件变量通知状态变化。5. 原子操作不用锁也能保证安全5.1 QAtomicInteger 和 QAtomicPointer有些场景下加锁的开销比操作本身还大。比如只是对一个整数做自增用QMutex保护就有点杀鸡用牛刀了。Qt 提供了原子类型来处理这类简单操作QAtomicInt counter(0); // 多个线程同时调用不会出错 void increment() { counter.fetchAndAddRelaxed(1); } int getValue() { return counter.loadRelaxed(); }QAtomicInt的fetchAndAdd操作是原子的底层由 CPU 的原子指令保证不需要操作系统层面的锁。性能比QMutex高很多。5.2 内存顺序Relaxed、Acquire、Release原子操作有一个容易让人迷糊的概念内存顺序。简单来说它决定了原子操作前后的普通内存访问会不会被重排序。Relaxed只保证操作本身原子不保证顺序Acquire保证此操作之后的读写不会被重排到它前面Release保证此操作之前的读写不会被重排到它后面QAtomicInt dataReady(0); int sharedData 0; // 线程 A写数据 void writer() { sharedData 42; dataReady.storeRelease(1); // 保证 sharedData 的写入在 storeRelease 之前完成 } // 线程 B读数据 void reader() { if (dataReady.loadAcquire() 1) { // 能保证看到 sharedData 42 int value sharedData; } }如果你只是用原子变量做计数器Relaxed就够了。但如果原子变量被用来做线程间的“标志位”就需要考虑Acquire和Release的配对使用。这个知识点比较底层日常开发中如果拿不准用默认的SeqCst顺序一致性最安全只是性能略低一点。5.3 原子操作的局限性原子操作只能保护单个变量的读写。如果你需要同时修改两个相关的变量原子操作就无能为力了——因为两个原子操作之间仍然可能被其他线程插入。QAtomicInt x(0), y(0); // 线程 A x.fetchAndAddRelaxed(1); y.fetchAndAddRelaxed(1); // 线程 B 可能在线程 A 的两个操作之间看到 x1, y0 的中间状态这种情况下还是得用互斥锁把两个操作包在一起。原子操作适合“独立、简单”的共享变量复杂的数据结构还是交给锁来管。6. 死锁的四种典型成因与排查思路6.1 加锁顺序不一致这是最常见的死锁原因。线程 A 先锁 M1 再锁 M2线程 B 先锁 M2 再锁 M1。当 A 拿到 M1、B 拿到 M2 时双方都在等对方释放死锁形成。// 线程 A mutex1.lock(); mutex2.lock(); // 等 B 释放 mutex2 // ... // 线程 B mutex2.lock(); mutex1.lock(); // 等 A 释放 mutex1 // ...解决方案给所有锁定义一个全局的获取顺序所有线程都按这个顺序加锁。比如按锁的内存地址排序或者按业务逻辑定义一个固定的层级。6.2 忘记释放锁手动 lock/unlock 的代码如果在某个分支上漏掉了 unlock锁就永远不会释放。用QMutexLocker可以基本杜绝这个问题但如果你在QMutexLocker的作用域内又手动调用了lock()那还是会出问题。6.3 在持有锁时调用外部代码这是一个隐蔽的坑。你持有锁的时候调用了一个回调函数或者虚函数而这个函数在某个实现里又去获取了另一把锁就可能形成死锁。void processData() { QMutexLocker locker(m_mutex); // 调用了一个可能获取其他锁的回调 emit dataProcessed(); // 如果槽函数里又去获取了别的锁... }建议尽量不要在持有锁的时候发出信号或者调用不确定的外部代码。先把数据准备好释放锁之后再发信号。6.4 死锁的排查手段死锁一旦发生程序就卡住了日志可能也停了。这时候可以用调试器 attach 到进程查看各个线程的调用栈找到它们在等哪把锁在加锁和解锁的地方打日志看最后卡在哪个锁上使用 Qt 的QThread::currentThread()在日志里区分线程我个人的习惯是在开发阶段给每个QMutex起个名字用一个包装类加锁时打印线程 ID 和锁名这样一旦死锁从日志就能看出是谁在等谁。7. 实战中的锁粒度与性能权衡7.1 锁太粗和锁太细都有问题锁的粒度是指锁保护的数据范围。粒度太粗比如用一个全局锁保护所有共享数据会导致线程之间频繁互相等待多线程退化成单线程。粒度太细比如每个小数据都配一把锁代码会变得复杂而且容易漏锁。我的经验是先按数据模块划分锁一个模块一把锁。比如用户数据一把锁配置数据一把锁日志数据一把锁。这样既不会太粗也不会太细。如果后续发现某个模块内部竞争特别激烈再考虑进一步拆分。7.2 减少锁的持有时间一个原则能在锁外做的事就不要放在锁内。比如// 不好的做法 void updateData(const QByteArray newData) { QMutexLocker locker(m_mutex); QByteArray processed heavyProcessing(newData); // 耗时操作在锁内 m_data processed; } // 更好的做法 void updateData(const QByteArray newData) { QByteArray processed heavyProcessing(newData); // 先在锁外处理 QMutexLocker locker(m_mutex); m_data processed; // 只有赋值在锁内 }这样锁的持有时间从“处理时间”缩短到“赋值时间”其他线程等待的时间大大减少。7.3 读写锁的实际性能测试我在一个数据缓存项目里对比过QMutex和QReadWriteLock的性能。场景是 8 个读线程、1 个写线程读操作占比约 90%。测试结果锁类型平均读延迟平均写延迟吞吐量QMutex12 微秒15 微秒基准值QReadWriteLock3 微秒45 微秒提升约 2.8 倍可以看到读延迟大幅降低写延迟有所增加但整体吞吐量提升明显。这个结果符合预期读写锁在读多写少的场景下确实有优势但写操作的代价变高了。7.4 无锁编程的诱惑与陷阱有时候你会听到“无锁编程”这个词觉得不用锁就能保证线程安全性能肯定更好。但实际情况是无锁编程的复杂度极高容易出微妙的 bug而且不是所有场景都能做到无锁。Qt 提供的原子操作可以算是一种轻量级的无锁手段但它只适用于简单的变量操作。对于复杂的数据结构比如链表、哈希表实现无锁版本需要非常深厚的功底而且调试困难。我的建议是除非性能瓶颈确实在锁竞争上否则优先用锁把精力放在业务逻辑上。8. 一个完整的线程安全队列实现8.1 需求分析队列是最常用的线程间通信结构之一。生产者往队列里放数据消费者从队列里取数据。需要满足队列为空时消费者等待队列满时如果有容量限制生产者等待多生产者、多消费者都能正确工作8.2 基于 QMutex QWaitCondition 的实现templatetypename T class ThreadSafeQueue { public: explicit ThreadSafeQueue(int maxSize -1) : m_maxSize(maxSize) {} void enqueue(const T item) { QMutexLocker locker(m_mutex); // 如果队列满了等待 while (m_maxSize 0 m_queue.size() m_maxSize) { m_notFull.wait(m_mutex); } m_queue.enqueue(item); m_notEmpty.wakeOne(); // 通知消费者有数据了 } T dequeue() { QMutexLocker locker(m_mutex); // 如果队列空了等待 while (m_queue.isEmpty()) { m_notEmpty.wait(m_mutex); } T item m_queue.dequeue(); m_notFull.wakeOne(); // 通知生产者有空位了 return item; } bool tryDequeue(T item, int timeoutMs 0) { QMutexLocker locker(m_mutex); if (m_queue.isEmpty()) { if (timeoutMs 0) return false; if (!m_notEmpty.wait(m_mutex, timeoutMs)) { return false; // 超时 } } if (m_queue.isEmpty()) return false; item m_queue.dequeue(); m_notFull.wakeOne(); return true; } int size() const { QMutexLocker locker(m_mutex); return m_queue.size(); } private: QQueueT m_queue; mutable QMutex m_mutex; QWaitCondition m_notEmpty; QWaitCondition m_notFull; int m_maxSize; };8.3 实现中的关键细节为什么用 while 而不是 if前面提过wait返回后条件可能又变了。比如消费者被唤醒后在重新获取 mutex 之前另一个消费者可能已经把数据取走了。所以必须重新检查队列是否为空。为什么用 wakeOne 而不是 wakeAll每次入队只增加一个元素只需要唤醒一个消费者就够了。唤醒全部会导致“惊群效应”——多个消费者同时醒来但只有一个能拿到数据其他的又得回去等浪费 CPU。size() 函数为什么也要加锁因为QQueue::size()不是原子操作如果另一个线程正在修改队列直接读 size 可能读到不一致的值。虽然加锁会让 size() 变慢但保证了正确性。8.4 使用示例ThreadSafeQueueQByteArray dataQueue(100); // 生产者线程 void producerThread() { for (int i 0; i 1000; i) { QByteArray data generateData(i); dataQueue.enqueue(data); } } // 消费者线程 void consumerThread() { while (true) { QByteArray data dataQueue.dequeue(); processData(data); } }这个队列实现可以直接用在大多数生产者-消费者场景里。如果不需要容量限制把maxSize设为 -1 即可。9. 那些文档里不会写的经验教训9.1 锁的命名与调试给每个锁起一个有意义的名字在加锁失败或者超时的时候打印出来。Qt 的QMutex本身没有名字但你可以用一个简单的包装类来实现class NamedMutex { public: explicit NamedMutex(const QString name) : m_name(name) {} void lock() { if (!m_mutex.tryLock(5000)) { qWarning() 锁超时: m_name 线程: QThread::currentThread(); m_mutex.lock(); // 继续等但至少日志里有了线索 } } void unlock() { m_mutex.unlock(); } private: QMutex m_mutex; QString m_name; };这个在排查死锁时非常有用。我曾经在一个项目里靠这个日志定位到一个隐藏的加锁顺序问题省了好几个小时的调试时间。9.2 信号槽与锁的交互Qt 的信号槽机制在跨线程时默认是队列连接QueuedConnection也就是说信号的发射和槽的执行不在同一个线程。这带来一个微妙的问题如果你在持有锁的时候发射信号槽函数在另一个线程执行时如果也去获取同一把锁就会死锁。// 危险的做法 void updateAndNotify() { QMutexLocker locker(m_mutex); m_data newData; emit dataChanged(); // 槽函数可能在另一个线程执行如果它也锁 m_mutex... }安全的做法是先释放锁再发信号void updateAndNotify() { { QMutexLocker locker(m_mutex); m_data newData; } // 锁在这里释放 emit dataChanged(); }9.3 条件变量的虚假唤醒QWaitCondition在某些平台上可能会出现“虚假唤醒”——没有任何人调用wakeOne或wakeAllwait就返回了。虽然 Qt 在内部做了一些处理但最安全的做法还是用while循环检查条件这样即使虚假唤醒发生了条件不满足就继续等不会出错。9.4 性能测试的重要性锁的选择和粒度调整最终要看实际性能数据。不要凭感觉猜“这个锁应该够快了”。用QElapsedTimer做简单的计时或者用性能分析工具看锁的竞争情况。我见过一个项目开发者花了两天时间把QMutex换成无锁队列结果性能只提升了 3%因为瓶颈根本不在锁上而在数据处理逻辑里。9.5 从单线程到多线程的迁移策略如果你有一个单线程程序要改成多线程不要一上来就把所有东西都并行化。我的建议是先找出真正的性能瓶颈只把瓶颈部分放到线程里共享数据尽量少能通过信号槽传递的就不要共享每加一个共享数据就配一把锁并且写清楚注释说明这把锁保护什么用压力测试跑长时间观察是否有数据不一致或者死锁多线程编程的难点不在于写代码而在于想清楚哪些数据会被哪些线程访问以及访问的顺序和时机。把这些问题想明白了代码自然就写对了。
RELATED

相关推荐

IPD开发中DCP与TR评审要素表:从评审靠感觉到有据可依

IPD开发中DCP与TR评审要素表:从评审靠感觉到有据可依

简介:IPD集成产品开发各阶段评审要素说明,是一份面向产品经理、研发经理、项目经理及评审委员的实用模板,系统梳理了从立项任务书、概念评审到发布评审的完整TR/DCP评审链路。文档将Phase0-1至Phase0-13各阶段的关键查核点逐一列出&#xff0…

📅 2026/10/14 1:09:55
GAN演进之路:从原始博弈到StyleGAN的生成对抗实践

GAN演进之路:从原始博弈到StyleGAN的生成对抗实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/14 1:09:55
电动汽车性能开发目标模板解析与应用

电动汽车性能开发目标模板解析与应用

1. 项目概述:电动汽车性能开发的导航图在电动汽车研发领域,性能目标设定就像建筑师的蓝图,直接决定了整车开发的方向和边界。这份Ford性能目标模板的独特价值在于,它将传统汽车巨头数十年的整车集成经验与电动化特性深度融合&…

📅 2026/10/14 1:09:55
MORE NEWS

更多资讯

📰

PoB离线Build规划:天赋树与伤害计算一次算清

PoB离线Build规划:天赋树与伤害计算一次算清 【免费下载链接】PathOfBuilding Offline build planner for Path of Exile. 项目地址: https://gitcode.com/GitHub_Trending/pa/PathOfBuilding 凌晨两点,你又一次在天赋树上手动数格子:…

📰

微信API发送失败别盲目重试:个人微信二次开发判断法

客服反馈:「发货通知怎么发了两条?」 打开日志,往往是这样:第一次请求超时了,程序自动又发了一次;其实第一条早就到了。 个人微信 API 二次开发里,失败就重试听着负责,却是双发和失…

📰

CKEditor 4 聚焦状态下销毁内联编辑器的空引用崩溃:`isInline of null` 问题与 Bender 手动测试解析

前端UI组件 【免费下载链接】ckeditor4 The best enterprise-grade WYSIWYG editor. Fully customizable with countless features and plugins. 项目地址: https://gitcode.com/gh_mirrors/ck/ckeditor4 点击查看 免费下载 导读 本文围绕 CKEditor 4 官方仓库中的…

📰

如何在电脑上运行《索尼克:释放》:Unleashed Recompiled 静态重编译版完整上手指南

如何在电脑上运行《索尼克:释放》:Unleashed Recompiled 静态重编译版完整上手指南 【免费下载链接】UnleashedRecomp An unofficial PC port of the Xbox 360 version of Sonic Unleashed created through the process of static recompilation. 项目…

📰

叉积判向:从三角形朝向到背面剔除的工程实战

做图形学的人,早晚都会撞上"叉积"这堵墙。第一次在代码里写cross(a, b) ax*by - ay*bx的时候,我压根没意识到,这个只有一行乘减运算的小东西,居然能决定三角形到底是顺时针还是逆时针,进而影响到背面剔除、…

📰

Wing 语言 inflight Handler 单例语义剖析:以 `inflight_handler_singleton.test.w` 快照测试为例

编程语言编译器云原生语言运行时 【免费下载链接】wing A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡ 项目地址: https://gitcode.com/gh_mirrors/wi/wing 点击查看 免…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬