C语言学习-ProtoThread

news2024/11/23 11:50:59

一、简介

随着RTOS的应用,程序在开发的时候,程序逻辑也变得越来越清晰。但是RTOS因为体量比较大,在一些内存比较小的MCU中无法应用。所以,在裸机的开发中,通常是使用一个while(1)来作为整个程序的大循环。当有条件需要执行的时候,CPU通常是处于“死等”的状态,或者先运行其他程序,等程序在while(1)中“绕回来”的时候再进行检查。这样的效率明显比较低。那有没有一种方法能像RTOS那样可以“并行”执行呢?说到这里就需要介绍protoThread了。

ProtoThread是一个极简的C语言协程库,由几个简单的.h文件构成。ProtoThread主要是利用switch case内嵌循环的特殊语法来实现的。因为ProtoThread完全是利用C语言的语法特性,所以ProtoThread可以适用所有C/C++项目

二、基本原理

达夫设备

达夫设备(Duff's device)是串行复制(serial copy)的一种优化实现,实现展开循环,进而提高执行效率。见如下代码:

void test_switch(uint8_t num)
{
    printf("test_switch enter,num:%d",num);
    uint8_t i = 2;
    switch(num)
    {
        case 0: do{ printf("case 0");
        case 1:     printf("case 1");
        case 2:     printf("case 2");
        case 3:     printf("case 3");
            }while(i--);
    }
    printf("test_switch exit");
}

int main(void)
{
    for(uint8_t i = 0;i < 3;i++)
    {
        test_switch(i);
    }
    return 0;
}

第一眼看到这样的代码,一定很怀疑是否可以执行。但是它就是可以执行的。那结果是什么呢?见下图:

可以看到,每一次调用该函数时,都是从对应编号的case开始执行,且该case下方的case也都会执行。那是因为每个case后没有break或return,程序就会继续向下执行。而执行到最后,有一个do{}while的循环。程序则会回到循环开头继续执行。是不是学到了新知识。这也是我们后边要说到的protoThread的基础。

演变

如果上边的代码能理解了,那我们看一下由上述代码演变而来的代码:

uint8_t test_for(uint8_t num)
{
    printf("test_for enter,num:%d",num);

    static uint8_t i = 0;
    static uint8_t state = 0;

    switch(state)
    {
        case 0:
            for(i = 0;i < 10;i++)
            {
                printf("for,%d",i);
                state = 1;
                return i;
        case 1:
            printf("case 1");
            }
    }
    return 0;
}

int main(void)
{
    for(uint8_t i = 0;i < 3;i++)
    {
        test_for(i);
    }
    return 0;
}

可以看到上述代码,case 1在一个for循环之内,而for循环,又在case 0之内。是不是有点绕,那看一下结果是什么。

为了方便观察结果,我在每一个节点处都打印了信息。可以看到,这里一共调用了10次test_for函数。第一次进入函数后,因为state为0,则执行了for循环。在for循环中将state状态置为1后,就退出了。第二次进入函数,因为state为1,则执行case1。然后,因为此时还在for循环内,则继续执行for循环。通过打印变量i的值,可以看到i值此时为1,表明,此时是第二次执行for语句。后续的调用都是先执行了case 1,然后又继续执行for循环,最后退出。而每一次调用test_for函数,也都只是运行一次for循环而已。需要注意的是,从第二次调用test_for函数,进入for循环都是从“半路”插入的,就是case1的地方。所以,也不会触发for循环的i=0赋值

进化

如果上边的代码也理解了,那我们再来一个更复杂的。使用宏定义。

#define BEGIN() static int state=0; switch(state) { case 0:
#define YIELD(x) do { state=__LINE__; return x; case __LINE__: printf("YIELD"); } while (0)
#define END() }
uint8_t test_declare(uint8_t num)
{
    printf("test_declare enter,num:%d",num);
    static int i = 0;
    BEGIN();
    printf("after begin");
    for (i = 0; i < 10; i++)
    {
        printf("for,i:%d",i);
        YIELD(i);
        printf("after yield,i:%d",i);
    }
    END();
    return 0;
}

int main(void)
{
    for(uint8_t i = 0;i < 3;i++)
    {
        test_declare(i);
    }
    return 0;
}

这里为什么要用宏来代码部分代码呢?有两部分原因,其一是为了便于我们理解后续的ProtoThread。另一方面呢,虽然不用宏也能写,但是在语法形式上,看起来就非常怪异。尤其是当逻辑代码比较多时,非常难以理解。为了突出算法的逻辑,优化语法的形式也是非常必要的。

这里解释一下各个宏定义。

BEGIN()只是申请了一个变量,然后产生一个switch后就结束了。

END()只是把switch的“}”补齐。

YIELD()稍微有点复杂。里边使用编译宏__LINE__。__LINK__会输出语句被调用时的行号。这里将行号赋值给一个case是为了让case更具唯一性,减少命名的负担。而do{} while(0)只是用来保证宏的安全性。这里边只是赋值state,然后就退出了。后边还包含了一个case。

如果该代码看不懂的话,可以结果test_for函数来看,会比较容易理解。

好了,解释了这么多,来看一下运行结果:

还是一共调用10次test_declare函数。第一次调用,执行了BEGIN()函数,执行case0,即顺序向下执行,执行了打印“after begin”。然后进入for循环开始执行。在YIELD中将行号赋值为state后,则退出。第二次进入后,执行BEGIN()函数,因为state变量已经在上一次调用中被赋值为行号。所以此时,则执行运行case _LINE_,打印“YIELD”。执行结束后,因为没有return/break,程序继续向下执行打印“after yield”。结束后,继续向下,因为在for循环里,则判断循环条件,并继续执行打印“for”。再次执行YIELD()时,赋值行号给state,退出。后续的调用,重复上述步骤。

三、ProtoThread

1.宏定义介绍

typedef unsigned short lc_t;

typedef struct {
    lc_t lc;
    char status;
} pt_t;

struct pt_sem {
  unsigned int count;
};

typedef struct pt_sem pt_sem_t;

#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED  2
#define PT_ENDED   3
LC_INIT()

初始化LC值,赋值为0

#define LC_INIT(s) s = 0;

LC_RESUME()

开始执行,创建一个switch

#define LC_RESUME(s) switch(s) { case 0:

LC_SET()

赋值行号,执行case。即相当于在此设置一个断点,后续可以从switch直接跳转到此处执行。

#define LC_SET(s) s = __LINE__; case __LINE__:

LC_END()

补全 "}"

#define LC_END(s) }

PT_INIT()

初始化PT变量,内部调用LC_INIT来初始化。

#define PT_INIT(pt)   LC_INIT((pt)->lc)

PT_THREAD()

protoThread的声明

#define PT_THREAD(name_args) char name_args

PT_BEGIN()

ProtoThread的起始,内部调用LC_RESUME创建一个switch。

#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; PT_YIELD_FLAG = PT_YIELD_FLAG; LC_RESUME((pt)->lc)

PT_END()

ProtoThread的结束,与PT_BEGIN()对应。内部补全“}”。并初始化参数值。且返回PT_ENDED值。

#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0;                        \
                   PT_INIT(pt); return PT_ENDED; }

PT_WAIT_UNTIL()

条件函数,只有满足condition条件才会继续向下执行,否则return PT_WAITING值。

#define PT_WAIT_UNTIL(pt, condition)                                           \
  do {                                                                         \
    LC_SET((pt)->lc);                                                          \
    if(!(condition)) {                                                         \
      return PT_WAITING;                                                       \
    }                                                                          \
  } while(0)

PT_WAIT_WHILE()

条件函数,满足cond则return PT_WAITING值,否则向下执行。

#define PT_WAIT_WHILE(pt, cond)  PT_WAIT_UNTIL((pt), !(cond))

PT_SCHEDULE()

判断pt函数是否还在运行。结束返回0,还在运行返回1.

#define PT_SCHEDULE(f) ((f) < PT_EXITED)

PT_WAIT_THREAD()

条件函数,判断pt函数是否运行完成。满足则return。否则继续向下执行。

#define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread))

PT_EXIT()

初始化参数,返回PT_EXITED

#define PT_SCHEDULE(f) ((f) < PT_EXITED)

PT_YIELD()

断点函数,再次运行则退出

#define PT_YIELD(pt)                                                           \
  do {                                                                         \
    PT_YIELD_FLAG = 0;                                                         \
    LC_SET((pt)->lc);                                                          \
    if(PT_YIELD_FLAG == 0) {                                                   \
      return PT_YIELDED;                                                       \
    }                                                                          \
  } while(0)

PT_YIELD_UNTIL()

判断函数,第一次运行且满足条件则向下运行,否则退出

#define PT_YIELD_UNTIL(pt, cond)                                               \
  do {                                                                         \
    PT_YIELD_FLAG = 0;                                                         \
    LC_SET((pt)->lc);                                                          \
    if((PT_YIELD_FLAG == 0) || !(cond)) {                                      \
      return PT_YIELDED;                                                       \
    }                                                                          \
  } while(0)

PT_SEM_INIT()

信号值初始化

#define PT_SEM_INIT(s, c) (s)->count = c

PT_SEM_WAIT()

条件函数,满足信号值>0的条件,则向下执行,否则退出,返回PT_WAITING。

#define PT_SEM_WAIT(pt, s)                                                     \
      do {                                                                     \
        PT_WAIT_UNTIL(pt, (s)->count > 0);                                     \
        --(s)->count;                                                          \
      } while(0)

2.实例分析

1).两个pt_thread函数每执行一次就跳到对方函数执行,两个函数看起来是并行的

static pt_t pt_1;
static pt_t pt_2;

static int protothread1_flag = 0;
static int protothread2_flag = 0;

static int pt_thread1(pt_t *pt)
{
    printf("[pt_1]:enter"); 

    PT_BEGIN(pt);

    while(1)
    {
        printf("[pt_1]:before yield"); 
        PT_YIELD(pt);
        printf("[pt_1]:after yield"); 
    }

    PT_END(pt);
}

static int pt_thread2(pt_t *pt)
{
    printf("[pt_2]:enter");

    PT_BEGIN(pt);

    while(1)
    {
        printf("[pt_2]:before yield"); 
        PT_YIELD(pt);
        printf("[pt_2]:after yield"); 
    }

    PT_END(pt);
}

int main(void)
{
    PT_INIT(&pt_1);

    PT_INIT(&pt_2);

    for(uint8_t i = 0;i < 10;i++)
    {
        pt_thread1(&pt_1);
        pt_thread2(&pt_2);
    }

    while(1);

    return 0;
}

结果如下:

可以看到,在第一次进入pt_thread1函数时,运行了PT_BEGIN,然后打印before。运行PT_YIELD时,插入断点,则直接跳出并且进入pt_thread2函数。第一次pt_thread2函数运行与pt_thread1相同,运行PT_BEGIN,然后打印before。运行PT_YIELD时,插入断点,直接退出。再次运行pt_thread1时,直接从第一次的断点处开始执行。所以并没有先打印before,而是先打印了after。打印后,因为还在while(1)循环中,所以从循环的起始开始运行。打印before后,再次运行PT_YIELD,则直接退出。再次调用pt_thread2运行与pt_thread1相同。

2).PT_YIELD比较简单,这里看一下PT_YIELD_UNTIL()

static pt_t pt_1;
static pt_t pt_2;

static int protothread1_flag = 0;
static int protothread2_flag = 0;

static int pt_thread1(pt_t *pt)
{
    printf("[pt_1]:enter"); 

    static int cnt = 0;
    cnt++;

    if(cnt < 5)
    {
        protothread1_flag = 1;
    }
    else
    {
        protothread1_flag = 0;
    }

    PT_BEGIN(pt);

    while(1)
    {
        printf("[pt_1]:before yield until"); 
        PT_YIELD_UNTIL(pt,protothread1_flag != 0);
        printf("[pt_1]:after yield until"); 
    }

    PT_END(pt);
}

static int pt_thread2(pt_t *pt)
{
    printf("[pt_2]:enter");

    static int cnt = 0;
    cnt++;

    if(cnt < 5)
    {
        protothread2_flag = 1;
    }
    else
    {
        protothread2_flag = 0;
    }


    PT_BEGIN(pt);

    while(1)
    {
        printf("[pt_2]:before yield until"); 
        PT_YIELD_UNTIL(pt,protothread2_flag != 0);
        printf("[pt_2]:after yield until"); 
    }

    PT_END(pt);
}

int main(void)
{
    PT_INIT(&pt_1);

    PT_INIT(&pt_2);

    for(uint8_t i = 0;i < 10;i++)
    {
        pt_thread1(&pt_1);
        pt_thread2(&pt_2);
    }

    while(1);

    return 0;
}

结果如下:

这里要先理解PT_YIELD_UNTIL()函数的意思。这个我们在上文说过:“判断函数,第一次运行且满足条件则向下运行,否则退出”。因为前5次,protothread1_flag和protothread2_flag的被置1,满足protothread_flag != 0的条件。所以,程序向下运行,打印相关信息。从第6次开始,protothread_flag被置为0,不满足protothread_flag != 0的条件。所以,当运行PT_YIELD_UNTIL时,直接退出

3).通常如果使用PT_YIELD_UNTIL,会使逻辑变得很复杂。所以,该函数在实际应用中使用的并不多。PT_WAIT_UNTIL函数则相对简单一些。PT_WAIT_UNTIL会根据条件语句来判断,直到条件不满足时,才会让出当前执行权限,功能上比PT_YIELD_UNTIL和PT_YIELD更灵活

static pt_t pt_1;
static pt_t pt_2;

static int protothread1_flag = 0;
static int protothread2_flag = 0;

static int pt_thread1(pt_t *pt)
{
    printf("[pt_1]:enter"); 

    PT_BEGIN(pt);

    while(1)
    {
        printf("[pt_1]:before wait");
        PT_WAIT_UNTIL(pt,protothread2_flag != 0);
        printf("[pt_1]:after wait");

        protothread1_flag = 1;
        protothread2_flag = 0;
    }

    PT_END(pt);
}

static int pt_thread2(pt_t *pt)
{
    printf("[pt_2]:enter");

    PT_BEGIN(pt);

    while(1)
    {
        protothread2_flag = 1;

        printf("[pt_2]:before wait");
        PT_WAIT_UNTIL(pt,protothread1_flag != 0);
        printf("[pt_2]:after wait");

        protothread1_flag = 0;
    }

    PT_END(pt);
}

int main(void)
{
    PT_INIT(&pt_1);
    PT_INIT(&pt_2);

    for(uint8_t i = 0;i < 10;i++)
    {
        pt_thread1(&pt_1);
        pt_thread2(&pt_2);
    }

    while(1);

    return 0;
}

看结果:

第一次调用pt_thread1时,程序打印before,因为protothread2_flag为默认值0。不满足“protothread2_flag != 0”的条件,所以直接让出当前执行权限。调用pt_thread2时,将protothread2_flag赋值为1,且打印before。因为protothread1_flag为默认值0,也不满足“protothread1_flag != 0”的条件,所以让出当前执行权限。再次回到pt_thread1中,因为在pt_thread2中将protothread2_flag赋值为1.则满足“protothread2_flag != 0”的条件,继续向下执行。赋值protothread1_flag=1、protothread2_flag=0。再次运行PT_WAIT_UNTIL,因为不满足“protothread2_flag != 0”的条件,所以让出执行权限。再次调用pt_thread2,因为在pt_thread1中赋值protothread1_flag=1。满足PT_WAIT_UNTIL的“protothread1_flag != 0”条件,所以继续运行,并赋值protothread1_flag=0。因为还在while(1)循环中,则继续从头开始执行循环。因为不满足PT_WAIT_UNTIL的“protothread1_flag != 0”条件,则让出运行权限。周而复始。此时看起来就像两个函数在同时执行

4).除了PT_WAIT_UNTIL这种让出执行权限的场景,在平时还需要用到“生产者-消费者”的场景,protoThread同样也提供了这样的场景。

static pt_t pt_1;

PT_THREAD(test_pt_wait(pt_t *pt))
{
    static uint8_t cnt = 0;
    static pt_sem_t temp_sem;
    cnt++;
    
    printf("test_pt_wait,cnt:%d",cnt);

    if(cnt > 5)
    {
        PT_SEM_SIGNAL(pt,&temp_sem);
    }

    PT_BEGIN(pt);
    PT_SEM_INIT(&temp_sem,0);

    PT_SEM_WAIT(pt,&temp_sem);

    printf("after PT_SEM_WAIT");

    PT_END(pt);
}

int main(void)
{
    PT_INIT(&pt_1);
    printf("start test_pt_wait");
    while(PT_SCHEDULE(test_pt_wait(&pt_1)));
    printf("end test_pt_wait");

    while(1);

    return 0;
}

结果如下:

这里使用PT_SCHEDULE来做条件判断。只有test_pt_wait完整退出后,程序才会继续往下走。否则,一直运行test_pt_wait函数

程序第一次进入test_pt_wait函数中后,初始化一个类似“计数信号量”的值。只有当这个值非零时,程序才会往下执行否则让出执行权。但是这里限制,只有在第五次进入test_pt_wait函数时,“技术信号量”才会有所增加。所以,在前五次,程序只是进来,发现不满足条件,就直接退出了。只有第6次,才会继续向下运行。运行则完整退出。程序也就退出PT_SCHEDULE,且退出while循环。打印最终日志。

四、总结

ProtoThread是一种针对C语言封装后的宏库函数,为C语言模拟了一种无堆栈的轻量线程环境,能够实现模拟线程的条件阻塞、信号量操作等操作系统中特有的机制,从而使程序实现“多线程”操作。每个ProtoThread线程仅增加10行代码2字节RAM的额外硬件资源消耗。对于资源紧缺且不能移植操作系统的嵌入式系统,使用protoThread能够方便直观的设计多任务程序,能够实现用线性程序结构处理事件驱动型程序和状态机程序,简化了该类程序的设计

优点:

代码简单,占用空间小,只消耗2字节RAM的额外资源。适用于廉价的嵌入式设备。

可移植性好,因为完全基于C语言语法实现。C51/ARM等平台均支持。

缺点:

因为是stackless,所以无法保证恢复栈变量,代码中需要保存状态的变量需要使用静态局部变量代替

代码并不是真的“多线程”,编写代码逻辑时需要格外小心

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

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

相关文章

Vue路由传递query参数的两种方式

路由是可以传递参数的&#xff0c;一般使用query进行传参&#xff0c;有两种方式&#xff0c;接下来一一展示 案例展示 先编写一个简单的案例 我这里用的一个三级路由 这里使用三级路由以及展示路由视图 这样点击就跳转对应的路径了&#xff0c;接下来进行路由跳转的时候传参…

python之selenium入门教程

selenium&#xff0c;一个第三方库&#xff0c;可以通过给driver发送命令来操作浏览器&#xff0c;以达到模拟人操作浏览器&#xff0c;实现网页自动化、测试等&#xff0c;减少了重复性工作。 selenium的工作的基本架构如下&#xff1a; 安装 本文是在python环境下使用sele…

聚观早报 | 保时捷回应12.4万帕纳梅拉遭抢购;英特尔大规模降薪

今日要闻&#xff1a;保时捷回应12.4万帕纳梅拉遭抢购&#xff1b;特斯拉大力生产4680电池和Semi电动重卡&#xff1b;Spotify 月活用户预计下季度将达5亿里程碑&#xff1b;PayPal将裁员约2000人约占员工总数7%&#xff1b;英特尔大规模降薪 CEO基本薪酬削减25%保时捷回应12.4…

MATLAB应用3——深度视觉 奥比中光Astra S显示RGB和深度信息

首先从官网下载OpenNI驱动并安装&#xff0c;以及添加环境变量。MATLAB代码&#xff1a;% 参考&#xff1a;https://blog.csdn.net/limingmin2020/article/details/109445787%% 首次使用需编译mxNI.cpp文件&#xff0c;生成mxNI.mexw64mex mxNI.cpp -IF:\VS2017\VC\Astra_S\Ope…

Linux 内核代码审查人员短缺问题解决方法

导读操作系统安全是现在最重要的事情&#xff0c;而 Linux 则是一个主要被讨论的部分。首先要解决的问题之一就是&#xff1a;我们如何确定提交到上游的补丁已经进行了代码审核&#xff1f; Wolfram Sang 从 2008 年开始成为一名 Linux 内核开发者&#xff0c;他经常在各地召开…

Java三大特性之二——继承(工作、面试、学习必备技能)

目录 继承的概述 继承的特点 继承中的成员变量 继承中的成员方法 方法的重写 继承中的构造方法 super关键字 super和this的区别 继承的概述 多个类中存在相同属性&#xff08;成员变量&#xff09;和行为&#xff08;方法&#xff09;时&#xff0c;将这些内容抽取到单独一…

【自学Python】Python字符串以某个字符开始或结尾

大纲 Python字符串开头 Python字符串开头教程 在开发过程中&#xff0c;很多时候我们需要判断一个 字符串 是否以某个字符或者是否以某个字符串开始的需求&#xff0c;在 Python 中&#xff0c;判断某个字符串是否以某个字符或者是否以某个字符串开头的函数为 startswith() 。…

TFAPI使用2.0建议

2.5 TFAPI使用2.0建议 学习目标 目标 无应用 无 2.5.2 TF2.0最新架构图 饱受诟病TF1.0的API混乱 删除 queue runner 以支持 tf.data。删除图形集合。API 符号的移动和重命名。tf.contrib 将从核心 TensorFlow 存储库和构建过程中移除 TensorFlow 2.0 将专注于 简单性 和 易用…

【Unity VR开发】结合VRTK4.0:忽略某一层级

介绍&#xff1a; 由前面学习可知&#xff1a; 对象指针将与任何包含碰撞体&#xff08;甚至是触发器碰撞体&#xff09;的游戏对象发生冲突&#xff0c;但有时我们希望对象指针完全忽略游戏对象&#xff0c;就好像它不在场景中一样。 例如&#xff0c;如果我们手里拿着一个…

静图怎么做成gif动画图片?三步教你在线生成gif图片

说到gif动图相信大家都不陌生。那么&#xff0c;作为一名软件小白想要自己动手制作专属的gif动态图&#xff0c;要如何操作呢&#xff1f;接下来&#xff0c;给大家推荐一款专业在线gif制作&#xff08;https://www.gif.cn/&#xff09;工具-【GIF中文网】&#xff0c;通过两个…

力扣sql简单篇练习(七)

力扣sql简单篇练习(七) 1 销售分析 III 1.1 题目内容 1.1.1 基本题目信息1 1.1.2 基本题目信息2 1.1.3 示例输入输出 a 示例输入 b 示例输出 1.2 示例sql语句 # 所查字段来自同一张表可优先考虑子查询,但需要考虑原表有,关联表没有的这种情况 SELECT p.product_id,p.produ…

GO基本知识学习(一)

文章目录 1 GO基本知识1.1 windows安装1.2 `Go Modules`依赖包查找机制1.3 变量定义1.4 创建一个go项目1.5`go`的变量类型1.6 go的变量初始化1.7 变量作用域1.8 注释1 GO基本知识 ​ 官网地址:https://www.golang.org/ ​ 国内下载地址:https://studygolang.com/dl 1.1 wi…

【springboot进阶】基于starter项目构建(二)构建starter项目-gson

这个系列讲解项目的构建方式&#xff0c;主要使用 父项目 parent 和 自定义 starter 结合。项目使用最新的 springboot3 和 jdk19。本系列的代码仓库看查看 gitee 仓库 的 starter 目录。 这篇我们开始学习创建属于自己的 starter &#xff0c;实现一些常用模块的封装和自动配置…

Python | 数据类型之集合 | 函数

知识目录一、集合简介1.1 集合的定义1.2 实例二、集合的基本操作三、函数3.1 函数的定义3.2 函数的调用3.3 全局变量和局部变量一、集合简介 1.1 集合的定义 集合&#xff08;set&#xff09;是一个无序的不重复元素序列。 可以使用大括号 { } 或者 set() 函数创建集合&…

Hudi系列13:Hudi集成Hive

文章目录一. Hudi集成Hive概述二. Hudi集成Hive步骤2.1 拷贝jar包2.1.1 拷贝编译好的hudi的jar包2.1.2 拷贝Hive jar包到Flink lib目录2.1.3 Flink以及Flink SQL连接Hive的jar包2.2 重启hive2.3 Flink访问Hive表2.3.1 启动Flink SQL Client2.3.2 创建hive catalog2.3.3 切换 ca…

cpanel面板的虚拟主机重装wordpress

cpanel面板的虚拟主机重装wordpress很多朋友购买的虚拟主机都是采用的cPanel面板。想要重装WordPress的话&#xff0c;跟着以下步骤操作即可。登录cPanel管理后台&#xff0c;进入文件管理&#xff1b;删除网站的所有文件&#xff08;如果有需要请先备份然后下载到本地&#xf…

Ubuntu18.04 gRPC编译安装

一、CMake版本检查 grpc编译cmake要求最低版本为3.15。首先&#xff0c;cmake -version 查看当前cmake版本&#xff0c;如果低于3.15&#xff0c;按照以下步骤进行安装。 1.1 卸载已经安装的旧版的CMake sudo apt-get autoremove cmake 1.2 下载最新版本CMake CMake下载地…

2023年湖北中级工程师职称怎么评?甘建二告诉你

春季开始了&#xff0c;又到了职称评审正式申报提交资料的时候了&#xff0c;很多想评职称的小伙伴都不知道中级职称要怎么评&#xff1f;需要本人提供什么资料&#xff1f;配合哪些事情&#xff1f;怎么评一个中级职称呢&#xff1f;接下来甘建二老师跟您一起来看看&#xff1…

Redis性能问题排查指引

目录 Redis性能问题排查手段 1. 定位问题点 2. 定位Redis具体性能问题 参考&#xff1a; Redis性能问题排查手段 1. 定位问题点 当发生业务系统访问Redis慢或者超时异常时&#xff0c;可能的原因有以下三个&#xff1a; 客户端问题&#xff1a; 如果客户端使用的是Java版…

VueJS 之组件的生命周期

文章目录参考描述生命周期生命周期图示生命周期钩子组件创建阶段组件运行阶段组件销毁阶段举个栗子参考 项目描述搜索引擎Bing哔哩哔哩黑马程序员VueJS官方文档 描述 项目描述Edge109.0.1518.70 (正式版本) (64 位)操作系统Windows 10 专业版vue/cli5.0.8npm8.19.3VueJS2.6.1…