FreeRTOS队列

news2025/2/26 9:53:17

队列简介

队列是一种任务到任务,任务到中断,中断到任务数据交流得一种机制。在队列中可以存储数量有限,大小固定得多个数据,队列中的每一个数据叫做队列项目,队列能够存储队列项目的最大数量称为队列的长度,在创建队列的时候就需要指定所创建的队列的长度以及队列项目的大小。因为队列是用来在任务与任务或任务与中断之间传递消息的一种机制,因此队列也叫做消息队列

数据存储

队列通常采用FIFO(先进先出)的存储缓冲机制,当有新的数据被写入队列中时,永远都是写入到队列的尾部,而从队列中读取数据时,永远都是读取队列的头部数据。但同时FreeRTOS的队列也支持将数据写入到队列的头部,并且可以指定是否覆盖先前已经在队列头部的数据(对应3种写入数据的方式)。

多任务访问

队列不属于某个特定的任务,可以在任何的任务或中断种往队列中写入消息,或者从队列中读取消息。

队列读取阻塞

在任务从队列读取消息时,可以指定一个阻塞超时时间。如果任务在读取队列时,队列为空,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列中有可用消息。当有其他任务或中断将消息写入队列中,因等待队列而阻塞任务将会被添加到就绪态任务列表中,并读取队列中可用的消息。如果任务因等待队列而阻塞超时时间超过指定的阻塞超时时间,那么任务也将自动被转移到就绪态任务列表中,但不再读取队列中的数据。

因为同一个队列可以被多个任务读取,因此可能会有多个任务因等待同一个队列,而被阻塞,在这种情况下,如果队列中有可用的消息,那么也只有一个任务会被解除阻塞并读取到消息,并且按照阻塞的先后和任务的优先级,决定应该解除哪一个队列读取阻塞任务。

队列写入阻塞

与队列读取一样,在任务往队列写消息时,也可以指定一个阻塞超时时间。如果任务在写入队列时,队列已经满了,这时任务将被根据指定的阻塞超时时间添加到阻塞态任务列表中进行阻塞,以等待队列有空闲的位置可以写入消息。指定的阻塞超时时间为任务阻塞的最大时间,如果在阻塞超时时间到达之前,队列有空闲的位置,那么队列写入阻塞任务将会解除阻塞,并往队列中写入消息,如果达到指定的阻塞超时时间,队列依旧没有空闲的位置写入消息,那么队列写入阻塞任务将会自动转移到就绪态任务列表上,但不会往队列中写入消息。

因为队列可以被多个任务写入,因此可能会有多个任务因等待同一个任务,而被阻塞,在这种情况下,如果队列中有空闲的位置,那么也只有一个任务会被解除阻塞态并往队列中写入消息,并且会按照阻塞的先后和任务的优先级,决定应该解除哪一个队列写入阻塞任务。

队列的特性

常规特性

队列的简化操作如下图所示,从图中可以知道:

  • 队列可以包含若干个数据,队列中有若干项,这被称为“长度(length)”

  • 每个数据大小固定

  • 创建队列时就要指定长度,数据大小

  • 数据的操作采用先进先出的方法(FIFO,First In First Out);写数据时放到尾部,读数据时从头部读

  • 也可以强制写队列头部,覆盖头部数据

更详细的操作如下图所示:

传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里

  • 引用:把数据,把变量的地址复制进队列里

FreeRTOS传输小数据的时候,可以采用值传递,传输大数据时采用引用的方式

  • 局部变量的值可以发送到队列里,后续即使函数退出,局部变量被回收,也不会影响队列中的数据

  • 无需分配buffer来保存数据,队列中有buffer

  • 局部变量马上可以再次使用

  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据

  • 如果数据实在太大,你还是可以使用队列传输它的地址

  • 队列的空间有FreeRTOS内核分配,无需任务操心

  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。

队列的阻塞访问

只要知道队列的句柄,谁都可以读,写该队列。任务,ISR都可读,写队列。可以多个任务读写队列。

任务读写队列时,简单地说:如果读写不成功,则阻塞,可以指定超时时间。口语化的说,就是说可以定个闹钟;如果能读写了就马上进入就绪态,否则就阻塞到超时。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞态;还可以指定阻塞时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取 队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞态,有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务

  • 如果大家优先级相同,那等待时间最久的任务会进入就绪态

跟读队列时类似,一个任务要写队列时,如果队列满了,该任务可以进入阻塞态;还可以指定阻塞的时间,如果队列有空间了,则该阻塞的任务就会变为就绪态。如果一直都没有空间,则时间到之后它会进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写“满队列”时,这些任务都会进入阻塞态,有多个任务在等待同一个队列的空间,当队列中有空间时,哪个任务会进入就绪态?

  • 优先级最高的任务

  • 如果大家优先级相同,那等待时间最久的任务会进入就绪态

队列结构体

typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */
{
    int8_t * pcHead;           /*< 存储区域的起始地址 */
    int8_t * pcWriteTo;        /*< 下一个写入的位置 */

    union
    {
        QueuePointers_t xQueue;     /*< Data required exclusively when this structure is used as a queue. */
        SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */
    } u;

    List_t xTasksWaitingToSend;             /*< 等待发送列表 */
    List_t xTasksWaitingToReceive;          /*< 等待接收列表 */

    volatile UBaseType_t uxMessagesWaiting; /*< 非空闲队列项目的数量/
    UBaseType_t uxLength;                   /*< 队列长度 */
    UBaseType_t uxItemSize;                 /*< 队列项目的大小 */

    volatile int8_t cRxLock;                /*< 读取上锁计数器*/
    volatile int8_t cTxLock;                /*< 写入上锁计数器 */

    #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
        uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */
    #endif

    #if ( configUSE_QUEUE_SETS == 1 )
        struct QueueDefinition * pxQueueSetContainer;
    #endif

    #if ( configUSE_TRACE_FACILITY == 1 )
        UBaseType_t uxQueueNumber;
        uint8_t ucQueueType;
    #endif
} xQUEUE;

/* The old xQUEUE name is maintained above then typedefed to the new Queue_t
 * name below to enable the use of older kernel aware debuggers. */
typedef xQUEUE Queue_t;

队列相关的API函数

使用队列的流程:创建队列,写队列,读队列、删除队列

创建

队列的创建有两种方法:动态内存分配,静态内存分配

  • 动态内存分配:xQueueCreate,队列的内存在函数内部动态分配

函数原型如下:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );

参数

说明

uxQueueLength

队列长度,最多能存放多少个数据(item)

uxItemSize

每个数据(item)的大小:以字节为单位

返回值

非0:成功,返回句柄,以后使用句柄来操作队列

NULL:失败,内存不足

  • 静态内存分配:xQueueCreateStatic,队列的内存要实现分配好

函数原型如下:

QueueHandle_t xQueueCreateStatic(
                                   UBaseType_t uxQueueLength,
                                   UBaseType_t uxItemSize,
                                  uint8_t *pucQueueStorage,
                                  StaticQueue_t *pxQueueBuffer
                              );

参数

说明

uxQueueLength

队列长度,最多能存放多少个数据(item)

uxItemSize

每个数据(item)的大小:字节为单位

pucQueueStorag

如果uxItemSize非0,pucQueueStorageBuffer必须指向一个uint8_t数组,

此数组大小至少为"uxQueueLength * uxItemSize"

pxQueueBuffer

必须执行一个StaticQueue_t结构体,用来保存队列的数据结构

返回值

非0:成功,返回句柄,以后使用句柄来操作队列

NULL:失败,因为pxQueueBuffer为NULL

示例代码:

// 示例代码
 #define QUEUE_LENGTH 10
 #define ITEM_SIZE sizeof( uint32_t )
 
 // xQueueBuffer用来保存队列结构体
 StaticQueue_t xQueueBuffer;
 
 // ucQueueStorage 用来保存队列的数据
 // 大小为:队列长度 * 数据大小
 uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];
 
 void vATask( void *pvParameters )
 {
    QueueHandle_t xQueue1;
 
    // 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
    xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
                          ITEM_SIZE,
                          ucQueueStorage,
                          &xQueueBuffer ); 
 }

复位

队列刚刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()把队列恢复为初始状态,此函数原型为:

* pxQueue : 复位哪个队列;
 * 返回值: pdPASS(必定成功)
 */
BaseType_t xQueueReset( QueueHandle_t pxQueue);

删除

删除队列的函数为vQueueDelete(),只能删除使用动态方法创建的队列,它会释放内存。原型如下:

void vQueueDelete( QueueHandle_t xQueue );

写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

/* 等同于xQueueSendToBack
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSend(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );

/* 
 * 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToBack(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );


/* 
 * 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
 */
BaseType_t xQueueSendToBackFromISR(
                                      QueueHandle_t xQueue,
                                      const void *pvItemToQueue,
                                      BaseType_t *pxHigherPriorityTaskWoken
                                   );

/* 
 * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
 */
BaseType_t xQueueSendToFront(
                                QueueHandle_t    xQueue,
                                const void       *pvItemToQueue,
                                TickType_t       xTicksToWait
                            );

/* 
 * 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
 */
BaseType_t xQueueSendToFrontFromISR(
                                      QueueHandle_t xQueue,
                                      const void *pvItemToQueue,
                                      BaseType_t *pxHigherPriorityTaskWoken
                                   );

这些函数用到的参数是类似的,统一说明如下:

参数

说明

xQueue

队列句柄,要写哪个队列

pvItemToQueue

数据指针,这个数据的值会被复制进队列,

复制多大的数据?在创建队列时已经指定了数据大小

xTicksToWait

如果队列满则无法写入新数据,可以让任务进入阻塞状态,

xTicksToWait表示阻塞的最大时间(Tick Count)。

如果被设为0,无法写入数据时函数会立刻返回;

如果被设为portMAX_DELAY,则会一直阻塞直到有空间可写

返回值

pdPASS:数据成功写入了队列

errQUEUE_FULL:写入失败,因为队列满了

读队列

使用xQueueReceive()函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版本:在任务中使用、在ISR中使用。函数原型如下:

BaseType_t xQueueReceive( QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait );

BaseType_t xQueueReceiveFromISR(
                                    QueueHandle_t    xQueue,
                                    void             *pvBuffer,
                                    BaseType_t       *pxTaskWoken
                                );

参数说明如下:

参数

说明

xQueue

队列句柄,要读哪个队列

pvBuffer

bufer指针,队列的数据会被复制到这个buffer

复制多大的数据?在创建队列时已经指定了数据大小

xTicksToWait

果队列空则无法读出数据,可以让任务进入阻塞状态,

xTicksToWait表示阻塞的最大时间(Tick Count)。

如果被设为0,无法读出数据时函数会立刻返回;

如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写

返回值

pdPASS:从队列读出数据入

errQUEUE_EMPTY:读取失败,因为队列空了。

查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

/*
 * 返回队列中可用数据的个数
 */
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );

/*
 * 返回队列中可用空间的个数
 */
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

覆盖/偷看

当队列长度为1时,可以使用xQueueOverwrite()xQueueOverwriteFromISR()来覆盖数据。 注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也以意味着这些函数不会被阻塞。 函数原型如下:

/* 覆盖队列
 * xQueue: 写哪个队列
 * pvItemToQueue: 数据地址
 * 返回值: pdTRUE表示成功, pdFALSE表示失败
 */
BaseType_t xQueueOverwrite(
                           QueueHandle_t xQueue,
                           const void * pvItemToQueue
                      );

BaseType_t xQueueOverwriteFromISR(
                           QueueHandle_t xQueue,
                           const void * pvItemToQueue,
                           BaseType_t *pxHigherPriorityTaskWoken
                      );

如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是xQueuePeek()xQueuePeekFromISR()。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。 函数原型如下:

/* 偷看队列
 * xQueue: 偷看哪个队列
 * pvItemToQueue: 数据地址, 用来保存复制出来的数据
 * xTicksToWait: 没有数据的话阻塞一会
 * 返回值: pdTRUE表示成功, pdFALSE表示失败
 */
BaseType_t xQueuePeek(
                          QueueHandle_t xQueue,
                          void * const pvBuffer,
                          TickType_t xTicksToWait
                      );

BaseType_t xQueuePeekFromISR(
                                 QueueHandle_t xQueue,
                                 void *pvBuffer,
                             );

总结

首先,队列的本质是任务与任务,任务与中断之间的一种数据交流的机制,队列的内存空间由两部分组成,一部分是队列结构体,一部分是队列存储区域,可以详细见队列结构体的图,对于理解队列的源码有很大帮助。使用队列的流程是:创建队列,写队列,读队列,删除队列,其中,队列是消耗性资源,一次只能唤醒一个阻塞的任务。

创建队列:主要负责为队列申请内存,然后调用函数prvInitialiseNewQueue()对队列进行初始化,也即初始化队列结构体的成员变量,其中还会对队列进行重置。

写队列:函数xQueueSend()等写队列函数都是调用xQueueGenericSend()函数,只是带入的参数不同,指定了不同的写入位置。

读队列:有两种,一种最常用的就是,读取队列项之后会删除队列项,”偷看“则不会删除队列项,二者都是从队列头开始读数据。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/361606.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

生活中常见标识

一维码 一维条码即指条码条和空的排列规则,常用的一维码的码制包括:EAN码、39码、交叉25码、UPC码、128码、93码,ISBN码,及Codabar(库德巴码)等。 常见场景: - 快销品- 常用五金- 通讯设备其中蕴含的信息就是条码的****一串数字 二维码 常见场景

性能测试中,我遇到的8个常见问题总结

性能压测中我们需要明白以下几点&#xff1a; 1、好的开始是成功的一半&#xff0c;前期的准备非常重要&#xff1b; 2、过程中&#xff0c;关注每个细节&#xff0c;多个维度监控&#xff1b; 3、在调优中多积累经验&#xff1b; 4、对结果负责&#xff0c;测试报告要清晰…

Redis实战案例

文章目录1、SpringBoot整合Redis1.1、新建项目1.2、接口编写1.3、集成Redis1.3、测试1.4、序列化问题2、Redis实现分布式缓存2.1、背景介绍2.2、代码编写2.3、缓存改造2.4、小结3、RedisAOP自定义注解&#xff0c;优雅实现分布式缓存3.1、自定义注解3.2、AOP切面类3.3、测试3.4…

跳跃游戏II-力扣45-java 动态规划

一、题目描述给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说&#xff0c;如果你在 nums[i] 处&#xff0c;你可以跳转到任意 nums[i j] 处:0 < j < nums[i] i j < n返回到达 nums[n - …

手撸一个Table组件(Table组件不过如此)

一、前言 手写Table组件这个文章我一直都想写&#xff0c;今天终于得空来写它了。小编认为Table组件是组件库里"较为复杂"的一个组件&#xff0c;因为它的扩展性非常强&#xff0c;并且它的基础样式如何去写都非常考究&#xff0c;那么今天我就带大家来实现一个基础…

SpringBoot整合Spring Security过滤器链加载执行流程源码分析

文章目录1.引言2.Spring Security过滤器链加载1.2.注册名为 springSecurityFilterChain的过滤器2、查看 DelegatingFilterProxy类3.查看 FilterChainProxy类3.1 查看 doFilterInternal方法。3.2 查看 getFilters方法。4 查看 SecurityFilterChain接口5 查看 SpringBootWebSecur…

JDK8增加的特性

Java知识点总结&#xff1a;想看的可以从这里进入 目录13、JDK8增加的特性13.1、Lambda表达式13.2、方法的引用13.3、时间处理类13.4、接口增加方法13.5、注解新增13.6、Optional类13.7、Stream13、JDK8增加的特性 13.1、Lambda表达式 Lambda表达式和方法的引用 13.2、方法的…

Java8的Optional类的使用 和 Stream流式操作

Java知识点总结&#xff1a;想看的可以从这里进入 目录13.6、Optional类13.7、Stream13.7.1、Stream创建13.7.2、中间操作1、筛选2、切片3、映射4、排序13.7.3、终止操作1、遍历2、聚合3、匹配查找4、归约5、收集归集统计分组字符串拼接归约13.6、Optional类 Optional类是一个…

【已解决】异常断电文件损坏clickhouse启动不了:filesystem error Structure needs cleaning

问题 办公室有一台二手服务器&#xff0c;作为平时开发测试使用。由于机器没放在机房&#xff0c;会偶发断电异常断电后&#xff0c;文件系统是有出问题的可能的&#xff0c;尤其是一些不断在读写合并的文件春节后&#xff0c;发现clickhouse启动不了&#xff0c;使用systemct…

【Nginx】【一】Nginx简介

Nginx简介 背景介绍 Nginx&#xff08;“engine x”&#xff09;一个具有高性能的【HTTP】和【反向代理】的【WEB服务器】&#xff0c;同时也是一个【POP3/SMTP/IMAP代理服务器】&#xff0c;是由伊戈尔赛索耶夫(俄罗斯人)使用C语言编写的&#xff0c;Nginx的第一个版本是200…

nginx的平滑升级、反向代理负载均衡

文章目录一、负载均衡介绍二、nginx的平滑升级和版本回滚1.平滑升级2.版本回滚3.本实验纯代码过程三、反向代理负载均衡总结一、负载均衡介绍 四层负载均衡 所谓四层负载均衡是指OSI七层模型中的传输层, 那么传输层Nginx已经支持TCP/IP的控制, 所以只需要对客户端的请求进行TCP…

RPC编程:RPC概述和架构演变

RPC编程系列文章第一篇一&#xff1a;引言1&#xff1a;本系列文章的目标2&#xff1a;RPC的概念二&#xff1a;架构的演变过程1&#xff1a;单体架构1)&#xff1a;概念2)&#xff1a;特点3)&#xff1a;优缺点2&#xff1a;单体架构水平扩展1)&#xff1a;水平拓展的含义2)&a…

提到数字化,你想到哪些关键词

我们的生活中已经充满了数据&#xff0c;各种岗位例如运营、市场、营销上也都喜欢在职位要求加上一条利用数据、亦或是懂得数据分析。事实上&#xff0c;数据已经成为了构建现代社会的基本生产要素&#xff0c;并且因为不受自然环境的限制&#xff0c;已经成为了人们对未来社会…

【论文笔记】Manhattan-SDF==ZJU==CVPR‘2022 Oral

Neural 3D Scene Reconstruction with the Manhattan-world Assumption 本文工作&#xff1a;基于曼哈顿世界假设&#xff0c;重建室内场景三维模型。 1.1 曼哈顿世界假设 参考阅读文献&#xff1a;Structure-SLAM: Low-Drift Monocular SLAM in Indoor EnvironmentsIEEE IR…

ASEMI高压MOS管ASE20N65SE体积,ASE20N65SE大小

编辑-Z ASEMI高压MOS管ASE20N65SE参数&#xff1a; 型号&#xff1a;ASE20N65SE 漏极-源极电压&#xff08;VDS&#xff09;&#xff1a;650V 栅源电压&#xff08;VGS&#xff09;&#xff1a;30V 漏极电流&#xff08;ID&#xff09;&#xff1a;20A 功耗&#xff08;P…

Ardiuno-交通灯

LED交通灯实验实验器件&#xff1a;■ 红色LED灯&#xff1a;1 个■ 黄色LED灯&#xff1a;1 个■ 绿色LED灯&#xff1a;1 个■ 220欧电阻&#xff1a;3 个■ 面包板&#xff1a;1 个■ 多彩杜邦线&#xff1a;若干实验连线1.将3个发光二极管插入面包板&#xff0c;2.用杜邦线…

Docker----------day-mysql8主从复制

1.安装master 1.1拉取镜像 docker search mysql docker pull mysql mkdir -p /home/mysql8/data mkdir -p /home/mysql8/conf mkdir -p /home/mysql8/log1.2 2.启动交互式添加容器数据卷 #不添加容器数据卷 docker run -p 3307:3306 --name mysql_master -e MYSQL_ROOT_PASS…

xss基础

目录标题一、XSS的原理二、XSS漏洞分类1、反射型xss2、存储型XSS3、基于DOM的XSS三、XSS漏洞的危害及验证四、XSS漏洞的黑盒测试五、XSS漏洞的白盒测试一、XSS的原理 跨站脚本攻击XSS&#xff08;Cross Site Scripting&#xff09;&#xff0c;为了不和层叠样式表&#xff08;…

有序表之红黑树

文章目录1、五个条件2、调整策略2.1 插入调整的情况2.1.1 情况一&#xff1a;插入节点是红色&#xff0c;其父节点也是红色2.1.2 情况二2.1.2 代码实现2.2 删除调整的情况2.2.1 情况一&#xff1a;双重黑节点的兄弟节点也是黑色&#xff0c;且其兄弟的两个孩子也是黑色2.2.2 情…

100亿级订单怎么调度,来一个大厂的极品方案

背景 超时处理&#xff0c;是一个很有技术难度的问题。 所以很多的小伙伴&#xff0c;在写简历的时候&#xff0c;喜欢把这个技术难题写在简历里边&#xff0c; 体现自己高超的技术水平。 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;尼恩经常指导大家 优化简历。 最…