RT-Thread线程管理

一、概述

这是我学习RT-Thread线程管理过程中记录的笔记,方便自己查看,仅供参考,有什么不对的地方忘各位大佬指出。想要了解更详细的内容,请浏览官方文档“线程管理

如下图所示,一个子任务不间断地读取传感器数据,并将数据写到共享内存中,另外一个子任务周期性的从共享内存中读取数据,并将传感器数据输出到显示屏上。

这是一个简单的应用场景,可以理解为一个线程负责生产数据,一个线程负责消费数据,这样即可提高代码的执行速度,也大大的优化的程序代码。

在 RT-Thread 中,子任务对应的程序实体就是线程,线程是实现任务的载体,也是 RT-Thread 中最基本的调度单位,当线程运行时,它会认为自己是以独占 CPU 的方式在运行。

二、线程管理的功能特点

RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程和用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除,如下图所示,每个线程都有重要的属性,如线程控制块、线程栈、入口函数等。

  • RT-Thread 的线程调度器是抢占式的。

  • 当优先级高的线程就绪时,即可立刻得到了 CPU 的使用权。

  • 可以通过中断服务程序,使优先级高的线程就绪,并立即运行。

  • 调度器调度线程切换时,会保存当前线程的上下文(线程执行时的运行环境称为上下文,具体来说就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等),当再切回到这个线程时,线程调度器将该线程的上下文信息恢复。

三、线程的工作机制

  1. 线程控制块
    RT-Thread在文件rtdef.h文件中定义了,线程控制块的结构体 rt_thread ,线程控制块是操作系统用于管理线程的一个数据结构,它会存放线程的一些信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等,详细定义如下:

    /* 线程控制块 */
    struct rt_thread
    {
    /* rt 对象 */
    char name[RT_NAME_MAX]; /* 线程名称 */
    rt_uint8_t type; /* 对象类型 */
    rt_uint8_t flags; /* 标志位 */
    rt_list_t list; /* 对象列表 */
    rt_list_t tlist; /* 线程列表 */
    /* 栈指针与入口指针 */
    void *sp; /* 栈指针 */
    void *entry; /* 入口函数指针 */
    void *parameter; /* 参数 */
    void *stack_addr; /* 栈地址指针 */
    rt_uint32_t stack_size; /* 栈大小 */
    /* 错误代码 */
    rt_err_t error; /* 线程错误代码 */
    rt_uint8_t stat; /* 线程状态 */
    /* 优先级 */
    rt_uint8_t current_priority; /* 当前优先级 */
    rt_uint8_t init_priority; /* 初始优先级 */
    rt_uint32_t number_mask;
    ......
    rt_ubase_t init_tick; /* 线程初始化计数值 */
    rt_ubase_t remaining_tick; /* 线程剩余计数值 */
    struct rt_timer thread_timer; /* 内置线程定时器 */
    void (*cleanup)(struct rt_thread *tid); /* 线程退出清除函数 */
    rt_uint32_t user_data; /* 用户数据 */
    };

    注意:在程序运行过程中,程序的优先级是不会改变的,除非用户执行线程控制函数进行手动调整线程优先级。

  2. 线程栈

  • RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文保存在线程栈中。

  • 线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配(ARM 架构),当这个函数再调用另一个函数时,这些局部变量将放入栈中。

  • 线程第一次运行时,可以设置一些初始的环境:入口函数(PC 寄存器)、入口参数(R0 寄存器)、返回位置(LR 寄存器)、当前机器运行状态(CPSR 寄存器)。

  • 对于 ARM Cortex-M 架构,线程栈可构造如下图所示。

  • 线程栈大小设置,可以将适当设计较大的线程栈(注意堆栈溢出),然后在FinSH 中用 list_thread 命令查看线程运行的过程中线程所使用的栈的大小,最后最后对栈空间大小加以修改。

  1. 线程状态
线程状态 描述
初始状态 线程刚创建还没运行时就处于初始状态,在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT
就绪状态 在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY
运行状态 线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING
挂起状态 也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND
关闭状态 当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE
  1. 线程优先级
  • RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。

  • 在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置,对于 ARM Cortex-M 系列,普遍采用 32 个优先级。

  • 最低优先级默认分配给空闲线程使用,用户一般不使用。

  • 当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

  1. 时间片
    每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用,其单位是一个系统节拍(OS Tick),如下图所示:

  2. 线程运行模式

    • 无限循环模式
    void thread_entry(void* paramenter)
    {
    while (1)
    {
    /* 等待事件的发生 */
    /* 对事件进行服务、进行处理 */
    }
    }

    从程序中可以看出,一般通过while(1) 实现的是无限循环模式,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。
    注意:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。

    • 顺序执行或有限次循环模式:
    static void thread_entry(void* parameter)
    {
    /* 处理事务 #1 */
    /* 处理事务 #2 */
    /* 处理事务 #3 */
    }

    如简单的顺序语句、do while() 或 for()循环等,此类线程不会循环或不会永久循环,可谓是 “一次性” 线程,一定会被执行完毕。在执行完毕后,线程将被系统自动删除。

  3. 线程错误码

    #define RT_EOK 0 /* 无错误 */
    #define RT_ERROR 1 /* 普通错误 */
    #define RT_ETIMEOUT 2 /* 超时错误 */
    #define RT_EFULL 3 /* 资源已满 */
    #define RT_EEMPTY 4 /* 无资源 */
    #define RT_ENOMEM 5 /* 无内存 */
    #define RT_ENOSYS 6 /* 系统不支持 */
    #define RT_EBUSY 7 /* 系统忙 */
    #define RT_EIO 8 /* IO 错误 */
    #define RT_EINTR 9 /* 中断系统调用 */
    #define RT_EINVAL 10 /* 非法参数 */
  4. 线程状态切换

  5. 系统线程
    系统线程是指由系统创建的线程,用户线程是由用户程序调用线程管理接口创建的线程,在 RT-Thread 内核中的系统线程有空闲线程和主线程。

  6. 空闲线程
    空闲线程(idle)是系统创建的最低优先级的线程,线程状态永远为就绪态。当系统中无其他就绪线程存在时,调度器将调度到空闲线程,它通常是一个死循环,且永远不能被挂起。

    若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除,再将该线程的状态更改为关闭状态,不再参与系统调度,然后挂入 rt_thread_defunct 僵尸队列(资源未回收、处于关闭状态的线程队列)中,最后空闲线程会回收被删除线程的资源。

    空闲线程也提供了接口来运行用户设置的钩子函数,在空闲线程运行时会调用该钩子函数,适合钩入功耗管理、看门狗喂狗等工作。空闲线程必须有得到执行的机会,即其他线程不允许一直while(1)死卡,必须调用具有阻塞性质的函数;否则例如线程删除、回收等操作将无法得到正确执行。

  7. 主线程

三、线程的函数使用

可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。

  1. 创建动态线程

    rt_thread_t rt_thread_create(const char* name,
    void (*entry)(void* parameter),
    void* parameter,
    rt_uint32_t stack_size,
    rt_uint8_t priority,
    rt_uint32_t tick);
    • name:线程的名称;线程名称的最大长度由 rtconfig.h 中的宏 RT_NAME_MAX 指定,多余部分会被自动截掉
    • entry:线程入口函数
    • parameter:线程入口函数参数
    • stack_size:线程栈大小,单位是字节
    • priority:线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0~255,数值越小优先级越高,0 代表最高优先级
    • tick:线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
    • rt_thread_t:线程创建成功,返回线程句柄,创建失败时返回RT_NULL

    注意:使用rt_thread_create函数时,需要在 rtconfig.h 文件中定义RT_USING_HEAP宏,开启RT-Thread的堆内存。

  2. 删除线程

    rt_err_t rt_thread_delete(rt_thread_t thread);
    • thread:要删除的线程句柄
    • rt_err_t:删除线程成功返回:RT_EOK,删除线程失败返回:RT_ERROR。

    注意:

    • rt_thread_create() 创建的线程,不需要使用或运行出错时,需要使用rt_thread_delete()函数从系统中完全删除。
    • 用 rt_thread_delete() 函数删除线程接口,仅仅是把相应的线程状态更改为 RT_THREAD_CLOSE 状态,然后放入到 rt_thread_defunct 队列中;而真正的删除动作(释放线程控制块和释放线程栈)需要到下一次执行空闲线程时,由空闲线程完成最后的线程删除动作。
  3. 创建静态线程

    rt_err_t rt_thread_init(struct rt_thread* thread,
    const char* name,
    void (*entry)(void* parameter), void* parameter,
    void* stack_start, rt_uint32_t stack_size,
    rt_uint8_t priority, rt_uint32_t tick);
    • thread:线程句柄。线程句柄由用户提供出来,并指向对应的线程控制块内存地址
    • name:线程的名称;线程名称的最大长度由 rtconfig.h 中定义的 RT_NAME_MAX 宏指定,多余部分会被自动截掉
    • entry:线程入口函数
    • parameter:线程入口函数参数
    • stack_start:线程栈起始地址
    • stack_size:线程栈大小,单位是字节。在大多数系统中需要做栈空间地址对齐(例如 ARM 体系结构中需要向 4 字节地址对齐)
    • priority:线程的优先级。优先级范围根据系统配置情况(rtconfig.h 中的 RT_THREAD_PRIORITY_MAX 宏定义),如果支持的是 256 级优先级,那么范围是从 0 ~ 255,数值越小优先级越高,0 代表最高优先级
    • tick:线程的时间片大小。时间片(tick)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
    • rt_err_t:删除线程成功返回:RT_EOK,删除线程失败返回:RT_ERROR。
  4. 线程脱离函数

    rt_err_t rt_thread_detach (rt_thread_t thread);
    • thread:线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。

    注意:

    • 用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。
    • 这个函数接口是和 rt_thread_delete() 函数相对应的, rt_thread_delete() 函数操作的对象是 t_thread_create() 创建的句柄,而 rt_thread_detach() 函数操作的对象是使用 rt_thread_init() 函数初始化的线程控制块。
  5. 启动线程

    rt_err_t rt_thread_startup(rt_thread_t thread);
    • thread:线程句柄
  6. 获得当前线程

    rt_thread_t rt_thread_self(void);
    • rt_thread_t:返回thread时是,当前运行的线程句柄;RT_NULL是,失败,调度器还未启动。
  7. 使线程让出处理器资源
    作用:把当前线程从就绪线程队列中删除,然后挂到优先级队列链表的尾部(如果当前优先级只有这一个线程,则这个线程继续执行,不进行上下文切换)。

    rt_err_t rt_thread_yield(void);

    注意: rt_thread_yield() 函数和 rt_schedule() 函数比较相像,执行 rt_schedule() 函数后,当前线程并不一定被换出,即使被换出,也不会被放到就绪线程链表的尾部,而是在系统中选取就绪的优先级最高的线程执行(如果系统中没有比当前线程优先级更高的线程存在,那么执行完 rt_schedule() 函数后,系统将继续执行当前线程)。

  8. 使线程睡眠

    rt_err_t rt_thread_sleep(rt_tick_t tick);
    rt_err_t rt_thread_delay(rt_tick_t tick);
    rt_err_t rt_thread_mdelay(rt_int32_t ms);
    • tick/ms:线程睡眠的时间:sleep/delay 的传入参数 tick 以 1 个 OS Tick 为单位 ;mdelay 的传入参数 ms 以 1ms 为单位;

    注意:这三个函数接口的作用相同,调用它们可以使当前线程挂起一段指定的时间,当这个时间过后,线程会被唤醒并再次进入就绪状态。

  9. 挂起线程

    rt_err_t rt_thread_suspend (rt_thread_t thread);
    • thread:线程句柄
    • rt_err_t:RT_EOK:线程挂起成功;RT_ERROR:线程挂起失败,因为该线程的状态并不是就绪状态

    注意:

    • 使用rt_thread_suspend函数挂起和使用睡眠函数挂起的不同,使用睡眠函数挂起时,睡眠时间结束是会自动释放。
    • 一个线程尝试挂起另一个线程是一个非常危险的行为,因此RT-Thread对此函数有严格的使用限制:该函数只能使用来挂起当前线程(即自己挂起自己),不可以在线程A中尝试挂起线程B。
    • 使用rt_thread_suspend 挂起后,需要立刻调用 rt_schedule() 函数进行手动的线程上下文切换。
  10. 线程恢复

    rt_err_t rt_thread_resume (rt_thread_t thread);
    • thread:线程句柄
    • rt_err_t:RT_EOK:线程恢复成功;RT_ERROR:线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态
  11. 控制线程

    rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
    • thread:线程句柄
    • cmd:指示控制命令
    • arg:控制参数
    • rt_err_t:RT_EOK:控制执行正确;RT_ERROR:失败。

    制命令 cmd :

    • RT_THREAD_CTRL_CHANGE_PRIORITY:动态更改线程的优先级;
    • RT_THREAD_CTRL_STARTUP:开始运行一个线程,等同于 rt_thread_startup() 函数调用;
    • RT_THREAD_CTRL_CLOSE:关闭一个线程,等同于 rt_thread_delete() 或 rt_thread_detach() 函数调用。
  12. 设置和删除空闲钩子函数

    rt_err_t rt_thread_idle_sethook(void (*hook)(void));
    rt_err_t rt_thread_idle_delhook(void (*hook)(void));
    • hook:设置的钩子函数

    注意:空闲线程是一个线程状态永远为就绪态的线程,因此设置的钩子函数必须保证空闲线程在任何时刻都不会处于挂起状态,例如 rt_thread_delay(),rt_sem_take() 等可能会导致线程挂起的函数都不能使用。并且,由于 malloc、free 等内存相关的函数内部使用了信号量作为临界区保护,因此在钩子函数内部也不允许调用此类函数!

  13. 设置调度器钩子
    作用:在系统线程切换时,调度器钩子函数将被调用

    void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));
    • hook:表示用户定义的钩子函数指针
    • 钩子函数 hook() 的声明如下:
      void hook(struct rt_thread* from, struct rt_thread* to);
    • from:表示系统所要切换出的线程控制块指针
    • to:表示系统所要切换到的线程控制块指针

四、线程应用实例

创建线程示例

#include "stm32f10x.h"
#include "drv_gpio.h"
#include "drv_usart.h"
#include <rtthread.h>
#define THREAD_PRIORITY 25
#define THREAD_STACK_SIZE 512
#define THREAD_TIMESLICE 5
/* 线程 1 的对象和运行时用到的栈 */
static struct rt_thread thread1;
static rt_uint8_t thread1_stack[512];
/* 线程 1 入口 */
static void thread1_entry(void *parameter)
{
rt_uint32_t count = 0;
while (1)
{
/* 线程 1 采用低优先级运行,一直打印计数值 */
rt_kprintf("thread1 count: %d\n", count ++);
rt_thread_mdelay(500);
}
}
/* 线程 2 入口 */
static void thread2_entry(void *parameter)
{
rt_uint32_t count = 0;
/* 线程 2 拥有较高的优先级,以抢占线程 1 而获得执行 */
for (count = 0; count < 10 ; count++)
{
/* 线程 2 打印计数值 */
rt_kprintf("thread2 count: %d\n", count);
}
rt_kprintf("thread2 exit\n");
/* 线程 2 运行结束后也将自动被系统脱离 */
}
/* 线程例程初始化 */
int thread_sample_init()
{
rt_thread_t thread2_ptr;
rt_err_t rst;
/* 初始化线程 1 */
/* 线程的入口是 thread1_entry,参数是 RT_NULL
* 线程栈是 thread1_stack
* 优先级是 30,时间片是 5 个 OS Tick
*/
rst = rt_thread_init(&thread1,
"thread1",
thread1_entry,
RT_NULL,
&thread1_stack[0],
sizeof(thread1_stack),
RT_THREAD_PRIORITY_MAX - 2,
THREAD_TIMESLICE);
/* 启动线程 */
if (rst == RT_EOK)
{
rt_thread_startup(&thread1);
}
/* 创建线程 2 */
/* 线程的入口是 thread2_entry, 参数是 RT_NULL
* 栈空间是 512,优先级是 250,时间片是 25 个 OS Tick
*/
thread2_ptr = rt_thread_create("thread2",
thread2_entry,
RT_NULL,
THREAD_STACK_SIZE,
THREAD_PRIORITY,
THREAD_TIMESLICE);
/* 启动线程 */
if (thread2_ptr != RT_NULL)
{
rt_thread_startup(thread2_ptr);
}
return 0;
}
int main(void)
{
thread_sample_init();
rt_kprintf("RT-Thread Start...... \n");
while (1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
rt_thread_delay(1000); // 延时1000 ms
GPIO_SetBits(GPIOB, GPIO_Pin_12 );
rt_thread_delay(1000); // 延时1000 ms
}
}

运行结果如下:

注意:从运行结果可知,线程 2 计数到一定值会执行完毕,线程 2 被系统自动删除,计数停止。线程 1 一直打印计数。

线程时间片轮转调度示例

#include "stm32f10x.h"
#include "drv_gpio.h"
#include "drv_usart.h"
#include <rtthread.h>
#define THREAD_STACK_SIZE 512
#define THREAD_PRIORITY 30
#define THREAD_TIMESLICE 10
/* 线程入口 */
static void thread_entry(void* parameter)
{
rt_uint32_t value;
rt_uint32_t count = 0;
value = (rt_uint32_t)parameter;
while (1)
{
if(0 == (count % 5))
{
rt_kprintf("thread %d is running ,thread %d count = %d\n", value , value , count);
if(count> 50)
return;
}
count++;
}
}
int timeslice_sample(void)
{
rt_thread_t tid = RT_NULL;
/* 创建线程 1 */
tid = rt_thread_create("thread1",
thread_entry, (void*)1,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid != RT_NULL)
{
rt_thread_startup(tid);
}
/* 创建线程 2 */
tid = rt_thread_create("thread2",
thread_entry, (void*)2,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE-5);
if (tid != RT_NULL)
rt_thread_startup(tid);
return 0;
}
int main(void)
{
rt_kprintf("RT-Thread Start...... \n");
timeslice_sample();
while (1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
rt_thread_delay(1000); // 延时1000 ms
GPIO_SetBits(GPIOB, GPIO_Pin_12 );
rt_thread_delay(1000); // 延时1000 ms
}
}

运行结果如下:

注意:由运行的计数结果可以看出,线程 2 的运行时间是线程 1 的一半。

线程调度器钩子示例

#include "stm32f10x.h"
#include "drv_gpio.h"
#include "drv_usart.h"
#include <rtthread.h>
#define THREAD_STACK_SIZE 1024
#define THREAD_PRIORITY 20
#define THREAD_TIMESLICE 10
/* 针对每个线程的计数器 */
volatile rt_uint32_t count[2];
/* 线程 1、2 共用一个入口,但入口参数不同 */
static void thread_entry(void* parameter)
{
rt_uint32_t value;
value = (rt_uint32_t)parameter;
while (1)
{
rt_kprintf("thread %d is running\n", value);
rt_thread_mdelay(1000); // 延时一段时间
}
}
static rt_thread_t tid1 = RT_NULL;
static rt_thread_t tid2 = RT_NULL;
static void hook_of_scheduler(struct rt_thread* from, struct rt_thread* to)
{
rt_kprintf("from: %s --> to: %s \n", from->name , to->name);
}
int scheduler_hook(void)
{
/* 设置调度器钩子 */
rt_scheduler_sethook(hook_of_scheduler);
/* 创建线程 1 */
tid1 = rt_thread_create("thread1",
thread_entry, (void*)1,
THREAD_STACK_SIZE,
THREAD_PRIORITY, THREAD_TIMESLICE);
if (tid1 != RT_NULL)
{
rt_thread_startup(tid1);
}
/* 创建线程 2 */
tid2 = rt_thread_create("thread2",
thread_entry, (void*)2,
THREAD_STACK_SIZE,
THREAD_PRIORITY,THREAD_TIMESLICE - 5);
if (tid2 != RT_NULL)
{
rt_thread_startup(tid2);
}
return 0;
}
int main(void)
{
rt_kprintf("RT-Thread Start...... \n");
scheduler_hook();
while (1)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
rt_thread_delay(1000); // 延时1000 ms
GPIO_SetBits(GPIOB, GPIO_Pin_12 );
rt_thread_delay(1000); // 延时1000 ms
}
}

运行结果如下所示:

注意:由运行结果可以看出,对线程进行切换时,设置的调度器钩子函数是在正常工作的,一直在打印线程切换的信息,包含切换到空闲线程。

参考文献

RT-Thread 标准版本:https://www.rt-thread.org/document/site/#/rt-thread-version/rt-thread-standard/README

posted @   浇筑菜鸟  阅读(709)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示