Linux-线程基础

news2024/12/29 9:55:46

Linux线程基础

  • 一,线程概念
    • 什么是线程
    • Linux如何实现的线程
  • 二,页表与虚拟内存
    • 虚拟内存
    • 多级页表
  • 三,线程的特点
    • 线程的优点
    • 线程的缺点
    • 线程异常
    • 线程用途
    • 线程与进程比较
  • 四,线程控制
    • 创建线程
    • 线程终止
    • 线程等待
    • 线程取消
    • 线程分离
  • 五,原生线程库的理解
    • 用户级线程库的理解
    • 线程id理解
    • 线程独立栈结构
    • 线程的局部存储

一,线程概念

什么是线程

🚀许多书籍上对线程的定义是这样的:线程是一个执行分支,执行粒度比进程更细,调度的成本更低
🚀站在系统的层面上:线程就是进程的一个执行流

Linux如何实现的线程

🚀首先既然有线程这么个东西,那么操作系统肯定要对其进行管理,那么就要进行先描述再组织,就要为线程创建结构体对其描述,然后通过某种数据结构将所有的线程组织起来,而上面又说到线程是进程的一个执行流,那么势必就要维护线程与进程之间的关系,等等。
🚀然而在Linux中,并没有专门属于线程的数据结构,而是对进程的那套东西进行了复用。

在这里插入图片描述
🚀在Linux下,是通过轻量级进程来模拟的线程,并没有真正的线程,这一个个task_struct都指向同一个进程地址空间,它们就是进程的一个执行流,CPU调度的基本单位就是task_struct。

🚀之前传统的进程其实就是一个单执行流的进程,也就是单线程的进程。

🚀每个tack_struct就是一个线程,而进程是包括pcb,虚拟地址空间,以及页表,和映射到物理内存的代码和数据等各种资源的集合。所以,线程是CPU调度的基本单位,而进程是承担资源分配的基本单位。

🚀线程就是在进程的地址空间上运行的,所以说执行的粒度比进程更细。

🚀Linux下不在区分进程和线程,而是将调度的执行流统一为轻量化进程。

如何理解线程的调度成本更低

CPU中存在大量的寄存器,其中有得寄存器指向pcb,有的指向地址空间,有的指向页表,由于进程是承担资源分配得基本单位,那么地址空间,页表资源是属于某个进程得,当进程切换得时候那么这些东西都要切换,那么就需要修改CPU中寄存器的指向。并且现代计算机的整个体系结构是多级缓存的结构的,在物理内存和CPU之间存在了cache(高速缓存)来减轻由于物理内存和CPU之间速度差过大的问题,CPU获取的数据或者指令是从cache中获取的,当进程切换的时候,cache中的数据都会被认定为失效的数据,需要从新从物理内存中加载。
而线程的切换只需要修改CPU中指向pcb的寄存器的内容即可,因为线程是在进程的地址空间上运行的,所以线程切换时,地址空间,页表等资源不用切换,并且多个线程看到的是同一个地址空间,一个线程在cache缓存中的内容,可能也会被另一个线程用到,所以cache的内容也不会失效。
所以,线程的调度成本更低。

二,页表与虚拟内存

🚀众所周知,我们可执行程序中的地址都是虚拟地址,而CPU需要的是物理地址,所以CPU在执行我们的代码的过程中,势必发生虚拟地址到物理地址的转化过程,页表中存储了虚拟地址和物理地址间的映射关系,而转化的工作是CPU中的MMU内存管理单元硬件来完成的。

虚拟内存

🚀虚拟内存其实就是对内存的一种建模,并且虚拟内存的这种规则不仅约束着操作系统,进程,还约束着编译器,编译器在编译代码的时候,就是按照虚拟内存的方式来编译的,在编译的时候就已经形成了虚拟地址。

在这里插入图片描述
如何理解可执行程序被加载到内存的

可执行程序被加载到内存实际是分两种情况的,当可执行程序的体积较小的时候,是一下都加载到内存中的,在加载到内存之前OS就为其创建好了各种数据结构,包括地址空间等等,由于我们代码中使用的都是虚拟地址所以在可执行程序加载到内存之后还要填写页表完成虚拟地址到物理地址的转换。而当程序的体积很大的时候,OS首先会将一部分的代码加载到内存中,然后构建虚拟地址和物理地址间的映射关系,由于是部分加载的,势必还有一部分在磁盘中,所以页表中虚拟地址的旁边那栏,有的指向了物理地址,有的指向了磁盘,当程序运行中发现某个虚拟地址没有与其映射的物理地址,那么就会触发缺页中断,此时OS就会将物理内存中选中一些已经使用过的页(叫做牺牲页),将他们换回到磁盘上,然后将刚刚虚拟地址对应的那部分代码和数据从磁盘上加载到物理内存中,重新完善页表的信息,然后CPU再次获取该虚拟地址对应的物理地址的时候,就可以继续运行了。

在这里插入图片描述

🚀现在你就能理解,为什么有的游戏几十GB,而物理内存只有几GB,但是依然能跑起来的原因,就是因为这种部分加载的原理,先加载一批代码当这批代码跑完后换一批新的代码继续,这样反复迭代就可以使一个很大的程序跑起来了。

多级页表

🚀物理内存与磁盘之间的交互是以块为单位的,通常一个数据块的大小是4KB,之所以采用这种较大的数据块的形式而不是以字节为单位,是因为物理内存与磁盘的速度差是很大的,所以每次IO的效率是很低的,所以磁盘在向物理内存加载数据的时候都是以数据块的形式加载的,但这也意味着数据刷新的时候,即使只改了1个字节的内容,也要刷新整个4KB的内容。

🚀所以,物理内存也是实行分页管理的,每页的大小是4KB,以32位系统为例,进程地址空间的大小是4GB,假设物理内存也为4GB,那么物理内存是要被分成(4* 1024 * 1024 * 1024)/ (4 * 1024) = 2^20个物理页,并且内核中为了更好的管理物理内存,还为物理内存创建了struct page结构体(以字节为单位),总体也就是 5M左右,在结构体中描述了该物理内存页是否被使用等等的相关属性。

🚀32位机器的虚拟地址是32位的,如果只有一级页表的情况下,为了能够精确定位到物理内存的具体某个页的某个字节,所以32为地址是分为两部分使用,前20位能够确定是属于哪个物理页,后12位确定的是在某个物理页中距页首地址的偏移量。那么一级页表的行数至少为2^20行,假设每行占4字节,那么整个页表占用的空间就为4* (2^20) = 4MB,并且这4MB是要一直存在的,而且在64位下情况会变得更复杂,所以通常会使用多级页表来压缩。

在这里插入图片描述

🚀所以提出了多级页表的概念,这里以2级页表为例子,将32位分为三部分使用,10 + 10 + 12的方式,前10位确定属于哪个2级页表,第二个10位确定在物理内存的哪个页上,后12位是页内偏移量。

在这里插入图片描述
🚀这种二级页表的结构,单个页表的大小是4B(假设每个条目的大小是4字节),那么如果所有页表都存在的话占用的空间为 4MB + 4KB,但是如果一级页表的某一行是没有使用的话,那么其指向的二级页表是不存在的,所以说大部分情况下只有一级页表和少部分二级页表是存在的,所以其实采用二级页表的形式是更节省空间的,并且检索的效率也是更高的。

三,线程的特点

线程的优点

1,创建一个新的线程的代价比创建一个新的进程小得多。

在Linux下,创建一个新的线程只需要创建一个新的task_struct即可,而对于进程而言还有有与其配套的进程地址空间,页表等。

2,与进程相比,线程的切换需要操作系统做的工作更少。

线程的切换只需要改变CPU中特定寄存器的内容,而对于进程而言,还要修改存放页表地址寄存器,存放地址空间地址的寄存器的内容,并且之前的cache缓存都是失效的。

3,线程占用的资源更少。
4,充分利用多处理器的可并行数量。
5,在等待慢速IO操作结束的同时,程序可执行其他计算任务。

典型的操作就是边下载边观看电影。

6,计算密集型应用,为了能够在多处理器系统上运行,将计算分解到多个线程中是实现。

7,I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

1,性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2,健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。一个线程出现问题,整个进程都会受到影响。

3,缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数,可能会对整个进程造成影响。

线程异常

1,单个线程如果出现除零,野指针问题导致线程崩溃,那么进程也会随之崩溃。
2,线程是进程的执行分支,线程出现异常,就类似于进程出现异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也会终止。

线程用途

1,合理的使用多线程,能提高CPU密集型程序执行效率。
2,合理的使用多线程,能提高IO密集型程序的用户体验。

线程与进程比较

1,进程是承担分配资源的基本单位。
2,线程是调度的基本单位。
3,线程共享进程的数据,但是也有自己的一部分数据:

  • 线程ID(内核角度:LWP,用户角度:pthread_t)
  • 一组寄存器(线程也要维护自己的上下文数据)
  • errno
  • 信号屏蔽字
  • 调度优先级

4,进程的多个线程共享进程的地址空间,因此进程的代码段,数据段都是共享的,如果定义一个函数,在各线程中都可以使用,如果定义一个全局变量,那么所有的线程也都可以访问到,除此之外线程还共享以下资源:

  • 文件描述符表
  • 各种信号处理方式
  • 当前工作目录
  • 用户id和组id

5,线程与进程的关系图:
在这里插入图片描述

四,线程控制

创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);

🚀 第一个参数:这是一个输出型参数,传入的是线程id(pthread_t类型)的地址。
🚀 第二个参数:设置线程的属性,通常设置为nullptr即可。
🚀 第三个参数:线程的入口函数,这是一个函数指针变量,创建线程成功后通过回调的方式执行这个函数。
🚀 第四个参数:这是线程入口函数的参数为void*类型。

int g_val = 100;
void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    while (true)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        sleep(1);
    }
}
const int NUM = 3;
int main()
{
    pthread_t tids[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }

    while (true)
    {
        cout << "i am main thread ,pthread id :" << pthread_self() << endl;
        sleep(1);
    }
    return 0;
}

这段代码也验证了,线程共享进程的地址空间,全局变量g_val是在数据段的,每个线程都可以访问到g_val变量,定义的线程入口函数是在代码段的,如果线程能运行证明每个线程都能看到这部分空间,同时给给线程入口函数传递的参数(线程的名字),本质是传递了一个地址,指向的空间是在进程地址空间的堆区,这也能验证线程共享进程的地址空间。

在这里插入图片描述

注意: 由于Linux系统没有真正的线程,只有轻量级进程,所以只有轻量级进程的接口,我们使用的是原生线程库,属于第三方库所以在编译时要加 -l的编译选项。

thread:thread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f thread

线程终止

🚀线程终止有三种方式:
1,return

当线程的函数执行完成后,return返回值,可以终止线程,并且return的值可以被主线程接受到。

void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        sleep(1);
        cnt--;
    }
    return (void *)0;
}

2,pthread_exit

void pthread_exit(void *retval);//参数就是其要返回的值

pthread_exit的作用与return类似,都是终止进程,并返回某个变量,主线程可以接受其返回值。

void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        sleep(1);
        cnt--;
    }
    pthread_exit((void *)0);
}

3,exit

void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        sleep(1);
        cnt--;
    }
    exit(0);
}

注意:
exit是终止进程,调用exit会使整个进程都会终止,那么除了此线程以外的线程也都会被终止。

void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        exit(0);//打印一句就退出
        sleep(1);
        cnt--;
    }
}

在这里插入图片描述
🚀可以看到线程2打印了一句就将进程终止了,至于打印出了哪句都是随机的,可能调度器最先调度的是线程2,同时还有可能打印完这句后还没到exit时就被OS被切换执行另一个线程去了等等,情况不唯一,但是此处要验证的是通过exit终止线程,那么整个进程都会被终止。
在这里插入图片描述

线程等待

🚀我们创建线程就是要让其完成某项工作,那么新线程势必要给主线程回馈,主线程也要接受其返回值。同时线程退出后其相关资源并没有被释放,主线程通过等待的方式释放其资源。

int pthread_join(pthread_t thread, void **retval);

🚀第一个参数:线程id,选择等待哪一个线程。

🚀第二个参数:这是一个输出型参数,就是将线程的返回值void* 类型的值带出来,所以传递的是void* 类型变量的地址。

🚀pthread_join是一种阻塞式等待。

int g_val = 100;
void *thread_run(void *args)
{
    const char *tname = (const char *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << tname << " : &gval :" << &g_val << " g_val : " << g_val << endl;
        sleep(1);
        cnt--;
    }
    delete tname;
    pthread_exit((void *)0);
}
const int NUM = 3;
int main()
{
    pthread_t tids[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *tname = new char[64];
        snprintf(tname, 64, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, thread_run, tname);
    }

    void *res;
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(tids[i], &res);
        // cout << "thread-" << i + 1 << "exit code : " << (int)res << endl;
        cout << "thread-" << i + 1 << "exit code : " << (uint64_t)res << endl;
    }

    cout << "all thrread quit" << endl;

    return 0;
}

注意一个小细节:
由于我们使用的云服务器是64位的环境,所以指针变量的大小为8个字节,所以上面代码中注释掉的那行是有问题的。

在这里插入图片描述

线程取消

🚀线程是可以被取消的,并且被取消的线程返回值为PTHREAD_CANCELED本质就是-1。
在这里插入图片描述

int pthread_cancel(pthread_t thread);

🚀参数为要取消的线程的id。

void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "i am a new thread" << endl;
        sleep(1);
        cnt--;
    }
    // PTHREAD_CANCELED;
    pthread_exit((void *)0);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, nullptr);
    sleep(2);
    pthread_cancel(tid);
    void *res;
    pthread_join(tid, &res);
    cout << "new thread exit code : " << (int64_t)res << endl;
    return 0;
}

在这里插入图片描述
问题: 新线程能自己取消自己吗?新线程能取消主线程吗?

void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "i am a new thread" << endl;
        pthread_cancel(pthread_self());//自己取消自己
        sleep(1);
        cnt--;
    }
    pthread_exit((void *)0);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, nullptr);
    void *res;
    pthread_join(tid, &res);
    cout << "new thread exit code : " << (int64_t)res << endl;
    return 0;
}

事实证明,新线程是可以自己取消自己的。

void *thread_run(void *args)
{
    pthread_t *tid = (pthread_t *)args;
    int cnt = 5;
    while (cnt)
    {
        cout << "i am a new thread" << endl;
        pthread_cancel(*tid);
        sleep(1);
        cnt--;
    }
    // PTHREAD_CANCELED;
    pthread_exit((void *)0);
}
int main()
{
    pthread_t tid;
    pthread_t self = pthread_self();
    pthread_create(&tid, nullptr, thread_run, &self);
    void *res;
    pthread_join(tid, &res);
    cout << "11111111111111" << endl;
    cout << "new thread exit code : " << (int64_t)res << endl;
    return 0;
}

在这里插入图片描述

新线程是可以取消主线程的,但是当你干掉主线程之后,主线程退出但是进程并没有退出,还剩下一个线程在跑,进程的状态为Zl 表示不可被回收,因为此时其内部还有线程在运行。

线程分离

int pthread_detach(pthread_t thread);

🚀默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放其资源,从而造成内存泄漏。
🚀 如果主线程不关心新线程的返回值,那么我们可以告诉操作系统,当线程退出的时候,自动释放线程的资源。

线程可以自已分离自己,也可以主线程分离新线程,也可以是其他进程分离目标进程,推荐是主线程分离新线程。

一旦线程被分离后,其属性就不再是joinable的,不能再对其join,否则会出错。

void *thread_run(void *args)
{
    int cnt = 6;
    while (cnt)
    {
        cout << "i am a new thread" << endl;
        sleep(1);
        cnt--;
    }

    pthread_exit((void *)0);
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, thread_run, nullptr);

    // 分离线程
    pthread_detach(tid);

    int n = pthread_join(tid, nullptr);
    if (n != 0)
    {
        cout << "join fail : " << strerror(n) << endl;
    }
    return 0;
}

在这里插入图片描述

五,原生线程库的理解

用户级线程库的理解

🚀由于Linux操作系统没有真正的线程,只有轻量级进程,但是我们用户使用的是线程,所以原生线程库就是对轻量级进程的系统调用的封装,从而方便用户使用线程。

在这里插入图片描述
🚀由于我们使用的pthread库是一个第三方库,那么pthread库会被加载到进程地址空间的共享区,并且这个库内势必要对线程做管理,那么就要对线程先描述在组织,所以库内部会有类似于TCB这样的结构体对线程描述,然后将一个个这样的结构体,用某种数据结构组织起来。
🚀 类似于TCB的结构体内,包含 struct pthread(里面存放的是线程的属性信息,以及线程独立栈的大小等等。),线程局部存储,线程独立的栈结构等等。并且会将线程的数据,栈结构等传递给轻量级进程的系统调用去运行的。

线程id理解

🚀所谓线程的id(这里谈的是库级别的,不是LWP)其实就是库中对于线程描述的结构体的起始地址,就作为线程的id。

string toHex(pthread_t id)
{
    char buffer[128];
    snprintf(buffer, sizeof(buffer), "0x%x", id);
    return buffer;
}
void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "new thread id : " << toHex(pthread_self()) << endl;
        sleep(1);
        cnt--;
    }
    pthread_exit((void *)0);
}
int main()
{
    pthread_t tids[3];
    for (int i = 0; i < 3; i++)
    {
        pthread_create(tids + i, nullptr, thread_run, nullptr);
    }

    for (int i = 0; i < 3; i++)
    {
        pthread_join(tids[i], nullptr);
    }
    return 0;
}

在这里插入图片描述

线程独立栈结构

🚀进程的地址空间中存在栈结构,但是这个栈是主线程使用的,当有多个线程并发调度的时候,如果都是用这一个栈结构,那就会十分混来很难管理。所以没有线程都有属于其自己的独立栈结构,并且这个栈结构是在线程库中维护的。

🚀线程内部创建的所有局部变量,都是存放在线程独立栈上的。

void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "new thread id : " << toHex(pthread_self()) << " &cnt " << &cnt << endl;
        sleep(1);
        cnt--;
    }
    pthread_exit((void *)0);
}

在这里插入图片描述
🚀 如何理解同一个变量名但是对应的地址不同?

这是在线程函数内部创建的局部变量,是创建在线程的独立栈的结构上的,每个线程各自私有一份,所以看到的地址是不同的。

在这里插入图片描述

局部变量的创建都是转换为代码,当程序运行的时候去开辟的,并且是通过与rbp(栈底指针)的距离,来确定某个局部变量的地址的,由于每个线程都有独立的栈结构,所以当调度一个线程的时候,rbp和rsp指向的是线程独立栈的栈底和栈顶,所以在创建局部变量的时候,用通过rbp - 偏移量的形式完成的,所以这个局部变量虽然名字相同,但是在每个线程中的地址是不同的。

线程的局部存储

🚀线程的局部存储,就是将全局变量让每个线程私有一份,成为线程内部的全局变量。在gcc/g++中可以通过__thread 修饰去全局变量来完成。

__thread int g_val = 100;
void *thread_run(void *args)
{
    int cnt = 5;
    while (cnt)
    {
        cout << "g_val : " << g_val << " &g_val : " << &g_val << endl;
        sleep(1);
        cnt--;
    }
    pthread_exit((void *)0);
}

在这里插入图片描述
🚀可以看到,g_val全局变量确实被每个线程各自私有一份,是存储在线程的局部存储空间上的。

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

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

相关文章

python+vue宠物用品商城网站系统3zy71

依照这一现实为基础&#xff0c;设计一个快捷而又方便的网上宠物管理系统是一项十分重要并且有价值的事情。对于传统的宠物管理控制模型来说&#xff0c;在线宠物管理系统具有许多不可比拟的优势&#xff0c;首先是快速更新宠物分类、宠物信息、热销排行榜、宠物寄养&#xff0…

【嵌入式烧录/刷写文件】-1.8-S19文件转换为Hex文件

案例背景(共5页精讲)&#xff1a; 有如下一段Motorola S-record(S19/SREC/mot/SX)文件&#xff0c;将其转换为Hex文件。 S0110000486578766965772056312E30352EA6 S123910058595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F70717273747576775B S123912078797A7B7C7D7E7F8081…

CodeWhisperer 初体验

今年算是 AI 正式破圈的一年&#xff0c;无数的工具&#xff0c;产品横空出世。无论在面向企业的大语言模型&#xff0c;还是帮助个人的 AI 工具&#xff0c;数不胜数。其中关于 AI 编程助手领域&#xff0c;近年来也涌现了很多不错的产品&#xff0c;例如 Copilot, Cursor, 还…

网络爬虫技术在搜索引擎中的应用

网络爬虫技术在搜索引擎中扮演着非常重要的角色&#xff0c;主要应用在以下几个方面&#xff1a; 网页抓取&#xff1a;搜索引擎需要从互联网上抓取大量的网页&#xff0c;以建立自己的索引库。网络爬虫技术可以帮助搜索引擎快速、高效地抓取网页。 网页解析&#xff1a;搜索引…

vue 自适应的方法

1、使用 filter来处理。 2、使用vue3.x中的 filter &#xff08;&#xff09;方法&#xff0c;但是要注意 filter &#xff08;&#xff09;方法的返回是一个字符串&#xff0c;在进行渲染时可能会有问题。 3、使用 react. js中的 require &#xff08;&#xff09;方法&#x…

实时数仓中数据实时输出的思考与实现

随着数据量不断增长以及提升企业竞争力的需求增长&#xff0c;实时数仓已经成为了许多业务和组织的重要数据架构之一。在实时数仓中&#xff0c;数据实时输入和数据实时分析是关键步骤&#xff0c;但同样重要的是如何将处理后的数据输出到各种目标上。本文将探讨实时数仓中数据…

亚马逊正常购物下单流程是怎么样的?

当您想要在亚马逊上购物时&#xff0c;您可以按照以下步骤进行&#xff1a; 1、登录亚马逊账户&#xff1a;在亚马逊的官方网站上&#xff0c;使用您的亚马逊账户进行登录。如果您还没有账户&#xff0c;可以在网站上注册一个新账户。 2、浏览商品&#xff1a;在亚马逊首页上&…

CASAIM受邀参加广东省科学院幼儿园举行的第二届STEAM科技节暨庆“六一”科普嘉年华活动

今年6月1日是第63个“六一”国际儿童节&#xff0c;恰“接棒”第7个全国科技工作者日。CASAIM受邀参加广东省科学院幼儿园举行的第二届STEAM科技节暨庆“六一”科普嘉年华活动&#xff0c;展示高精度的三维扫描和3D打印技术&#xff0c;为广大儿童送上精彩的科普大礼。 从小朋友…

视图及其缩放

相机控制 在本课中&#xff0c;您将学习如何通过使用 ViewCube 更改模型视图来检查您的设计。 学会环顾四周 在创建设计时&#xff0c;能够从各个方面看到您的设计会有所帮助。 让我们了解如何更改视图。 你能旋转你的视图看看谁在幕后吗&#xff1f;ViewCube&#xff08;…

最新office365个人和家庭版下载及功能介绍

到了台新联想笔记本&#xff0c;想试试随机带的office365&#xff0c;才发现要有microsoft账户&#xff0c;要重新注册账号。 搞了一圈&#xff0c;很麻烦&#xff0c;发现微软登陆帐号时一直在转圈圈&#xff0c;而无法完成登录。 大概率还是因为服务器在海外的原因。 于是索性…

【企业化架构部署】Apache配置与应用

文章目录 一、构建虚拟web主机1.概述2.httpd服务支持的虚拟主机类型3.构建虚拟Web主机3.1基于域名的虚拟主机3.2基于IP地址的虚拟主机3.3基于端口的虚拟主机 4.Apache连接保持5.Apache访问控制 二、Apache日志管理rotatelogs分隔工具 三、总结1.Web虚拟主机部署步骤2.网页根目录…

【UE5 新手向】网络同步1 —— 开启 Actor 的位置网络同步

新建一个第三人称 C 项目 在播放设置中&#xff0c;将 Number of Players 改为2&#xff0c;并将 Net Mode 改为Play As Listen Server。 播放游戏&#xff0c;可以发现角色默认开启了同步。 停止播放&#xff0c;选中场景中的某个物体。 在 Details 面板选择新建蓝图。 选…

chatgpt赋能python:Python编程实现文件备份功能

Python编程实现文件备份功能 数据对于任何企业都是极其重要的。文件备份是数据备份的一种重要形式。在发生系统损坏、恶意攻击、误操作等情况时能够帮助我们恢复数据。本文将介绍如何使用Python编程实现文件备份功能。 备份的重要性 对于企业而言&#xff0c;数据备份就像是…

clang 01. clang driver流程分析

文章目录 前言在这里简要概述一下clang的流程 1.clang driver代码分析1.1创建诊断&#xff08;DIagnosticsEngine&#xff09;实例1.2创建Driver(clang::driver::Driver)的实例1.3通过Driver的BuildCompilation方法生成需要执行的命令1.4Jobs构建完成&#xff0c;通过Driver的E…

OS-文件管理1-文件-文件的逻辑结构与物理结构。

一&#xff0c;文件管理 关键词&#xff1a;如何组织及提供的功能。 二&#xff0c;文件-文件基本概念。 1.文件&#xff0c;记录&#xff0c;数据项 2.文件属性 三&#xff0c;文件-文件控制块FCB与索引结点。 文件控制块FCB&#xff1a;用来存放控制文件需要的各种信息…

在软件定义汽车的时代,低代码究竟给车企数字化转型带来了什么?

前言&#xff1a; 软件定义汽车&#xff08;Software Defined Vehicles, SDV&#xff09;&#xff0c;是由百度自动驾驶事业部总经理王劲提出的概念。其核心思想是&#xff0c;决定未来汽车的是以人工智能为核心的软件技术&#xff0c;而不再是汽车的马力大小&#xff0c;是否…

Java并发体系-第三阶段-JUC并发包-[1]

AtomicXXXFieldUpdater 算是一个小补充 简介 public class AtomicIntegerFieldUpdaterTest {public static void main(String[] args) {AtomicIntegerFieldUpdater<Test> updater AtomicIntegerFieldUpdater.newUpdater(Test.class, "value");Test ts new T…

Maven处理依赖冲突

1.java常用的包依赖异常有&#xff1a; 1&#xff09;AbstractMethodError 2&#xff09;NoClassDefFoundError 3&#xff09;ClassNotFoundException 4&#xff09;LinkageError Maven会根据pom文件中的groupId、artifactId、version来判断jar是否冲突 如果出现了同名不…

开发软件必须写代码?来看smardaten如何零代码开发学生管理系统

一、前言 互联网产品在我们的生活中无处不在&#xff0c;但你知道开发一个这样的产品需要的成本有多大吗&#xff1f; 传统的产品研发模式是&#xff1a;功能需求&#xff0c;需要调研&#xff0c;画原型&#xff0c;开发&#xff0c;测试&#xff0c;上线&#xff0c;跟踪运…

基于Python+OpenCV的图像搜索引擎(CBIR+深度学习+机器视觉)含全部工程源码及图片数据库下载资源

目录 前言总体设计系统整体结构图系统流程图 运行环境模块实现1. 数据预处理2. 定义图像描述符3. 索引化数据集4. 设计搜索引擎内核5. 执行搜索 系统测试1. 处理数据集2. 执行搜索 工程源代码下载其它资料下载 前言 本项目旨在开发一套完整高效的图像搜索引擎&#xff0c;为用…