Re:Linux系统篇(五十五)线程篇 · 八:为什么需要条件变量?从生活比喻、API 梳理到 pthread_cond 实战源码探究
◆ 博主名称 小此方-CSDN博客大家好欢迎来到小此方的博客。⭐️Linux系列个人专栏 【主题曲】Linux⭐️此方的GitHub github_此方⭐️Re系列专栏我们思考 (Rethink) · 我们重建 (Rebuild) · 我们记录 (Record)文章目录概要序論一、从互斥到同步为什么仅有互斥是不够的1.1 纯互斥场景下的极端故事超级自习室1.1.1 争夺钥匙与高效释放1.1.2 极端循环与“饥饿问题”Tips:其他线程获取临界资源的契机1.2 问题的解决方案与临时调优1.2.1 临时调优方案在循环中加入微小休眠1.2.2 制度层面重塑管理者规则与队列机制1.3 引入线程同步Thread Synchronization1.3.1 什么是线程同步1.3.2 互斥与同步的互补关系二、理解同步的概念2.1 一个游戏拿苹果与放苹果2.1.1 阶段一没有铃铛与队列纯互斥逻辑2.1.2 阶段二引入“铃铛与队列”同步机制2.2 专业抽象的概念定义2.2.1 条件变量Condition Variable2.2.2 同步概念与竞态条件三、快速梳理条件变量的调用接口3.1 条件变量的初始化与销毁3.1.1 静态初始化3.1.2 动态初始化3.1.3 销毁条件变量3.2 在条件变量下等待pthread_cond_wait3.2.1在条件变量中等待的线程被唤醒后pthread_cond_wait内部做了什么工作3.3 唤醒在条件变量下等待的线程3.3.1 唤醒单个线程pthread_cond_signal3.3.2 广播唤醒所有线程pthread_cond_broadcast四、第一个测试 demo4.1 测试代码展示4.2 Demo 核心逻辑拆解与原理探究4.2.1 为什么一定要先加锁再调用 pthread_cond_wait4.2.2 pthread_cond_wait 内部隐藏的自动解锁机制4.2.3 线程被唤醒后的重新竞争锁概要序論Hello大家好我是此方上两篇我们讲到了互斥量的概念、接口、原理和封装。本文开始讲解同步和条件变量相关内容。一、从互斥到同步为什么仅有互斥是不够的引入新的技术必然是为了解决旧技术带来的新问题。在上一篇文章中我们学习了互斥锁Mutex的基本概念与使用。互斥成功解决了多线程并发访问临界资源时的“数据不一致”与“竞争条件”问题确保了同一时刻只有一个执行流能够进入临界区。然而单纯依靠“纯互斥”真的就完美无缺了吗1.1 纯互斥场景下的极端故事超级自习室为了更好地理解纯互斥带来的隐患我们来看一个生活中“超级自习室”的例子。假设有一个“超级自习室”里面只有一张桌子且自习室的门锁只能由一把钥匙打开。自习室相当于临界资源。钥匙相当于互斥锁。想进去自习的人相当于各个线程执行流。1.1.1 争夺钥匙与高效释放当一个人线程 A成功拿到了钥匙并进入自习室后其他人就只能在门外等待。过了一段时间线程 A 准备离开自习室。他打开门、走出自习室并把钥匙归还到了门口的钥匙挂钩上释放锁。但此时存在一个极其特殊的物理现象由于线程 A 离钥匙挂钩距离最近刚刚把钥匙挂上去他又可以瞬间再次伸手将钥匙拿下来此时门外排队的其他人在做什么呢其他的线程离钥匙挂钩相对较远或者正处于“准备唤醒”、“休眠恢复”等各种微小的系统开销与延迟中。因此线程 A 刚出来又立马申请到了钥匙重新关上门进入自习室。1.1.2 极端循环与“饥饿问题”在自习室内部线程 A 发现自己其实并没有什么实质性的自习任务可做没有做有效动作于是他又开门出来挂上钥匙。挂上钥匙的瞬间他又再次抢到了钥匙……如此循环往复申请锁进入临界区但未进行有效操作或高频重复操作释放锁凭借距离优势瞬间重新申请锁从互斥规则来看纯互斥有错吗没有任何错因为任何时刻确实都只有一个人进入了自习室数据安全得到了绝对保证。但是从系统整体的运行效率与公平性来看不高效线程 A 在频繁申请和释放锁却没能进行有效的业务处理。不太公平其他想要自习的人其他线程长时间在门外等待永远抢不过距离挂钩最近的线程 A。在多线程编程中这种其他线程长时间获取不到临界资源、导致无法继续推进的现象就称为线程饥饿问题Starvation。在 Ubuntu 或 CentOS 等不同的 Linux 发行版/内核调度环境下如果不加任何干预纯互斥锁的这种弊端往往表现得非常明显。Tips:其他线程获取临界资源的契机既然活跃线程每次都能近水楼台先得月其他线程有没有可能获取道临界资源呢我让GPT帮我整理了三种情况1.2 问题的解决方案与临时调优既然纯互斥可能会带来“饥饿问题”和“效率低下”我们该如何改进1.2.1 临时调优方案在循环中加入微小休眠在编写多线程代码时如果在循环体内释放锁之后加上一行极小的休眠指令如usleep(100);// 释放锁后给其他线程一些调度和申请的时间当线程 A 释放锁后主动休眠哪怕只有 100 微秒就能将CPU的使用权和抢锁机会让渡出来给其他线程留出反应和唤醒的时间。这种做法能够让各个线程更加平均地申请到锁显著缓解饥饿现象。但这仅仅是一种通过“人为硬编码延时”进行的妥协式调优并没有从根本的机制层面彻底解决“访问顺序”问题。1.2.2 制度层面重塑管理者规则与队列机制要从根本上解决自习室的秩序问题自习室管理员提出了两项严格的新规定不能立即申请第二次刚从自习室出来释放锁的人不能当场重新伸手抢钥匙。按顺序排队所有想进入自习室的人必须先在一个指定的队列末尾进行排队。刚出来的人如果还想再次进入必须跑回到队列的尾部重新排队等待下一次轮到自己。通过这种“先来后到”的排队约束所有人都有了明确且可预测的进入顺序。1.3 引入线程同步Thread Synchronization通过给纯互斥加上“按顺序排队/访问”的约束规则我们就正式引出了操作系统中的线程同步Thread Synchronization机制。1.3.1 什么是线程同步所谓的线程同步在保证临界资源安全互斥的前提下让所有的执行流按照某种一定的顺序来访问临界资源。1.3.2 互斥与同步的互补关系互斥解决的是“安全”问题确保数据不被破坏即“不能同时进”。同步解决的是“高效与公平”问题避免线程饥饿并提高整体系统的协同吞吐量即“有序轮流进”。只有将“互斥”与“同步”结合起来多线程对共享资源的协同访问才算真正走向了成熟与高效。二、理解同步的概念在深入操作系统底层接口之前我们先回顾一个之前接触过的类似机制管道Pipe。在进程间通信的管道中当管道为空时读端会被阻塞不能继续读取当管道被写满时写端会被阻塞不能继续写入。读端与写端之间这种“有资源才读、有空间才写”的配合本质上就是一种典型的同步机制。先讲抽象的概念容易劝退我们先来做一个游戏。2.1 一个游戏拿苹果与放苹果假设桌子上有一个盘子盘子里最多只能放一个苹果。现在有两类角色放苹果的人拿苹果的人由于盘子属于共享资源为了防止多人同时抢夺盘子上方挂着一把锁每次只能有一个人拿到锁并靠近盘子。2.1.1 阶段一没有铃铛与队列纯互斥逻辑在这个阶段桌子上只有一把锁。拿苹果的人因为蒙着眼睛在不拿到锁、伸手摸盘子之前根本不知道盘子里到底有没有苹果。此时的情况如下拿苹果的人先申请锁。拿到锁后伸手摸盘子发现盘子是空的。他什么也拿不到只能释放锁离开。由于他距离锁最近刚释放锁他又顺手重新申请了锁再摸一次盘子发现还是空的又释放锁……这种模式带来的危害拿苹果的人在没有苹果时疯狂地“申请锁→ \rightarrow→发现没苹果→ \rightarrow→释放锁→ \rightarrow→再申请锁”。极大地消耗了精力占用 CPU 资源却没有做出任何有效动作。而放苹果的人想过来放苹果却根本抢不到锁。2.1.2 阶段二引入“铃铛与队列”同步机制为了解决上述危害游戏规则进行了升级在盘子旁边放置了一个铃铛并在旁边设立了一条排队队列。现在的互动约定如下拿苹果的人申请锁后摸盘子如果摸到了苹果直接拿走释放锁离开。如果发现盘子为空他不再盲目重新抢锁而是主动释放锁并跑到队列末尾排队休眠。放苹果的人申请锁后将苹果放入盘子放完苹果后他去按一下铃铛。铃声响起唤醒在队列头部等待的第一个人。被唤醒的人直接从队头出来去拿苹果拿完后再回到队尾排队。引入后的好处拿苹果的人不再进行无意义的重复抢锁盘子为空时就安静地在队列中等待放苹果的人放完资源后通过“按铃”精准通知等待者。整个过程井然有序资源得到了最充分的利用。故事中的**“铃铛 队列”** 组合在 Linux 多线程编程中对应的正是条件变量Condition Variable2.2 专业抽象的概念定义看懂了上面的故事我们再来看操作系统中关于同步与条件变量的专业定义就会一目了然2.2.1 条件变量Condition Variable当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。例如一个线程访问队列时发现队列为空它只能等待直到其它线程将一个节点添加到队列中。这种情况下就需要用到条件变量。2.2.2 同步概念与竞态条件同步在保证数据安全互斥的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步。竞态条件Race Condition由于执行时序的问题而导致程序运行出现异常或结果不符合预期我们称之为竞态条件。三、快速梳理条件变量的调用接口条件变量的调用接口与我们前面讲过的互斥量调用接口十分相似在使用时都需要引入头文件pthread.h。在 POSIX 线程库中条件变量的数据类型为pthread_cond_t。下面我们对条件变量的核心 API 进行快速梳理。3.1 条件变量的初始化与销毁与互斥锁类似条件变量也分为静态初始化与动态初始化两种方式。3.1.1 静态初始化如果条件变量被定义为全局变量或static静态变量可以使用宏进行快速静态初始化无需手动销毁pthread_cond_tcondPTHREAD_COND_INITIALIZER;3.1.2 动态初始化如果条件变量是局部变量或在堆上动态分配的则需要调用pthread_cond_init函数进行动态初始化intpthread_cond_init(pthread_cond_t*restrict cond,constpthread_condattr_t*restrict attr);cond指向需要初始化的条件变量的指针。attr条件变量的属性通常设置为NULL表示使用默认属性。3.1.3 销毁条件变量对于通过pthread_cond_init动态初始化的条件变量在不再使用时必须调用pthread_cond_destroy进行销毁和资源释放intpthread_cond_destroy(pthread_cond_t*cond);3.2 在条件变量下等待pthread_cond_wait当线程访问临界资源前发现条件不满足时需要调用等待接口使自身挂起休眠进入该条件变量的等待队列中。intpthread_cond_wait(pthread_cond_t*restrict cond,pthread_mutex_t*restrict mutex);cond指定要在哪个条件变量下进行等待。mutex互斥锁指针。关键疑问这里可以注意到一个很特殊的细节——pthread_cond_wait的第二个参数接收了一把互斥锁mutex。为什么在条件变量下等待时必须把互斥锁作为参数传进去3.2.1在条件变量中等待的线程被唤醒后pthread_cond_wait内部做了什么工作脱离条件变量队列重新竞争互斥锁关键步骤成功获取锁函数准备返回函数返回继续向下执行代码3.3 唤醒在条件变量下等待的线程当其它线程改变了状态例如产生了新的资源、放下了苹果就需要去唤醒在条件变量队列中等待的线程。POSIX 提供了两种唤醒方式3.3.1 唤醒单个线程pthread_cond_signalintpthread_cond_signal(pthread_cond_t*cond);功能唤醒在指定条件变量cond下等待的一个线程相当于按一下铃铛唤醒队列头部的线程。3.3.2 广播唤醒所有线程pthread_cond_broadcastintpthread_cond_broadcast(pthread_cond_t*cond);功能唤醒在指定条件变量cond下等待的所有线程相当于大喇叭广播让队列中所有等待的线程同时醒来重新竞争资源。四、第一个测试 demo4.1 测试代码展示#includeiostream#includepthread.h#includevector#includeunistd.hintticket100000;pthread_mutex_t _mutexPTHREAD_MUTEX_INITIALIZER;pthread_cond_t _condPTHREAD_COND_INITIALIZER;void*RunThread(void*args){char*namestatic_castchar*(args);while(true){{pthread_mutex_lock(_mutex);pthread_cond_wait(_cond,_mutex);if(ticket0){ticket--;std::cout线程name抢到票: ticketstd::endl;}pthread_mutex_unlock(_mutex);}}}intmain(){intcnt5;std::vectorpthread_tpvr;while(cnt){pthread_t tid;pvr.push_back(tid);char*namenewchar[64];intnsnprintf(name,64,thread-%d,cnt);if(n0)perror(snprintf error);pthread_create(tid,nullptr,RunThread,name);cnt--;}while(true){std::cout唤醒一个线程std::endl;usleep(1000);pthread_cond_signal(_cond);}for(autoe:pvr){pthread_join(e,nullptr);}return0;}4.2 Demo 核心逻辑拆解与原理探究在这段代码中主线程创建了 5 个工作线程它们都在执行RunThread函数。我们可以通过以下三个关键问题来深入理解这段 Demo 的运行机制。4.2.1 为什么一定要先加锁再调用 pthread_cond_wait观察RunThread函数内部逻辑pthread_mutex_lock(_mutex);pthread_cond_wait(_cond,_mutex);判断条件本身就是访问临界资源线程之所以要等待是因为“某种条件不满足”如票数不足或资源未就绪。而要检查条件是否满足就必须读取共享变量ticket。检查共享变量的操作必须在加锁保护的临界区内部进行。休眠必须在临界区内发起当判定条件不满足需要休眠时线程此时已经处于临界区内部手握互斥锁。4.2.2 pthread_cond_wait 内部隐藏的自动解锁机制这就引出了一个严密的逻辑冲突如果线程拿着锁直接挂起休眠了其他线程不就再也无法申请到锁去改变条件了吗这就是为什么pthread_cond_wait必须传入第二个参数_mutex的根本原因当线程执行pthread_cond_wait时操作系统会在将该线程挂入_cond等待队列的同时自动释放传入的互斥锁_mutex线程执行链路申请锁成功进入临界区→ \rightarrow→判定条件不满足→ \rightarrow→调用pthread_cond_wait→ \rightarrow→线程挂入条件变量等待队列→ \rightarrow→自动释放锁→ \rightarrow→允许新线程进入临界区。这样一来其他线程如主线程或其他生产者才能顺利拿到互斥锁去修改共享变量或发出唤醒信号。4.2.3 线程被唤醒后的重新竞争锁在主线程中通过一个死循环定期调用pthread_cond_signal(_cond);运行表现与底层动作有序被唤醒主线程每发出一次pthread_cond_signal就会从_cond等待队列的队头唤醒一个线程。从终端打印输出可以清晰看到各个线程被唤醒的顺序完全遵循入队列的顺序避免了线程饥饿。唤醒后重新竞争锁当被唤醒的线程从pthread_cond_wait函数返回时它并不是直接继续往下执行而是必须在函数内部重新去竞争_mutex锁安全退出临界区只有重新成功抢到锁的线程才会从pthread_cond_wait内部返回出来接着执行后续的ticket--操作并在最后通过pthread_mutex_unlock(_mutex)释放锁。如果将主线程中的pthread_cond_signal替换为广播唤醒pthread_cond_broadcast虽然所有等待线程会在瞬间全部被唤醒但由于重新竞争锁的机制存在它们依然会排队依次获取锁进入后续逻辑绝对不会出现多个线程同时在临界区内修改数据导致的混乱现象。好的本期内容就到这里如果对你有帮助还不要忘记点赞三联支持。我是此方我们下期再见。bye! Linux、C、算法持续连载中欢迎关注WeChat Official Account 【此方的技术栈】。

相关新闻

多模态数据库SurrealDB:简介、实战

多模态数据库SurrealDB:简介、实战

概述 关系型、nosql、向量数据库、嵌入式、 简介 官网,用Rust写的开源(GitHub,32.8K Star,1.3K Fork)多模型数据库,把文档、图、关系、时序、地理空间和向量全塞进同一个引擎里;对外暴露Surr…

2026/10/17 16:37:06 阅读更多 →
Nginx安全配置实战:从基础代理到多层拉黑策略

Nginx安全配置实战:从基础代理到多层拉黑策略

1. 项目概述:从“拉黑”需求看Nginx在前端架构中的核心价值 最近在部署一个基于Nuxt.js的服务端渲染应用时,我遇到了一个挺典型的运维需求:如何在前端服务器层面,高效、精准地拦截恶意请求?这个需求在社区里常被简称为…

2026/10/17 7:16:39 阅读更多 →
金融级AI应用实战:从零构建可复现的信用评分预测服务

金融级AI应用实战:从零构建可复现的信用评分预测服务

在实际金融科技项目中,银行和金融机构对人工智能(AI)的投入早已超越概念验证阶段,进入如何将AI能力安全、合规、高效地融入核心业务流程的深水区。无论是用于风险控制的智能风控模型,还是提升客户体验的智能客服&#…

2026/10/17 14:32:41 阅读更多 →

最新新闻

2026盘点 10 款AI写小说软件:从大纲范例到全篇创作,TaoToken 统一 Key 让多模型切换不再报 401

2026盘点 10 款AI写小说软件:从大纲范例到全篇创作,TaoToken 统一 Key 让多模型切换不再报 401

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

2026/10/17 17:30:24 阅读更多 →
TRAE 2.0 SOLO 实测:从 PRD 到 Vercel 上线,AI 编程全流程自动跑通

TRAE 2.0 SOLO 实测:从 PRD 到 Vercel 上线,AI 编程全流程自动跑通

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

2026/10/17 17:30:24 阅读更多 →
RCNN人脸检测工程实践:候选区域到边界框精修全解析

RCNN人脸检测工程实践:候选区域到边界框精修全解析

人脸检测这块,这几年被各种端到端模型刷屏,但如果你真正上手做过工程化项目,会发现很多所谓“新方案”在落地时并不比经典方法省心。RCNN_人脸检测这个项目,最初就是我在某一次实际业务需求中,想把基于区域的卷积神经网…

2026/10/17 17:30:24 阅读更多 →
Salesforce CRM Manager 岗位全解析:数据质量、权限与集成实战

Salesforce CRM Manager 岗位全解析:数据质量、权限与集成实战

简介:这份PDF面向希望系统了解Salesforce CRM Manager岗位的求职者、招聘方及CRM从业者,围绕该职位的职责边界、汇报关系与核心技能展开,帮助读者判断自身能力匹配度并规划学习路径。内容涵盖数据质量管理与分析、实时报告输出、系统推广与培…

2026/10/17 17:30:24 阅读更多 →
Unity联机游戏开发入门:从架构选型到状态同步Demo实战

Unity联机游戏开发入门:从架构选型到状态同步Demo实战

1. 从单机到联机:为什么第一个Demo总是跑不起来很多人第一次尝试给Unity游戏加联机功能,都会经历一个相似的挫败过程:按照教程装好了网络库,写了几行同步代码,本地测试一切正常,结果一打包发给朋友&#xf…

2026/10/17 17:29:24 阅读更多 →
AI管理体系ISO 42001:从HLS骨架到风险双轨落地

AI管理体系ISO 42001:从HLS骨架到风险双轨落地

简介:ISO/IEC DIS 42001:2022《信息技术 人工智能 管理体系》是国际标准化组织与国际电工委员会联合发布的人工智能管理体系国际标准草案,面向AI治理、合规与标准化从业者,也适合企业AI项目负责人及高校相关专业师生。草案围绕组织责任、风险…

2026/10/17 17:29:24 阅读更多 →

日新闻

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

DeepSeek选型与部署:V3/R1、Ollama、API全攻略

简介:《DeepSeek使用方法.pdf》是一份面向DeepSeek入门及进阶用户的完整使用指南,重点围绕V3与R1两款模型的能力差异与适用场景展开,帮助读者根据任务类型与成本选择合适的模型。资源共1个PDF文件,压缩包大小约311KB,篇…

2026/10/17 0:00:25 阅读更多 →
200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

200+Deepseek润色指令拆解:从底层原理到场景落地的完整指南

简介:这是一份聚焦DeepSeek提示词优化的《200Deepseek润色指令》PDF文档,共收录两百余条可直接套用的润色指令,涵盖语法修正、风格统一、逻辑梳理、表达精炼等维度,并针对商务写作、学术论文、广告文案等场景给出差异化建议。文档…

2026/10/17 0:00:25 阅读更多 →
OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

OpenCV轮廓法物体计数系统:轻量、可控、可落地的工业级实现

简介:本资源是一份基于OpenCV的本科课程设计完整报告,面向计算机视觉初学者、图像处理学习者及高校通信/信息类专业学生,解决物体智能计数与检测结果结构化记录的实际问题。项目覆盖图像预处理、轮廓检测、边界框绘制、轨迹栏交互式参数调节、…

2026/10/17 0:00:25 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/17 1:44:59 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/17 17:22:31 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →