(二)RT-Thread入门——线程管理

news2024/9/29 1:19:16

目录

线程管理

线程管理特点

线程工作机制 

线程控制块

线程属性

线程栈

线程状态

线程优先级

时间片

线程入口函数

无限循环模式

顺序执行或有限次循环模式

线程错误码

线程状态切换

线程操作

创建动态线程

删除 

初始化静态线程

脱离

获得当前线程

让出处理器资源

睡眠

控制线程

挂起线程

恢复线程

设置钩子函数

运行代码


线程管理

        RT-Thread是一个嵌入式实时多线程操作系统,基本属性之一是支持多任务,也就是允许多个任务同时运行,但是这并不意味着处理器在同一时刻真地执行了多个任务。事实上,这是通过多线程的方式实现的。线程是 RT-Thread 中最基本的调度单位,我们可以设置不同的优先级,重要的任务可设置相对较高的优先级,非重要的任务可以设置较低的优先级,不同的任务还可以设置相同的优先级,轮流运行,其实有点类似于之前学的中断。

        当线程运行时,它会认为自己是以独占 CPU 的方式在运行,线程执行时的运行环境称为上下文,也就是各个变量和数据,包括所有的寄存器变量、堆栈、内存信息等。

线程管理特点

        RT-Thread 线程管理的主要功能是对线程进行管理和调度,系统中总共存在两类线程,分别是系统线程用户线程,系统线程是由 RT-Thread 内核创建的线程,用户线程是由应用程序创建的线程,这两类线程都会从内核对象容器中分配线程对象,当线程被删除时,也会被从对象容器中删除。 而我们主要学习的就是用户线程的创建与调度,以及管理之类的。

        RT-Thread 的线程调度器是抢占式的,主要的工作就是从就绪线程列表中查找最高优先级线程,保证最高优先级的线程能够被运行,最高优先级的任务一旦就绪,就能得到 CPU 的使用权。

        当调度器调度线程切换时,先将当前线程上下文保存起来,当再切回到这个线程时,线程调度器将该恢复线程的上下文信息。

线程工作机制 

线程控制块

      线程控制块就是一个结构体,描述了线程里面的所有信息,例如优先级、线程名称、线程状态等,也包含线程与线程之间连接用的链表结构,线程等待事件集合等。  

        我们来仔细看一下这个结构体。 

/* 线程控制块 */
struct rt_thread
{
    /* rt 对象 */
    char        name[RT_NAME_MAX];     /* 线程名称 */
    rt_uint8_t  type;                   /* 对象类型 */
    rt_uint8_t  flags;                  /* 标志位 */

#ifdef RT_USING_MODULE
    void       *module_id;                /**< id of application module */
#endif

    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;                   /* 线程状态 */

    /* 对称多处理器,多核才用,我们不用 */
#ifdef RT_USING_SMP
    rt_uint8_t  bind_cpu;                        /**< thread is bind to cpu */
    rt_uint8_t  oncpu;                            /**< process on cpu` */

    rt_uint16_t scheduler_lock_nest;              /**< scheduler lock count */
    rt_uint16_t cpus_lock_nest;                   /**< cpus lock count */
    rt_uint16_t critical_lock_nest;               /**< critical lock count */
#endif /*RT_USING_SMP*/

    /* 优先级 */
    rt_uint8_t  current_priority;    /* 当前优先级 */
    rt_uint8_t  init_priority;        /* 初始优先级 */
    /* 如果优先级大于32 */
#if RT_THREAD_PRIORITY_MAX > 32
    rt_uint8_t  number;
    rt_uint8_t  high_mask;
#endif
    rt_uint32_t number_mask;

    /* 事件 */
    #if defined(RT_USING_EVENT)
    /* thread event */
    rt_uint32_t event_set; /* 事件集合 */
    rt_uint8_t  event_info;
#endif

#if defined(RT_USING_SIGNALS)
    rt_sigset_t     sig_pending;                   /**< the pending signals */
    rt_sigset_t     sig_mask;                      /**< the mask bits of signal */

    /* 对称多处理器,多核才用,我们不用 */
#ifndef RT_USING_SMP
    void            *sig_ret;     /**< the return stack pointer from signal */
#endif
    rt_sighandler_t *sig_vectors;                  /**< vectors of signal handler */
    void            *si_list;                    /**< the signal infor list */
#endif

    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;                      /* 用户数据 */
};

 rt_list_t   list;

        这里面的 rt_list_t 是一个什么结构类型呢,我们来看看。我们提高定义可以得到,这是一个双链表类型。

/**
 * Double List structure
 */
struct rt_list_node
{
    struct rt_list_node *next;                          /**< point to next node. */
    struct rt_list_node *prev;                          /**< point to prev node. */
};
typedef struct rt_list_node rt_list_t;    

        cleanup 会在线程退出时,被空闲线程回调一次以执行用户设置的清理现场等工作。最后的一个成员 user_data 可由用户挂接一些数据信息到线程控制块中,以提供一种类似线程私有数据的实现方式。


线程属性

线程栈

        RT-Thread 线程具有独立的栈,当进行线程切换时,会将当前线程的上下文存在栈中,当线程要恢复运行时,再从栈中读取上下文信息,进行恢复。线程栈还用来存放函数中的局部变量:函数中的局部变量从线程栈空间中申请;函数中局部变量初始时从寄存器中分配,当这个函数再调用另一个函数时,这些局部变量将放入栈中。

        线程栈的增长方向是芯片构架密切相关的,对于 ARM Cortex-M 架构,线程栈可构造如下图所示。

线程状态

        主要包含5个状态,我们来看一看。

状态描述
初始状态当线程刚开始创建还没开始运行时就处于初始状态;在初始状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_INIT
就绪状态

在就绪状态下,线程按照优先级排队,等待被执行;一旦当前线程运行完毕让出处理器,操作系统会马上寻找最高优先级的就绪态线程运行。此状态在 RT-Thread 中的宏定义为 RT_THREAD_READY

这个状态线程是参与调度的,只不过没有占用CPU。

运行状态线程当前正在运行。在单核系统中,只有 rt_thread_self() 函数返回的线程处于运行状态;在多核系统中,可能就不止这一个线程处于运行状态。此状态在 RT-Thread 中的宏定义为 RT_THREAD_RUNNING
挂起状态也称阻塞态。它可能因为资源不可用而挂起等待,或线程主动延时一段时间而挂起。在挂起状态下,线程不参与调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_SUSPEND
关闭状态当线程运行结束时将处于关闭状态。关闭状态的线程不参与线程的调度。此状态在 RT-Thread 中的宏定义为 RT_THREAD_CLOSE

线程优先级

        RT-Thread 最大支持 256 个线程优先级 (0~255),数值越小的优先级越高,0 为最高优先级。在一些资源比较紧张的系统中,可以根据实际情况选择只支持 8 个或 32 个优先级的系统配置;对于 ARM Cortex-M 系列,普遍采用 32 个优先级。最低优先级默认分配给空闲线程使用,用户一般不使用,主要用来回收资源。在系统中,当有比当前线程优先级更高的线程就绪时,当前线程将立刻被换出,高优先级线程抢占处理器运行。

时间片

        每个线程都有时间片这个参数,但时间片仅对优先级相同的就绪态线程有效。系统对优先级相同的就绪态线程采用时间片轮转的调度方式进行调度时,时间片起到约束线程单次运行时长的作用。

线程入口函数

        线程控制块中的 entry 是线程的入口函数,它是线程实现预期功能的函数。线程的入口函数由用户设计实现,一般有以下两种代码形式。

无限循环模式

        作为一个实时系统,一个优先级明确的实时系统,如果一个线程中的程序陷入了死循环操作,那么比它优先级低的线程都将不能够得到执行。所以在实时操作系统中必须注意的一点就是:线程中不能陷入死循环操作,必须要有让出 CPU 使用权的动作,如循环中调用延时函数或者主动挂起。用户设计这种无限循环的线程的目的,就是为了让这个线程一直被系统循环调度运行,永不删除。 

void thread_entry(void* paramenter)
{
    while (1)
    {
    /* 等待事件的发生 */

    /* 对事件进行服务、进行处理 */
    }
}

顺序执行或有限次循环模式

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

static void thread_entry(void* parameter)
{
    /* 处理事务 #1 */
    …
    /* 处理事务 #2 */
    …
    /* 处理事务 #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 /* 非法参数      */

线程状态切换

        线程通过调用函数 rt_thread_create/init() 进入到初始状态;初始状态的线程通过调用函数 rt_thread_startup() 进入到就绪状态;就绪状态的线程被调度器调度后进入运行状态,调用rt_thread_suspend()函数切换为挂起状态;当处于运行状态的线程调用 rt_sem_take(),rt_mutex_take(),rt_mb_recv() 等函数或者获取不到资源时,将进入到挂起状态;处于挂起状态的线程,如果等待超时依然未能获得资源或由于其他线程释放了资源,那么它将返回到就绪状态。挂起状态的线程,如果调用 rt_thread_delete/detach() 函数,将更改为关闭状态;而运行状态的线程,如果运行结束,就会在线程的最后部分执行 rt_thread_exit() 函数,将状态更改为关闭状态。

        就绪状态与运行状态是等同的。

线程操作

      线程的相关操作包含:创建 / 初始化线程、启动线程、运行线程、删除 / 脱离线程。可以使用 rt_thread_create() 创建一个动态线程,使用 rt_thread_init() 初始化一个静态线程,动态线程与静态线程的区别是:动态线程是系统自动从动态内存堆上分配栈空间与线程句柄(初始化 heap 之后才能使用 create 创建动态线程),静态线程是由用户分配栈空间与线程句柄。

创建动态线程

rt_thread_t rt_thread_create(const char *name, // 线程名字
                             void (*entry)(void *parameter),// 线程处理函数,也就是线程在哪执行
                             void       *parameter,// 传递的参数
                             rt_uint32_t stack_size,// 线程大小
                             rt_uint8_t  priority,// 线程优先级 0-31
                             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)的单位是操作系统的时钟节拍。当系统中存在相同优先级线程时,这个参数指定线程一次调度能够运行的最大时间长度。这个时间片运行结束时,调度器自动选择下一个就绪态的同优先级线程进行运行
返回线程控制块
thread线程创建成功,返回线程句柄
RT_NULL线程创建失败

删除 

        调用该函数后,线程对象将会被移出线程队列并且从内核对象管理器中删除,线程占用的堆栈空间也会被释放,收回的空间将重新用于其他的内存分配。 

rt_err_t rt_thread_delete(rt_thread_t thread)// 参数是结构体指针,返回值是一个错误码
参数描述
thread要删除的线程句柄
返回错误码
RT_EOK删除线程成功
-RT_ERROR删除线程失败

        接下来,我们来自己创建一个线程,代码如下。

rt_thread_t th1_ptr = NULL;

void th_entry(void *parameter)
{
    while(1){
        rt_kprintf("th_entry running......");
        rt_thread_mdelay(1000);
    }
}
int main(void)
{
    th1_ptr = rt_thread_create("first_th",th_entry,NULL,512,20,5);

    if (th1_ptr == RT_NULL) {
        LOG_E("rt_thread_create fail ....");
    }

    LOG_D("rt_thread_create successed....");
}

        运行结果如下,我们可以清楚的看到线程是创建成功了,接下来我们来尝试一下其他的函数,delete函数我们不建议自己去调用,因为线程会自己调用。

初始化静态线程

        和创建动态线程很像,就是加上了栈起始参数。 静态线程是指线程控制块、线程运行栈一般都设置为全局变量,在编译时就被确定、被分配处理,内核不负责动态分配内存空间。

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_EOK线程创建成功
-RT_ERROR线程创建失败

 

 

脱离

        对于用 rt_thread_init() 初始化的线程,使用 rt_thread_detach() 将使线程对象在线程队列和内核对象管理器中被脱离。线程脱离函数如下:

rt_err_t rt_thread_detach (rt_thread_t thread);
参数描述
thread线程句柄,它应该是由 rt_thread_init 进行初始化的线程句柄。
返回——
RT_EOK线程脱离成功
-RT_ERROR线程脱离失败

获得当前线程

        在程序的运行过程中,相同的一段代码可能会被多个线程执行,在执行的时候可以通过下面的函数接口获得当前执行的线程句柄:

rt_thread_t rt_thread_self(void);
返回描述
thread当前运行的线程句柄
RT_NULL失败,调度器还未启动

让出处理器资源

        当前线程的时间片用完或者该线程主动要求让出处理器资源时,它将不再占有处理器,调度器会选择相同优先级的下一个线程执行。线程调用这个接口后,这个线程仍然在就绪队列中。线程让出处理器使用下面的函数接口:

rt_err_t rt_thread_yield(void);

睡眠

        让运行的当前线程延迟一段时间,在指定的时间到达后重新运行,这就叫做 “线程睡眠”。线程睡眠可使用以下三个函数接口:

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 为单位;
返回——
RT_EOK操作成功

控制线程

        当需要对线程进行一些其他控制时,例如动态更改线程的优先级,可以调用如下函数接口:

rt_err_t rt_thread_control(rt_thread_t thread, rt_uint8_t cmd, void* arg);
函数参数描述
thread线程句柄
cmd指示控制命令
arg控制参数(参数地址)
返回——
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() 函数调用。

挂起线程

        线程挂起使用下面的函数接口:

rt_err_t rt_thread_suspend (rt_thread_t thread);
参数描述
thread线程句柄
返回——
RT_EOK线程挂起成功
-RT_ERROR线程挂起失败,因为该线程的状态并不是就绪状态

恢复线程

        恢复线程就是让挂起的线程重新进入就绪状态,并将线程放入系统的就绪队列中;

rt_err_t rt_thread_resume (rt_thread_t thread);
参数描述
thread线程句柄
返回——
RT_EOK线程恢复成功
-RT_ERROR线程恢复失败,因为该个线程的状态并不是 RT_THREAD_SUSPEND 状态

设置钩子函数

        在整个系统的运行时,系统都处于线程运行、中断触发 - 响应中断、切换到其他线程,甚至是线程间的切换过程中,或者说系统的上下文切换是系统中最普遍的事件。有时用户可能会想知道在一个时刻发生了什么样的线程切换,可以通过调用下面的函数接口设置一个相应的钩子函数。在系统线程切换时,这个钩子函数将被调用:

void rt_scheduler_sethook(void (*hook)(struct rt_thread* from, struct rt_thread* to));

        钩子函数 hook() 的声明如下:

void hook(struct rt_thread* from, struct rt_thread* to);
函数参数描述
from表示系统所要切换出的线程控制块指针
to表示系统所要切换到的线程控制块指针

运行代码

rt_thread_t th1_ptr = NULL;
struct rt_thread th2;

rt_uint8_t th2_stack[512] = {0};

void th1_entry(void *parameter)
{
    int i;
    for(i = 0; i < 5; i++)
    {
        rt_kprintf("th1_entry running......\n");
        rt_thread_mdelay(1000);
    }
}

void th2_entry(void *parameter)
{
    int i;
    for(i = 0; i < 5; i++)
    {
        rt_kprintf("th2_entry running......\n");
        rt_thread_mdelay(1000);
    }
}

void scheduler_hook(struct rt_thread* from, struct rt_thread* to)
{
    rt_kprintf("from:%s ------> to:%s\n",from->name,to->name);
}



int main(void)
{
    int ret = 0;

    rt_scheduler_sethook(scheduler_hook);

    th1_ptr = rt_thread_create("first_th",th1_entry,NULL,512,20,5);

    if (th1_ptr == RT_NULL) {
        LOG_E("rt_thread_create fail ....\n");
        return -RT_ENOMEM;
    }

    LOG_D("rt_thread_create successed....\n");
    /* 运行线程*/
    rt_thread_startup(th1_ptr);


    ret = rt_thread_init(&th2, "second_th", th2_entry, NULL, th2_stack, sizeof(th2_stack), 19, 5);
    if (ret < 0) {
            LOG_E("rt_thread_init fail ....\n");
            return ret;
        }

        LOG_D("rt_thread_init successed....\n");
        /* 运行线程*/
    rt_thread_startup(&th2);
}

运行结果

总结

        本节学习的是有关线程的知识,不得不说,RT官方的手册实在是太清晰了。 

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

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

相关文章

数据结构基础篇》》用c语言实现复数的八个基本运算

数据结构开讲啦&#xff01;&#xff01;&#xff01;&#x1f388;&#x1f388;&#x1f388; 本专栏包括&#xff1a; 抽象数据类型线性表及其应用栈和队列及其应用串及其应用数组和广义表树、图及其应用存储管理、查找和排序将从简单的抽象数据类型出发&#xff0c;深入浅出…

B-013 缓启动电路设计

缓启动电路设计1 简介2 案例分析2.1 电路说明2.2 原理分析3 电路参数设定说明1 简介 缓启电路的供电是由一个PMOS控制通断的&#xff0c;软启动的设计是让PMOS的导通时间变缓&#xff0c;电路上的做法是在PMOS的栅极和源极之间接一个合适的电容&#xff0c;PMOS的导通时间就会…

Arcgis中创建Python脚本工具

文章目录创建工具步骤第一步&#xff1a;第二步&#xff1a;第三步&#xff1a;定义工具工具箱Toolbox工具类1、__init__2、getParameterInfo3、isLicensed4、updateParameters5、updateMessage6、execute进度条的使用代码相比于自定义工具箱的源脚本和参数定义难以集中管理的缺…

中国专利电子申请网站系统环境配置方法

一、在线平台使用环境要求 支持的操作系统、浏览器、office的版本如下&#xff0c;必须匹配对应的版本&#xff1a; 操作系统&#xff1a;WINDOWS XP、WINDOWS 7、WINDOWS 8 浏览器&#xff1a;IE8、IE9、IE10 文档编辑软件&#xff1a;OFFICE2003、OFFICE2007 强烈推荐使用中…

1. Maven基础

1. Maven简介 Maven是专门用于管理和构建Java项目的工具&#xff0c;它的主要功能有&#xff1a; 提供了一套标准化的项目结构 提供了一套标准化的构建流程&#xff08;编译&#xff0c;测试&#xff0c;打包&#xff0c;发布……&#xff09; 提供了一套依赖管理机制 1.1…

Allegro快速编辑丝印文字操作指导

Allegro快速编辑丝印文字操作指导 Allegro支持丝印文字的编辑,下面介绍快速编辑丝印文字的两种方法如下 以编辑下方丝印文字为例 方法一: 选择Text edit 命令 点击丝印文字,丝印会被高亮起来 输入需要更改后的文字,如下 右击选择done 文字被更改好了 方法二 选择se…

Function composition

In mathematics, function composition is an operation  ∘  that takes two functions f and g, and produces a function h g  ∘  f such that h(x) g(f(x)). In this operation, the function g is applied to the result of applying the function f to x. That is…

van-uplaoder保存文件到后端,回显后端接口返回的数据

实现功能&#xff1a;在移动端使用van-uploader组件上传图片&#xff0c;然后调用接口保存到后端数据库&#xff0c;提交保存信息成功后&#xff0c;调用另外的接口返回数据用来回显uploaded的文件&#xff0c;&#xff08;一般正常的返回数据的接口是个图片地址&#xff0c;可…

15 CPP函数重载

函数重载的细节&#xff1a; 1 使用重载函数时&#xff0c;如果数据类型不匹配&#xff0c;C尝试使用类型转换与形参进行匹配&#xff0c;如果转换后有多个函数能匹配上&#xff0c;编译将报错。 2 引用可以作为函数重载的条件&#xff0c;但是调用重载函数的 时候&#xff0…

javaSE - 认识字符串(String class),String类里面方法的使用,下半部分

一、字符, 字节与字符串 1.1、字符与字符串之间进行转换 字符串内部包含一个字符数组&#xff0c;String 可以和 char[] 相互转换 将整个字符数组转换成字符串 public static void main(String[] args) {char[] chars {a,b,c,d,e,f,g};String str new String(chars);Sys…

Sulfo-NHS-SS-biotin,CAS:325143-98-4介绍,生物素双硫键琥珀酰亚胺

英文名称&#xff1a;Sulfo-NHS-SS-biotin 化学式&#xff1a;C19H27N4NaO9S4 分子量&#xff1a;606.7 CAS&#xff1a;325143-98-4 纯度&#xff1a;95% 储存条件&#xff1a;-20C 结构式&#xff1a; 简介&#xff1a;磺基NHS SS生物素是一种可切割试剂&#xff0c;用…

【剧前爆米花--爪哇岛寻宝】抽象类和接口(上)——理论及逻辑理解

作者&#xff1a;困了电视剧 专栏&#xff1a;《JavaSE语法与底层详解》 文章分布&#xff1a;这是一篇关于抽象类和接口的文章&#xff0c;在本篇文章中我会介绍其相关的定义和语法&#xff0c;并且揭示接口和抽象类的运行逻辑&#xff0c;提高对面对象编程的理解。 目录 抽象…

吴恩达week6 ~批量梯度下降 指数加权平均 动量梯度下降 学习率衰减 Adam

文章目录前言一、小批量梯度下降 mini-batch1、batch gradient descent2、stochastic gradient descent3、mini-batch gradient descent二、指数加权平均1.什么是指数加权平均2、理解指数加权平均3、与普通求平均值的区别4、指数加权平均的偏差修正三、gradient descent with m…

Allegro批量替换过孔类型操作指导

Allegro批量替换过孔类型操作指导 Allegro支持批量替换过孔类型,具体操作如下 例如需要把这些VIA10的过孔全部替换成VIA8的过孔 选择菜单上面的Tool-padstack-Group edit 右击选择temp Group 选中需要替换的过孔 选完之后右击选择complete 弹出Padstack Map窗口,modify…

java中多线程、并发、并行、线程与进程、线程调度、创建线程的方式

多线程&#xff1a; 多线程比单线程快&#xff0c;前面简单介绍过&#xff1a;集合如果是不安全的&#xff0c;那么它就是多线程的&#xff0c;了解多线程之前&#xff0c;先了解什么是并发和并行。 并发&#xff1a;指两个或多个事件在同一个时间段内发生。 并行&#xff1…

高通平台开发系列讲解(AtCoP篇)AtCoP架构简介

文章目录 一、ATCoP简介二、ATCoP架构三、流程沉淀、分享、成长,让自己和他人都能有所收获!😄 📢 本篇讲介绍高通ATCoP的架构。 一、ATCoP简介 ATCoP(AT Command Processor)是高通平台对于AT命令处理的模块,通过它,我们可以实现对AT命令的修改和新增。ATCoP接收从串口…

[附源码]Python计算机毕业设计公司办公自动化系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

跟大佬展开激励讨论String的不可变性

目录 大佬证明&#xff1a; 我的证明&#xff1a; 后续 什么是不可变&#xff1f; String为什么不可变 为什么要设计成为不可变的呢&#xff1f; 1.首先我们最先可以想到的Java主要做的就是安全 2.其次是字符串常量池的需要 String真的不可变吗&#xff1f; ps&#…

平淡无奇,2022年终总结

1前言 一年又一年&#xff0c;时间过得太快啦&#xff01; 最近总是想着2022年的点点滴滴&#xff0c;一年时光不得写点什么不一样的&#xff0c;可是思前想后觉得这一年也没什么不一样的啊。所以呢&#xff0c;2022年终总结标题就取名为平淡无奇。 人一生最重要的是什么&#…