
1. 从先进先出说起queue到底解决什么问题很多刚接触C STL的同学第一次看到queue的时候都会觉得这不就是一个包装好的链表或者数组吗我自己也能写。话是没错但问题在于——你自己写的那个大概率没有STL版本严谨也没有它考虑得周全。queue也就是队列解决的是**数据先进先出FIFOFirst In First Out**的问题。说得直白一点你往队列里放数据的顺序就是你将来取数据的顺序。先放进去的先出来后放进去的后出来。现实生活中到处都是这种模型。比如食堂打饭排队先到的先打到饭再比如打印机任务列表你先提交的文档先被打出来还有电影院取票机排在最前面的那个人最先拿到票。这些场景有一个共同特征次序很重要而且不能乱。在C STL里queue是一个容器适配器container adapter注意这个词——适配器。它不像vector、deque那样自己管理内存、存储数据而是在底层包装了另一个容器然后对外只暴露队头出、队尾进这些特定操作。底层具体用哪个容器干活你可以自己指定默认是deque。这就引出了queue最核心的价值你不需要关心数据怎么存的只需要关心数据的先后顺序。它替你挡掉了底层实现细节把先进先出这个语义做得干净利落。那queue能用在哪里实际开发中非常常见任务调度多个任务按提交顺序执行先提交的先处理消息队列的简化版本生产者往队尾丢消息消费者从队头拿消息BFS广度优先搜索图论算法最经典的配套数据结构比如迷宫最短路径、社交网络好友推荐缓冲队列例如网络请求的排队处理、日志的异步写入如果你正在做一个跟顺序处理相关的功能queue大概率是一个很顺手的工具。接下来我会从基本用法到内部原理、从性能分析到实际踩坑把这个组件一次讲透。2. 基础用法扫盲构造、入队、出队、遍历一个不落2.1 需要包含的头文件#include queue就这么一句其它什么都不用。queue不是独立实现的容器而是基于其它容器包装出来的所以头文件路径也很轻量。2.2 创建一个队列STL的queue有几种构造方式最常用的是默认构造#include iostream #include queue int main() { std::queueint q; // 默认底层容器是 deque std::queueint, std::dequeint q2; // 显式指定底层容器 std::queueint, std::listint q3; // 用 list 做底层 return 0; }第二种写法其实和第一种完全等价因为deque就是默认值。第三种写法则指定了std::list作为底层容器这在某些特定场景下会用到后面讲性能对比时我会细说。2.3 核心操作速览queue的接口非常少我列出最常用的几个push(x)向队尾添加一个元素pop()移除队头元素注意没有返回值front()返回队头元素的引用back()返回队尾元素的引用empty()判断队列是否为空size()返回队列中的元素个数写一个完整的例子#include iostream #include queue int main() { std::queueint q; q.push(10); q.push(20); q.push(30); std::cout 队列大小: q.size() std::endl; // 3 std::cout 队头元素: q.front() std::endl; // 10 std::cout 队尾元素: q.back() std::endl; // 30 q.pop(); // 移除队头的 10 std::cout 弹出一次后队头: q.front() std::endl; // 20 return 0; }2.4 两个容易踩坑的点第一个坑pop()不返回被删除的元素。这一点跟front()和back()配合使用才行。如果你需要取走队头元素正确姿势是先front()拿到值再pop()而不是指望pop把元素返回给你。早期Java的Deque接口里pop()抛异常而不返回元素也是同一个道理设计上为了保证效率省去了拷贝返回值的开销。第二个坑front()和pop()之前一定要检查empty()。对一个空队列调用front()或pop()是未定义行为代码可能在某些环境直接崩溃也可能静默读到了脏数据。规范的写法应该是if (!q.empty()) { int val q.front(); q.pop(); // 处理 val }2.5 遍历的两种姿势queue本身不支持随机访问也没有迭代器可以for循环遍历这一点跟vector、map很不一样。你只有两种常规办法方法一边弹边访最推荐的临时遍历方式#include iostream #include queue int main() { std::queueint q; for (int i 0; i 5; i) q.push(i * 10); while (!q.empty()) { std::cout q.front() ; q.pop(); // 原队列会被清空 } // 输出: 0 10 20 30 40 return 0; }这个方法简单直接但会破坏原队列。如果只是想遍历看看不想改动原数据就用第二种。方法二拷贝一份再遍历std::queueint copy q; while (!copy.empty()) { std::cout copy.front() ; copy.pop(); }注意拷贝有开销。如果队列里的元素是巨大的自定义对象拷贝整个队列的成本不可忽视。这种情况下就需要权衡了要么接受破坏原队列要么换用std::deque这种支持迭代器的容器。这个选择在你做真实项目时经常遇到我后面也会再展开讲。3. queue的底层机制deque、list与适配器设计的取舍3.1 为什么queue默认的底层容器是deque刚接触STL的人经常会问queue底层为什么不是vectorvector明明也很常见。这是个好问题。vector的致命弱点在于队头操作向vector头部插入或删除元素是O(n)的时间复杂度因为所有后续元素都要整体迁移。而deque双端队列的设计目标之一就是同时支持头尾两端的高效插入删除。它内部由一段一段连续缓冲区拼接而成具体细节各家实现有所差异但整体思路类似无论从头部还是尾部操作平摊下来都是O(1)复杂度。queue只允许队尾入、队头出这两个操作对deque来说都是最擅长的所以默认用它再合适不过。3.2 什么时候该换用list做底层你可以在模板参数里把底层容器换成std::liststd::queueint, std::listint q;什么时候值得这么做一个典型的场景是你的元素经常需要在队列中间做操作或者你需要频繁进行元素的删除与插入注意标准queue不允许中间操作但如果你把它只是当作一个按序管理数据的容器偶尔也会有这种需求这时可以借助底层容器的接口。另一个场景是元素很大。deque本质上还是连续内存的分段组合如果元素都是动辄几百字节的大结构体每次push时都要整体拷贝/移动元素C11以后移动语义会缓解一部分问题但根本拷贝仍可能发生。list是链式存储插入只动指针。代价是list访问元素的缓存命中率差因为节点散落在内存各处。我的建议绝大多数普通数据类型int、double、普通struct直接用默认deque就对了没事别换list。当你发现队列元素很大且有性能问题时可以考虑换list并做性能对比。盲目换底层容器不如优化数据本身的结构。3.3 适配器模式的设计思想queue的本质是适配器也就是包装你但暴露更简单的接口给你。这个设计想法很巧妙生产环境里我们经常只关心先进先出这个语义而不想被底层各种容器的接口干扰。用queue你拿到的是一个语义清晰的最小接口集不需要背vector十几个方法更不容易误用。同样套路的还有std::stack栈和std::priority_queue优先队列它们都是在别的容器上罩了一层壳。三个适配器一对比就很清晰适配器默认底层容器核心语义queuedeque先进先出stackdeque后进先出priority_queuevector按优先级出队注意priority_queue默认底层是vector因为它需要对元素建堆deque的分段结构反而不适合堆的连续索引需求。这也再次说明STL的每一种默认选择背后都有数据结构和性能的考量不是随便选的。3.4 queue内部结构的技术示意为了让你看得更透我画个逻辑图不是mermaid帮大家理解队尾(back) 队头(front) ↓ ↓ ------------------------ | e6 | e5 | e4 | e3 | e2 | e1 | ------------------------ ↑ ↑ push(e7)会从这边进 pop()会从这边出数据从队尾进从队头出。放进去的早晚顺序决定了取出来的早晚顺序。就是这么朴素但朴素不等于简单因为在多线程、异步、高并发场景中这个模型支撑起了大量复杂系统的骨架。4. queue在实战中的典型应用从BFS到任务调度4.1 广度优先搜索BFS队列最经典的算法应用就是BFS。当你需要在一个图或者网格中逐层向外扩展搜索时queue是天然匹配的。我举个经典的迷宫最短路径例子。假设给定一个N*M网格0表示可通行1表示墙壁起点在(0,0)终点在(N-1,M-1)求最短步数。#include iostream #include queue #include vector using namespace std; int bfs(const vectorvectorint grid) { int n grid.size(); int m grid[0].size(); vectorvectorint dist(n, vectorint(m, -1)); // -1 表示未访问 queuepairint, int q; dist[0][0] 0; q.push({0, 0}); int dx[] {-1, 1, 0, 0}; int dy[] {0, 0, -1, 1}; while (!q.empty()) { auto [x, y] q.front(); q.pop(); if (x n - 1 y m - 1) { return dist[x][y]; } for (int k 0; k 4; k) { int nx x dx[k]; int ny y dy[k]; if (nx 0 nx n ny 0 ny m grid[nx][ny] 0 dist[nx][ny] -1) { dist[nx][ny] dist[x][y] 1; q.push({nx, ny}); } } } return -1; // 无法到达 }这段代码里queue是BFS的核心载体。每一层距离为d的位置都先入队然后依次处理再入队下一层距离为d1的位置。队列天然保证了按层扩展的顺序这正好是BFS的精髓。4.2 任务队列与生产者-消费者模型在实际后台服务里最常见的队列用法是任务缓冲多个生产者往队列中添加任务多个消费者从队列中取出并执行任务。生产者的速度和消费者的速度经常不一致中间用一个队列缓冲就能解耦。一个极简的伪代码思路如下std::queuestd::functionvoid() task_queue; // 加锁保护队列的读写真实项目里至少用互斥锁复杂点用条件变量 // 生产者线程 task_queue.push([]{ std::cout task1\n; }); // 消费者线程 while (running) { std::unique_lockstd::mutex lock(mtx); if (!task_queue.empty()) { auto task task_queue.front(); task_queue.pop(); lock.unlock(); task(); // 在锁外执行任务缩短持锁时间 } }这里有三个细节值得注意必须加锁STL的queue不是线程安全的多个线程同时对它push和pop会造成数据竞争属于未定义行为。尽量缩短临界区取出任务后就释放锁再去执行任务。如果拿着锁执行任务可能导致一个耗时的任务阻塞所有消费者的取任务动作性能会明显拖垮。用std::move优化如果任务对象很大入队时可以用q.push(std::move(task))移动语义减少拷贝开销。4.3 滑动窗口类问题很多数组、字符串算法题里滑动窗口也经常借助queue来实现。比如给定一个数组求每个长度为k的连续子数组的最大值。经典的解法之一是单调队列即维护一个从队头到队尾递减的deque这里用deque而不是queue因为要从队尾也能弹出元素。queue的变形应用这里简单提一句如果以后你在刷题或面试中遇到滑动窗口最值这类题往队列上想往往是解法的起点。4.4 消息队列的原型启蒙很多做后台开发的同学学习了消息队列中间件比如Kafka、RabbitMQ的使用但这些中间件底层到底在干嘛其实核心思想就是一个更大、更可靠、可跨机器的队列生产端往队列放消息消费端从队列取消息中间多了持久化、分区、高可用等机制。先把单机的queue玩明白再去理解分布式消息队列会顺手很多。因为你已经掌握了队列的数据流模型剩下的就是学怎么在网络和磁盘条件下把它做大做强。5. 性能分析与优化queue并不是魔法5.1 deque的随机内存块结构很多人以为deque是双端都能高效增删的vector这个理解方向是对的但细节上与vector差别很大。vector是单一连续内存而deque是由多个固定大小的buffer块组成的。索引访问的时候它需要计算元素落在哪一块、偏移量是多少一次访问要多算一步除法/取模。因此在纯随机访问场景下deque比vector慢一些。不过queue本身不允许随机访问所以这个劣势在queue这个语境下几乎无关紧要。deque真正厉害的地方在于头尾增删都是平摊O(1)而且增加元素时不需要像vector那样频繁整体搬迁。对于队列频繁push和pop的典型场景deque是接近最优的选择。5.2 pop的开销到底有多大很多做C开发的同学都会忽略一个细节当queue里存放的是复杂对象时pop()会销毁队头对象这会调用析构函数。如果析构函数做了较重的清理工作比如释放网络连接、关闭文件队列操作的性能就会明显下降。反过来如果你为了减少拷贝而一直存储指针或智能指针std::shared_ptr那么pop()销毁的是指针而不会销毁对象本身。这时候队列实际上只管理了对象的引用。这种模式在真实系统里非常常见但也带来了生命周期管理的复杂度当对象在队列中被pop掉如果没有其它地方持引用就应该安全释放。一个常见的错误是从队列取出对象后忘记对空指针做判断导致崩溃。安全起见存指针时尽量用智能指针。5.3 一个对比实验deque队列 vs list队列我写了一个小测试来对比std::queueint, std::dequeint和std::queueint, std::listint批量push 100万个整数的表现在我的常用测试环境下deque明显更快差距通常在几十毫秒级别换成复杂对象后差距方向可能相反。这里不贴具体数字了因为机器不同、编译选项不同数字意义不大。关键结论是小元素、高频push/popdeque稳赢因为缓存友好性更好大元素、低频操作list可能更好因为它避免了连续内存上的拷贝迁移没有通用最优解实践是检验的唯一标准5.4 queue的C11移动语义优化C11之后你可以给queue的push传入右值例如q.push(std::move(obj))。如果你在queue里存的是大对象比如一个包含大量字符串的std::vector成员移动构造能把拷贝开销降为基本为零。一个简单的经验std::queuestd::vectorint q; std::vectorint big(100000, 42); q.push(std::move(big)); // 避免一次十万个int的拷贝 // 此时 big 处于未指定但有效的状态一般不要再使用这个优化的收益很大值得写进代码规范里凡是向容器里push大对象优先考虑移动语义不要默认传左值。5.5 emplace构造一个入队一个除了pushqueue在C11之后还支持emplace。它的意思是直接在队列内部就地构造对象省去了临时对象构造和拷贝/移动的开销。std::queuestd::pairint, int q; q.emplace(3, 4); // 等价于 q.push(std::make_pair(3, 4));emplace的底层实现通常比push(std::move(...))再少一步因为连临时对象都不需要构造但差别通常不大。代码风格上如果想表达这个对象就应该属于队列用emplace很自然如果想表达我已经有这个对象入库一份用push配合移动语义更清晰。6. 几个绕不开的典型坑empty判断、swap、容器选择6.1 空队列的front/pop害人无数我之前提到过一次这里再展开说一下。std::queue的front()和pop()在队列为空时是未定义行为。某些标准库实现下front()可能返回一个默认构造的临时值pop()可能直接崩溃。这完全取决于具体标准库实现所以绝对不能赌。我见过非常典型的线上bug一个后台服务消费任务队列由于某个异常导致队列被清空但消费线程没检查empty()直接front()拿到一个伪值然后继续执行结果触发了一大串空指针和逻辑错乱。后来排查时发现根因就是少写了一个if (!q.empty())。做事规范while (!q.empty()) { T task std::move(q.front()); // 取出 q.pop(); // 移除 process(task); // 处理 }这一个习惯能让你避开这辈子大量的crash。6.2 queue支持swap操作queue支持swap成员函数可以高效地交换两个队列的内容。注意绝大多数STL容器的swap是O(1)的交换内部指针/缓冲区并不是把元素逐一搬移。std::queueint a; std::queueint b; a.push(1); a.push(2); b.push(99); std::cout a.front() b.front(); // 1 99 a.swap(b); std::cout a.front() b.front(); // 99 1这在某些场景下很有用。例如当你有一个待处理队列处理一段时间后想清空并换一批任务可以用swap把当前队列和一个临时空队列交换然后旧队列自动释放。好处是比逐个pop()更高效语义也更干净。6.3 queue和deque的选择何时升级很多程序员从学习C起就习惯queue一直用到真实项目但有些场景下queue并不是最好用的那个。queue有一个天然的局限不能从中间删除元素不能随机访问。如果业务需求开始变复杂比如我需要按下标查一下第k个任务我需要删除中间某个任务queue的接口就不够用了。此时有两个选择继续用queue但自己维护额外的索引结构代价略高直接换成std::deque用它提供更丰富的接口并在需要时只按照队列的语义去使用它我的判断标准如果先进先出确实是当前业务的主模型queue更清晰、更不容易出错如果业务已有其它非队列操作就别硬套queue了deque能给你更大的灵活性。工程做久了你会发现清晰度和灵活度之间需要做权衡而不是盲目追崇某个容器。6.4 不要把queue当栈用有些同学没有注意语义会用queue来实现后进先出的操作结果逻辑一团乱。queue是FIFOstack是LIFO两者刚好相反。如果确实需要后进先出请用std::stack。虽然底层都是容器适配器但接口和语义完全不同。用错容器代码读起来很痛苦。7. 从queue到priority_queue优先队列的对比与选择7.1 优先队列的基本语义std::priority_queue是queue家族的另一个重要成员但它不是先进先出而是每次出队的是优先级最高或最低的元素。底层实现通常基于堆默认是大顶堆插入和删除都是O(log n)复杂度。#include iostream #include queue #include vector int main() { std::priority_queueint pq; // 默认大顶堆最大值优先 pq.push(10); pq.push(5); pq.push(20); while (!pq.empty()) { std::cout pq.top() ; // 注意是top()不是front() pq.pop(); } // 输出: 20 10 5 return 0; }7.2 什么时候用priority_queue优先级队列的典型场景是任务按紧急程度/权重出队比如操作系统的进程调度优先级高的先执行、合并K个有序链表每次取最小元素、Dijkstra最短路径算法。对比普通queue容器出队顺序插入复杂度弹出复杂度典型场景queue按入队顺序O(1)O(1)按序消费、BFSpriority_queue按优先级O(log n)O(log n)Dijkstra、任务调度如果数据本身就带着优先级属性就应该用priority_queue而不是自己在queue外面排序或者遍历找最大值。我们常说选择合适的数据结构是性能的一半这个例子体现得特别明显。7.3 自定义比较规则的priority_queue想知道priority_queue怎么自定义优先级用第三个模板参数传一个比较器。比如想实现小顶堆最小值优先#include queue #include vector #include iostream int main() { std::priority_queueint, std::vectorint, std::greaterint pq; pq.push(10); pq.push(5); pq.push(20); while (!pq.empty()) { std::cout pq.top() ; // 5 10 20 pq.pop(); } return 0; }再说一个更复杂的存的是自定义结构体按某个字段比较。struct Task { int priority; std::string description; }; struct TaskCompare { bool operator()(const Task a, const Task b) const { return a.priority b.priority; // 大顶堆priority 大的先出 } }; std::priority_queueTask, std::vectorTask, TaskCompare task_pq;这里容易出错的地方是比较器的方向。priority_queue默认是大顶堆所以比较器里写成谁更小返回true会让小的沉底大的浮上来后先出队。如果写反了整个优先级逻辑就颠倒了。7.4 从queue到priority_queue的迁移建议如果你项目中已经有一个queue突然发现每个任务多了一个优先级字段并且需要按优先级出队那么是时候迁移到priority_queue了。迁移成本很低接口从front()换成top()其余基本不变。但要注意priority_queue不支持遍历方式也没有front/back只有top能看队首也没有迭代器。这些限制就是为堆结构付出的代价——任何数据结构都不可能面面俱到。8. 多线程环境下的queue锁、条件变量与无锁设计8.1 标准queue不是线程安全的这一点前面已经提过STL所有容器默认都不是线程安全的queue也不例外。多线程并发push/pop必须在外部加锁。最简单的做法#include mutex #include queue std::queueint q; std::mutex q_mutex; void producer(int val) { std::lock_guardstd::mutex lock(q_mutex); q.push(val); } bool consumer(int out) { std::lock_guardstd::mutex lock(q_mutex); if (q.empty()) return false; out q.front(); q.pop(); return true; }8.2 条件变量让消费者等待上面的消费者若发现队列为空直接返回false。如果生产者还没产生数据消费者就会疯狂空转白白消耗CPU。更合理的做法是使用条件变量让消费者在空队列上睡眠等生产者push后再唤醒。#include condition_variable #include mutex #include queue std::queueint q; std::mutex q_mutex; std::condition_variable q_cv; // 生产者 void producer(int val) { { std::lock_guardstd::mutex lock(q_mutex); q.push(val); } q_cv.notify_one(); // must be in block and after unlock or before? } // 消费者 int consumer() { std::unique_lockstd::mutex lock(q_mutex); q_cv.wait(lock, [] { return !q.empty(); }); // 队列非空才继续 int val q.front(); q.pop(); return val; }这段代码里最重要的是wait那一行。std::condition_variable::wait会先释放锁让生产者可以拿到锁push等被唤醒后它会重新获取锁并且检查lambda谓词是否满足。如果满足继续往下走如果不满足继续等待。这个机制避免了假唤醒的问题操作系统可能因为各种原因唤醒等待线程但队列仍然为空是生产环境里一个很必要的细节。8.3 锁-free设计的粗浅讨论更高阶的方案是无锁队列。大体思路是用原子操作atomic实现多生产者/多消费者场景下的push和pop避免线程切换和锁竞争带来的开销。C标准库没有直接提供无锁队列实现需要借助第三方库或者自研。但在绝大多数业务场景里常规的mutex加条件变量已经够用了。真正到了需要无锁的时候通常意味着队列已经成为系统核心瓶颈而且此时的数据结构和内存管理复杂度会飙升不建议初学者直接上手。我的建议先用带锁的queue跑通业务用性能分析工具测压力如果确实锁竞争成为瓶颈再考虑无锁化或者用无锁队列库。过早优化是万恶之源这在队列场景里表现得尤为明显。8.4 一个小型任务系统的完整实现思路把前面的碎片拼起来一个基于queue的简单任务系统可以是这样的一个全局的任务队列存放std::functionvoid()多个工作线程每线程循环从队列取任务执行一个关闭标志当系统需要优雅退出时生产不再push消费者处理完剩余任务后退出这个系统在C后台开发中几乎是必备基本功。它麻雀虽小五脏俱全锁、条件变量、队列、线程生命周期管理全部包含在内。强烈建议每个学C的同学自己动手实现一遍胜过看十篇文章。9. 一些真实项目的输出格式与常见问题汇总在整理这篇文章的工程经验时我想到几个高频疑问。这里统一回应一下。9.1 queue为什么不提供clear或取队尾pop标准queue刻意保持极简接口只暴露队尾进、队头出的操作。这是设计哲学功能越单一语义越清晰越不容易用错。如果你确实需要清空队列可以while(!q.empty()) q.pop();或者直接q std::queueT();重新赋值。如果要删除某个特定位置元素那就不要用queue换deque或list。9.2 能不能用queue存引用不能。容器元素必须是可拷贝/可移动的类型引用不可拷贝也不可移动所以std::queueint不能编译。如果你确实需要存引用语义用std::reference_wrapper包一层或者干脆存指针。存指针时记得用智能指针管理生命周期。9.3 queue的size返回的是什么类型size()返回size_type本质是一个无符号整数通常是std::size_t。需要注意不要拿有符号数和它比较而产生符号转换的坑比如q.size() -1永远为true因为-1会被转换成很大的无符号数。这一条在编码规范里值得单独列一行。9.4 在C11之前和之后的差异C11为queue引入了移动构造、移动赋值、emplace等特性。如果项目还在用老标准就少了一些便利。当前绝大多数编译环境已默认支持C17/20可以放心使用新特性。但老项目上如果版本受限写代码前先确认标准版本尤其是跨平台交接的项目。9.5 允许的底层容器到底有哪些标准规定queue的底层容器必须支持front()、back()、push_back()、pop_front()这四个操作。默认用dequelist是常见替代vector不行它没有pop_front。自己手写一个满足要求的容器也可以塞进模板参数不过这属于进阶玩法日常工作很少需要。10. 一个完整的工程示例模拟银行排队叫号系统我最后再分享一个综合例子把前面所有知识点串起来。假设我们要做一个简单的银行排队叫号系统多个客户到达按到达顺序排号柜台叫号处理。参考实现如下。#include iostream #include queue #include string struct Customer { int id; std::string name; int priority; // 0-普通1-优先2-贵宾 }; int main() { std::queueCustomer normal_queue; // 实际项目中可能用 priority_queue 做分级排队这里先用 queue 演示 normal_queue.push({1, 小王, 0}); normal_queue.push({2, 小李, 1}); normal_queue.push({3, 小张, 0}); std::cout 当前排队人数: normal_queue.size() std::endl; while (!normal_queue.empty()) { Customer c normal_queue.front(); normal_queue.pop(); std::cout 请 c.id 号客户 c.name 到柜台办理 std::endl; } std::cout 所有客户办理完毕 std::endl; return 0; }输出结果基本符合预期按到达顺序叫号。如果业务变成贵宾优先这个普通queue就撑不住了需要换成priority_queue并且自定义比较逻辑。这样一个简单的演变正好说明了一开始选用queue是对的业务复杂后再升级到priority_queue也是顺理成章的这就是为什么要把基础容器的语义和使用边界彻底吃透。我在实际工作中体会最深的一点是队列相关的bug绝大多数不是因为不懂得API而是因为对先进先出这个语义的使用场景不够敏感。多线程环境忘记加锁、pop前没检查空队列、把FIFO当LIFO用这些都是理念问题不是语法问题。希望这篇文章不只是让你记住几个接口而是帮你建立什么时候该用队列、底层发生了什么、会遇到什么坑的完整认知。等你需要在真实项目里做任务调度、BFS、缓冲处理时能自然而然想到queue知道它行也知道它不行那这篇分享就算真正帮到你了。