【正点原子】嵌入式Linux C应用编程-第十一章

news2024/9/23 13:20:30

第十一章:线程

前言:

与进程类似,线程是允许应用程序并发执行多个任务的一种机制,线程参与系统调度,事实上,系统调度的最小单元为线程,而不是进程

1:线程的概念

什么是线程?

线程是参与系统调用的最小单位。它被包含在进程中,是进程中的实际运行单位。

一个进程中可以创建多个线程,多个线程实现并发运行,每个线程执行不同的任务

线程如何创建起来?

当一个程序启动时,就存在一个进程被OS创建,与此同时一个线程也立刻运行,该线程叫做程序的主线程,因为它是程序一开始就运行的线程

应用程序以main作为入口开始运行,所以main是主线程的入口函数,main函数所执行的任务就是主线程需要执行的任务

由此可见,任何一个进程都包含一个主线程,只有主线程的进程称为单线程进程。而所谓多线程是指除了主线程之外,还包含其他线程,其他线程一般为主线程创建,那么创建的新线程就是主线程的子线程

主线程的两个重点:

1:子线程由主线程创建,主线程是子线程的父辈

2:主线程通常需要最后结束运行,执行各种清理工作,譬如回收各个子线程

线程是程序基本的运行单位,而进程不是,真正运行的是进程中的线程,抢占CPU资源的也是线程。可以认为进程仅仅是一个容器,它包含了线程所允许所需要的数据结果、环境变量等信息

同一个进程中的多个线程共享该进程的全部系统资源,如:虚拟空间、文件描述符、全局变量。但各个线程有各自的调用栈(线程),自己的寄存器环境、自己的线程本地存储

1:线程不单独存在、而是包含在进程中

2:多线程宏观实现同时运行的效果

3:共享进程资源,首先表现在:所有线程具有相同的地址空间,意味着,线程可以访问地址空间的每一个虚拟地址

多进程和多线程的优劣势分析

多进程劣势

1:进程间的切换开销大。进程间的切换开销大于同进程中线程的切换

2:进程间的通信较为麻烦。每个进程都有自己的地址空间,相互独立,处于不同的地址空间,不像线程地址空间一样

多线程优点

1:开销少

2:通信容易

3:线程的创建速度大于进程的创建速度

4:在多核处理器中多线程更具有优势

总结

1:多线程较为广泛,相比较多进程

2:多线程编程难度大,对程序员的功底深,因为需要在多线程环境下考虑多个问题,例如:线程安全、信号处理

3:多进程通常在大型应用程序项目中,中小型项目使用多线程更多

并发与并行 

并发是一个一个运行,并行是真实的一次运行多个。

2:线程ID 

进程ID在整个系统中是唯一的,而线程ID不同,线程ID只有在它所属的进程上下文中才有意义

进程 ID 使用 pid_t 数据类型来表示,它是一个非负整数。而线程 ID 使用 pthread_t 数据类型来表示,

获取线程ID函数pthread_self


一个线程可通过库函数 pthread_self()来获取自己的线程 ID,其函数原型如下所示:
#include <pthread.h>
pthread_t pthread_self(void);
使用该函数需要包含头文件<pthread.h>。
该函数调用总是成功,返回当前线程的线程 ID

线程 ID 在应用程序中非常有用,原因如下:
1:很多线程相关函数,譬如后面将要学习的 pthread_cancel()、 pthread_detach()、 pthread_join()等,它们都是利用线程 ID 来标识要操作的目标线程;
2:在一些应用程序中,以特定线程的线程 ID 作为动态数据结构的标签,这某些应用场合颇为有用,既可以用来标识整个数据结构的创建者或属主线程,又可以确定随后对该数据结构执行操作的具体线程。

3:线程创建 pthread_create

主线程可以使用库函数 pthread_create()负责创建一个新的线程, 创建出来的新线程被称为主线程的子线程,

其函数原型如下所示:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

函数参数和返回值含义如下:
thread: pthread_t 类型指针, 当 pthread_create()成功返回时,新创建的线程的线程 ID 会保存在参数 thread所指向的内存中,后续的线程相关函数会使用该标识来引用此线程。


attr: pthread_attr_t 类型指针,指向 pthread_attr_t 类型的缓冲区, pthread_attr_t 数据类型定义了线程的各种属性,如果将参数 attr 设置为 NULL, 那么表示将线程的所有属
性设置为默认值,以此创建新线程。


start_routine: 参数 start_routine 是一个函数指针,指向一个函数, 新创建的线程从 start_routine()函数开始运行,该函数返回值类型为void *,并且该函数的参数只有一个void *,其实这个参数就是pthread_create()函数的第四个参数 arg。如果需要向 start_routine()传递的参数有一个以上,那么需要把这些参数放到一个结构体中,然后把这个结构体对象的地址作为 arg 参数传入。


arg: 传递给 start_routine()函数的参数。一般情况下,需要将 arg 指向一个全局或堆变量,意思就是说在线程的生命周期中,该 arg 指向的对象必须存在,否则如果线程中访问了该对象将会出现错误。 当然也可将参数 arg 设置为 NULL,表示不需要传入参数给 start_routine()函数。
返回值: 成功返回 0;失败时将返回一个错误号,并且参数 thread 指向的内容是不确定的


线程创建成功, 新线程就会加入到系统调度队列中,获取到 CPU 之后就会立马从 start_routine()函数开始运行该线程的任务;调用 pthread_create()函数后,通常我们无法确定系统接着会调度哪一个线程来使用CPU 资源,先调度主线程还是新创建的线程呢(而在多核 CPU 或多 CPU 系统中,多核线程可能会在不同的核心上同时执行)?如果程序对执行顺序有强制要求,那么就必须采用一些同步技术来实现。 这与前面学习父、子进程时也出现了这个问题, 无法确定父进程、子进程谁先被系统调度
 

线程创建例子 

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>

static void *new_thread_start(void *arg)  
{
     printf("thread:%d,%lu\n",getpid(),pthread_self());
     return (void*)0;  //返回值亮点

}
int main()
{
    pthread_t thread;  //这个变量的生存周期需要值得注意一下
    int ret;

    ret = pthread_create( (pthread_t *)&thread, NULL, new_thread_start, NULL);
    if(ret != 0 )  //判断函数是否使用成功,形成习惯
    {
        printf("pthread_create fail\n");
        exit(1);
    }

    printf("zhu:%d,%lu\n",getpid(),pthread_self());
    sleep(1);  //等待子线程执行完

    return 0;


}

代码总结

1:明确pthread_t的数据类型,因为在linux是usnigned long int ,而在其他的平台,就不是了,可能

2:主线程休眠一下sleep(1),表示休眠一秒

3:编译出现的问题,gcc的时候需要加 -lpthread

4:注意使用变量的生存时间

除了在线程 start 函数中执行 return 语句终止线程外, 终止线程的方式还有
多种,可以通过如下方式终止线程的运行:
1:线程的 start 函数执行 return 语句并返回指定值,返回值就是线程的退出码;
2:线程调用 pthread_exit()函数;
3:调用 pthread_cancel()取消线程;
如果进程中的任意线程调用 exit()、 _exit()或者_Exit(),那么将会导致整个进程终止,这里需要注意,因为exit结束的是主线程,而上面的结束的是当前使用线程
 

pthread_exit()函数将终止调用它的线程,其函数原型如下所示:
#include <pthread.h>
void pthread_exit(void *retval);

参数 retval 的数据类型为 void *,指定了线程的返回值、也就是线程的退出码,该返回值可由另一个线程通过调用 pthread_join()来获取;同理,如果线程是在 start 函数中执行 return 语句终止,那么 return 的返回值也是可以通过 pthread_join()来获取的。


参数 retval 所指向的内容不应分配于线程栈中,因为线程终止后,将无法确定线程栈的内容是否有效;
出于同样的理由,也不应在线程栈中分配线程 start 函数的返回值。因为我们定义的线程函数的类型是void*,因此返回值是地址,而我们知道在C语言中如果需要返回地址的话,那么这个地址不应该在栈中。
调用 pthread_exit()相当于在线程的 start 函数中执行 return 语句,不同之处在于,可在线程 start 函数所调用的任意函数中调用 pthread_exit()来终止线程。 如果主线程调用了 pthread_exit(),那么主线程也会终止,但其它线程依然正常运行,直到进程中的所有线程终止才会使得进程终止
 

4: 回收线程pthread_join

在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid()) 阻塞等待子进程退出并获取其终止状态,回收子进程资源; 而在线程当中, 也需要如此, 通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码, 回收线程资源;


int pthread_join(pthread_t thread, void **retval);
函数参数和返回值含义如下:
thread: pthread_join()等待指定线程的终止,通过参数 thread(线程 ID) 指定需要等待的线程;
retval: 如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到*retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消, 则将 PTHREAD_CANCELED 放在*retval 中。 如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL
返回值: 成功返回 0;失败将返回错误码。

调用 pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则 pthread_join()立刻返回。 如果多个线程同时尝试调用 pthread_join()等待指定线程的终止,那么结果将是不确定的

若线程并未分离(detached),则必须使用 pthread_join()来等待线程终止,回收
线程资源;如果线程终止后,其它线程没有调用 pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程。
当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收
 

pthread_join()与waitpid()区别

1:线程之间关系是对等的。进程中的任意线程均可调用 pthread_join()函数来等待另一个线程的终止。
譬如,如果线程 A 创建了线程 B,线程 B 再创建线程 C,那么线程 A 可以调用 pthread_join()等待线程 C 的终止,线程 C 也可以调用 pthread_join()等待线程 A 的终止;这与进程间层次关系不同,
父进程如果使用 fork()创建了子进程,那么它也是唯一能够对子进程调用 wait()的进程,线程之间不存在这样的关系。
2:不能以非阻塞的方式调用 pthread_join()。对于进程,调用 waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。
 

 程序说明

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <unistd.h>


void *threadret ;


static void *new_thread_start(void *arg)
{
    
     printf("thread:%d,%lu\n",getpid(),pthread_self());
    
     pthread_exit((void*)2);
    // return (void *)0;

}
int main()
{
    pthread_t thread;
    int ret;

    ret = pthread_create( (pthread_t *)&thread, NULL, new_thread_start, NULL);
    if(ret != 0 )
    {
        printf("pthread_create fail\n");
        exit(1);
    }
   
    printf("zhu:%d,%ld\n",getpid(),pthread_self());
    pthread_join(thread,(void **)&threadret); 
    printf("ret=%d\n",threadret);  //这里形式自己写错误了,写成了*threadret,是没有*的
    //exit();
    return 0;


}
//错误码打印
ret = pthread_join(tid, &tret);
if (ret) {
fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
exit(-1);
}

打印的结果为2

 5:取消线程

在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用 pthread_exit()退出,或在线程 start 函数执行 return 语句退出。
有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出
 

取消一个线程函数pthread_cancel

int pthread_cancel(pthread_t thread);
参数 thread 指定需要取消的目标线程;成功返回 0,失败将返回错误码。


发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1) 的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消 ,所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。就类似于信号一样,信号处理的方式我们也可以自己选择,信号也只是请求,如果你不管它,那么默认执行

因此当我们的主线程执行取消请求给子线程时(此时子线程未完成),那么子线程就会立刻退出,使用pthread_join

当主线程发送取消请求之后,新线程便退出了,而且退出码为-1
 

取消状态以及类型

默认情况下,线程是响应其它线程发送过来的取消请求的, 响应请求然后退出线程。 当然,线程可以选择不被取消或者控制如何被取消,通过 pthread_setcancelstate()和 pthread_setcanceltype()来设置线程的取消性状态和类型


int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);


pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数 state 中给定的值,并将线程之前的取消性状态保存在参数 oldstate 指向的缓冲区中, 如果对之前的状态不感兴趣, Linux 允许将参数 oldstate 设置为 NULL; pthread_setcancelstate()调用成功将返回 0,失败返回非 0 值的错误码

pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。
参数 state 必须是以下值之一:
⚫ PTHREAD_CANCEL_ENABLE: 线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
⚫ PTHREAD_CANCEL_DISABLE: 线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。
 

pthread_setcanceltype()函数


如果线程的取消性状态为 PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用 pthread_setcanceltype()函数来设置,它的参数 type 指定了需要设置的类型,而线程之前的取消性类型则会保存在参数 oldtype 所指向的缓冲区中,如果对之前的类型不敢兴趣, Linux下允许将参数 oldtype 设置为 NULL。 同样 pthread_setcanceltype()函数调用成功将返回 0,失败返回非 0 值的错误码。


pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。
参数 type 必须是以下值之一:
⚫ PTHREAD_CANCEL_DEFERRED: 取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point,将在 11.6.3 小节介绍) 为止,这是所有新建线程包括主线程默认的取消性类型。
⚫ PTHREAD_CANCEL_ASYNCHRONOUS: 可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少, 不再介绍!
当某个线程调用 fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型,而当某线程调 用 exec 函 数 时 , 会 将 新 程 序 主 线 程 的 取 消 性 状 态 和 类 型 重 置 为 默 认 值 , 也 就 是PTHREAD_CANCEL_ENABLE 和 PTHREAD_CANCEL_DEFERRED

取消点


若将线程的取消性类型设置为 PTHREAD_CANCEL_DEFERRED 时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。
那什么是取消点呢? 所谓取消点其实就是一系列函数, 当执行到这些函数的时候,才会真正响应取消请求, 这些函数就是取消点; 在没有出现取消点时,取消请求是无法得到处理的, 究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。


取消点函数包括哪些呢?下表给大家简单地列出了一些

 线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)

//该函数就不存在取消点,因此我们需要自己产生一个取消点
static void *new_thread_start(void *arg)
{
printf("新线程--start run\n");
for ( ; ; ) {
}
return (void *)0;
}

线程可取消性的检测

假设线程执行的是一个不含取消点的循环(譬如 for 循环、 while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它,就如上给大家列举的例子。
在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?

此时可以使用 pthread_testcancel(),该函数目的很简单,就是产生一个取消点,线程如果已
有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。 其函数原型如下所示:
#include <pthread.h>
void pthread_testcancel(void);

直接调用即可
 


 

 

 

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

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

相关文章

【游戏编程扯淡精粹】自研引擎切 UE

【游戏编程扯淡精粹】自研引擎切 UE UF2022 的两篇讲座&#xff0c;再加上 The Machinery 引擎项目失败 结合过去两年笔者使用自研引擎的体验&#xff0c;其实有一部分是共通的 Crystal Dynamics&#xff1a;如何从自研引擎转变到虚幻引擎5 游戏技术&#xff08;featurelist…

LVGL的学习及使用

1、LVGL简介 LVGL是最受欢迎的免费开源嵌入式图形库&#xff0c;可为任何MCU、MPU和显示器类型创建漂亮的用户界面。使用SquareLine工作室&#xff0c;使用拖放UI编辑器来简化开发。 1.1、LVGL源码下载 lvgl 在github 上的开源代码 https://github.com/lvgl/lvgl 下载的源码包里…

Vue的数据绑定

一、Vue的数据绑定 1、单向数据绑定&#xff1a;将Model绑定到View上&#xff0c;当通过JavaScript代码改变了Model时&#xff0c;View就会自动刷新。不需要进行额外的DOM 操作就可以实现视图和模型的联动 ​ a、数据只保存一份 ​ b、data—->DOM ​ &#xff08;1&am…

在wsl下开发T113的主线linux(3)-写入spinand测试

接下来是烧写入硬件验证&#xff0c;我的板子焊接的是W25N01GV&#xff0c;这里使用xfel&#xff0c;因为支持写入spi-nand。GitHub - xboot/xfel: Tiny FEL tools for allwinner SOC, support RISC-V D1 chipTiny FEL tools for allwinner SOC, support RISC-V D1 chip - GitH…

QML学习笔记【04】:常用控件

1 Repeater与model Window {width: 640; height: 480visible: truetitle: qsTr("Hello World")Column{id: colspacing: 30Repeater{model: 3 //model控制了所有的数据&#xff0c;这里定义了Button的数量Button{width: 100; height: 50text: "btn" index…

整数划分——完全背包的变形

整数划分——完全背包的变形一、题目二、思路分析1、状态转移方程&#xff08;1&#xff09;状态表示&#xff08;2&#xff09;方程书写2、循环与初始化&#xff08;1&#xff09;循环&#xff08;2&#xff09;初始化三、代码一、题目 二、思路分析 这道题这么看的话还是比较…

Docsify使用之Markdown语法

Docsify使用过程中的排版&#xff0c;他是基于Markdown语法的。我们来看一下使用的常用语法&#xff1a; 字体加粗&#xff1a; 在需要加粗的文字前后各加两个** 具体格式如下 **加粗内容** 在需要加粗的文字前后各加一个* 具体格式如下 *倾斜内容* 在需要加粗并且倾斜的…

缅怀2022,展望2023

个人主页&#xff1a;董哥聊技术我是董哥&#xff0c;嵌入式领域新星创作者创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01;文章目录1、缘起2、收获3、憧憬不知不觉&#xff0c;2022已然到了最后一天&#xff0c;同时也是我技术创作一周年…

2.脚手架和逆向工程-使用renren开源

1.脚手架工程 脚手架工程提供了业务模块通用的类&#xff0c;比如返回结果封装、异常封装、分页工具类等 比较好用脚手架工程如renren-fast 备份地址 gitgithub.com:nome1024/renren-fast.git 2.逆向工程——使用renren-generator生成代码 逆向工程的作用是根据数据库快速生…

2022年仪器仪表行业研究报

第一章 行业概况 仪器仪表是用以检出、测量、观察、计算各种物理量、物质成分、物性参数等的器具或设备。真空检漏仪、压力表、测长仪、显微镜、乘法器等均属于仪器仪表。仪器仪表是人们对客观世界的各种信息进行测量、采集、分析与控制的手段和设备&#xff0c;是人类了解世界…

MySQL之表的修改和约束条件的添加

修改表中的数据&#xff1a;update[DML] 语法格式&#xff1a; update 表名 set 字段名1值1&#xff0c;字段名2值2&#xff0c;字段名3值3......where 条件;注意&#xff1a;没有条件限制会导致所有数据全部更新 举例&#xff1a; - 将id号为10的学生的姓名改变为"jas…

【自学Python】解释型程序与编译型程序

解释型程序与编译型程序 解释型程序与编译型程序教程 高级语言所编制的程序不能直接被计算机识别&#xff0c;必须经过转换才能被执行&#xff0c;按转换方式可将它们分为两类&#xff1a;解释型程序与编译型程序。 解释型程序 执行方式类似于我们日常生活中的 “同声翻译”…

Vue--》实现todo-list组件的封装与使用

目录 项目结构 创建todolist组件 创建todoinput组件 创建todobutton组件 项目结构 今天用 vite 脚手架搭建一个 vue3 的小案例&#xff0c;vite的搭建过程参考&#xff1a;vite的搭建 。其项目结构组件构成如下&#xff1a;注意&#xff1a;因为使用的是 vite 框架&#x…

51单片机GMS短信自动存取快递柜

实践制作DIY- GC0103-直流电机PID速度控制 一、功能说明&#xff1a; 基于51单片机设计-GMS短信自动存取快递柜 功能介绍&#xff1a; STC89C52RC最小系统板0.96寸OLED显示器DY-SV17F串口语音播报模块4*4矩阵键盘GSM短信模块4路舵机&#xff08;模拟4个柜子&#xff09; ***…

再学C语言21:循环控制语句——do while循环

一、其他赋值运算符 除了最基本的赋值运算符&#xff0c;C还有多个赋值运算符 a b等于a a ba - b等于a a - ba * b等于a a * ba / b等于a a / ba % b等于a a % b 这些赋值运算符的优先级与赋值运算符同样低 运算符优先级&#xff1a;赋值运算符 < 关系运算符 <…

《C++程序设计原理与实践》笔记 第9章 类相关的技术细节

在本章中&#xff0c;我们继续关注主要的程序设计工具——C语言。本章主要介绍与用户自定义类型&#xff08;即类和枚举&#xff09;相关的语言技术细节。这些语言特性大部分是以逐步改进一个Date类型的方式来介绍的。采用这种方式&#xff0c;我们还可以顺便介绍一些有用的类设…

数据结构与算法_五大算法之分治算法

这篇笔记记录分治算法的思想和两道leetcode题。 分治算法思想&#xff1a; 规模为n的原问题的解无法直接求出&#xff0c;进行问题规模缩减&#xff0c;划分子问题&#xff0c;子问题相互独立而且和原问题解的性质是相同的&#xff0c;只是问题规模缩小了。递归地缩小问题规模…

用Python记录一场2023的烟花

弹指间&#xff0c;2023已经到来&#xff0c;新的一年&#xff0c;祝大家新年快乐&#xff0c;阖家幸福呀~~~ 好吧&#xff0c;进入正题&#xff0c;2023的到来&#xff0c;肯定少不了烟花吧&#xff08;外面不让放炮&#xff0c;那咱们就用python放炮【DOGE】&#xff09; 首…

JSON Web Tokens(JWT)简单使用

文章目录什么是JWT&#xff1f;JWD对字符串进行Base64加密JWT加密字符串解释JWT使用场景jwt 特点JWT token在线解密什么是JWT&#xff1f; JWT&#xff08;json web token&#xff09;&#xff0c;它并不是一个具体的技术实现&#xff0c;而更像是一种标准。 JWT规定了数据传输…

Flink系列Table API和SQL之:动态表、持续查询、将流转换成动态表、更新查询、追加查询、将动态表转换为流、更新插入流(Upsert)

Flink系列Table API和SQL之&#xff1a;动态表、持续查询、将流转换成动态表、更新查询、追加查询、将动态表转换为流、更新插入流一、表和流的转换二、动态表三、持续查询四、将流转换成动态表五、更新查询六、追加查询七、将动态表转换为流八、更新插入流(Upsert)一、表和流的…