Linux -- 线程概念和控制

一 什么是线程

1.1 线程的引出

  我们开始理解一下Linux中的线程。我们以前说过,一个进程被创建出来,要有自己对应的进程PCB的,也就是 task_struct,也要有自己的地址空间、页表,经过页表映射到物理内存中。所以在进程角度,我们是通过地址空间的映射看到操作系统给予我们的资源,所以地址空间是进程的资源窗口,地址空间也是进程的资源

  以前我们谈的进程,它所创建的地址空间内的所有资源,都是由一个叫 task_struct 所享有的,那么页表也是属于它独有的。那么如果我们再创建一个“进程”,但是不再给这个“进程”创建新的地址空间和页表,它只需要在创建时指向“父进程”的地址空间。将来“父进程”就将代码区中的代码分一部分给这个“子进程”,以及其它数据分一部分给它,此时我们就可以让“父进程”在运行的时候“子进程”也在运行。那么该父进程能创建一个,就能创建很多个,如下图:

   那么我们新创建出来的“子进程”,它们在执行粒度上要比“父进程”的执行粒度要更细一些,因为以前“父进程”需要执行全部代码,而这些“子进程”只需要执行一部分代码,所以,为了明显区分这些“子进程”和“父进程”,我们把这种形式的“子进程”,称为线程

  所以在 Linux 中,线程在进程“内部”执行,也就是线程在进程的地址空间内运行。那么它为什么要在进程的地址空间内运行呢?首先,任何执行流要执行,都要有资源!而地址空间是进程的资源窗口,但是线程有地址空间吗,没有,因为他并没有建立与内存的联系,线程需要依附于进程!

  那么在 CPU 看来,它知道这个 task_struct 是进程还是线程吗?它需要知道吗?并不需要!因为CPU只有调度执行流的概念!

  这样,我们的多个新task_struct都可以被CPU调度,不再像以前一样只有进程被CPU调度,大幅提高了CPU的效率,并且这些task_struct的的创建成本明显比一个进程小的多,因此这些能被CPU调度却没有被操作系统分配资源的tack_struct,我们称为线程。 

  1.2 线程的定义

那么有了上面的基础,我们现在重新定义线程和进程的概念:

  线程—— CPU调度的最小单位

   所以什么到底什么是进程呢?我们以前说的进程等于 描述进程的结构体+ 代码数据,但是今天很显然已经有分歧了,因为它只是地址空间的一个执行分支,一个执行分支不能代表整个进程!那么我们现在需要重新理解一下了,全部 task_struct 执行流都叫做进程执行流,地址空间都叫做进程所占有的资源,页表和该进程所占用的物理内存,我们把这一整套才称之为进程!如下图:

  进程—— 操作系统分配资源的最小实体 

  那么执行流是资源吗?是的!所以不要认为一个进程能被调度,它就是进程的所有,它只是进程内部的一个执行流资源被CPU执行了!所以进程和线程之间的关系是:进程内部是包含线程的,因为进程是承担分配系统资源的基本实体,而线程是进程内部的执行流资源!

  那么如何理解我们以前学的进程呢?其实就是操作系统以进程为单位给我们分配资源,只是我们以前进程内部,只有一个执行流资源,也就是只有一个 task_struct!只是我们可以认为,以前我们学的进程只是进程的一种特殊情况!

二 线程管理

  那么既然操作系统要对进程管理,如果线程多起来了,操作系统要对线程管理吗?很明显,如果不对线程管理,那么线程就不知道自己属于哪个进程,更不知道应该执行哪个进程的代码,所以必须得对线程管理,所以需要先描述再组织进行管理!

  同时,我们以前也学过,进程有描述进程并管理的结构体,有些系统对线程也有类似的结构:

  所大多数操作系统都是对线程重新进行先描述再组织,重新为线程建立一个内核数据结构对线程管理起来,而这个结构叫做 struct tcb;除此之外还要把进程和线程之间关联起来。实际上这样做太复杂了,维护的关系太复杂了。

  在Windows操作系统,内核中有真线程,名为TCB :线程控制块。因为 TCB 属于 PCB,所以还需要维护进程与线程之间的调度关系算法,这过于复杂。

  在Linux中,由于线程的控制块与进程控制块相似性非常高,所以直接复用了PCB的结构体,用PCB模拟线程的TCB。所以Linux没有真正意义上的线程,而是用进程方案模拟的线程。这样做的好处是复用代码和结构更简单,好维护,效率更高,也更安全。

三 页表的补充说明

  我 们上面的进程中,创建线程后给线程分配一部分代码和数据,也就是资源,那么我们应该如何理解基于地址空间的多个执行流分配资源的情况呢?怎么知道哪部分资源给哪个线程呢?接下来我们基于地址空间理解一下。

  首先CPU里面有一个CR3寄存器,它会保存页表的地址,方便找到进程的页表。我们也知道,物理内存被分为许多的页框,每个页框的大小为 4KB。下面我们理解一下虚拟地址是如何转换为物理地址的,我们以32位的计算机为例,也就是虚拟地址也是32位的。

  接下来我们展开说一说页表。首先,页表不是一个整体,我们假设页表是一个整体,就单单是一个映射关系,如下图,每一列分别是虚拟地址、物理地址、权限,假设每一行就10个字节,单单这一个页表建立整个虚拟空间的地址映射关系就需要有 2^32 个映射条目,这样算下来这个页表就已经几十G了,所以页表不可能是这个形式的。

  其实 32 位的虚拟地址不是一个整体,其实是将它分为了 10 + 10 + 12,其中 10 + 10 分别代表一级和二级目录。

  其中第一级页表,只有 1024 个条目,也就是一个数组,因为用 10 个比特位表示的最大值就是 1024,所以这 10 个比特位代表的十进制数就是该一级页表的下标,而一级页表中存放的是二级页表的地址,所以只需要拿着前十位找到二级页表的地址,找到二级页表,然后拿着次十位,也是 10 个比特位,把它转为十进制数,然后在二级页表中索引它的下标,那么二级页表中存的是什么呢?存的是页框的起始地址!如下图:

 

  其实这个一级页表就叫做页目录,我们把页目录里面的内容叫做页目录表项;把二级页表里面的内容叫做页表表项。所以我们就能通过虚拟地址的前 20 位找到物理内存中页框的起始地址。

  那么剩下的 12 位呢?那么我们知道 2^12 的大小刚好就是 4096,如果取字节为单位,也就是页框的大小!所以剩下的 12 个比特位就是作为某个物理地址的页框中的偏移量!也就是说,物理地址 = 页框起始地址 + 虚拟地址的最后12位!所以这就是虚拟地址到物理地址转换的过程!

  在正常情况下,我们不可能将虚拟空间全部用完,所以二级页表也不一定全部存在。所以当需要访问一个虚拟地址时,怎么知道这个虚拟地址在不在物理内存中呢?就有可能在查页目录的时候,它的二级页表的目录根本就不存在,说明就没有被加载到内存,这个时候就是缺页中断。另外,也有可能二级页表和页框没有建立映射关系,在二级页表中还有一个字段中的标记位会记录页框是否存在。

  那么就有一个问题了,我们通过页表找到的是物理内存的某一个地址,可是对于某一个类型,可能是 intdouble 等等,我们并不是访问一个字节呀,对于上面两种类型我们访问的是 4、8 个字节啊。这时候,就能体现了类型的价值!例如一个整型变量 a,占4个字节,就要有4个地址,但是为什么我们 &a,只拿到了一个地址?因为我们只能取一个地址,那么4个地址中只能取最小的那一个,由于有类型的存在,我们只要从下往上连续读取 4 个字节就能找到它了!也就是根据起始地址+偏移量读取该变量。那么CPU怎么知道根据什么类型读取多少字节呢?其实类型是给 CPU 看的,CPU在读取类型时,是知道有多少字节的!我们根据软件帮CPU找到起始地址,接下来CPU就要读取内存,读的过程把物理内存在硬件上拷贝给CPU,拷贝的时候CPU就知道拷贝多少字节了!

  所以我们上面说的 CR3 寄存器中,指向的其实是页目录的地址,任何一个进程必须得有页目录。如果对物理地址进行访问的时候,如果物理地址不存在,或者越界了,CPU 中的 CR2 寄存器,保存的是引起缺页中断或者异常的虚拟地址,完成建立物理地址后就会去 CR2 取回对应的虚拟地址。

  最后,我们谈上面的内容都是为了理解如何进行资源分配的,线程的资源全部都是通过地址空间来的,而代码和数据都是通过地址空间+页表映射来的,所以线程分配资源的本质,就是分配地址空间范围!

四  线程和进程的切换

为什么线程比进程要更轻量化呢?

  • 创建和释放更加轻量化
  • 切换更加轻量化

线程切换时,线程的上下文肯定是要切换的,但是,页表不需要切换,地址空间不需要切换,所以线程在切换的时候,只是局部在切换,所以线程切换的效率更高。

  线程在执行,本质就是进程在执行,因为线程是进程的执行分支。线程在执行本质就是进程在调度,CPU内有一个硬件级别的缓存,叫做 cachecache 也是根据局部性原理,将线程/进程当前访问的代码附近的代码都加载到 cache 中,所以在进程调度的时候它应该会越跑越快,因为它的命中率会越来越高,这部分 cache 我们称为进程运行时的热数据,热数据就是这部分数据被高频访问,所以CPU在硬件上它就会把对应的数据加载到 cache 里。

  所以在调度的时候,它切换的是一个进程中的多个线程,那么它在切换的时候,此时上下文虽然一直在变化,但是 cache 里的数据一直不变,或者少量的更新,因为每一个线程很多属性都是共享的,就是为了让多个线程同时访问,所以数据就可以在一个进程内部的多个线程互相调度的时候,CPU当前 cache 中的数据就可以被多个线程用上,所以在线程切换的时候,只需要切换线程,不需要对 cache 保存。但是当线程的所有时间片用完了,整个进程也要被切换,CPU寄存器要保存,最重要的是,热缓存数据需要被丢弃掉,把另一个进程放上来,需要重新缓存 cache 中的数据,就要需要由冷变热,这就需要一段时间。所以线程切换的效率更高,更重要的是体现在 cache 数据不需要重新被缓存!

五 线程的优缺点

5.1 线程的优点

  1. 创建一个新线程的代价要比创建一个新进程小得多
  2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  7. 线程不是越多越好,正常情况下最合适的原则是:进程/线程与cpu个数/核数保持一致
  8. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
  9. 线程不是越多越好,但是比计算密集型应用可以多一点

5.2 线程的缺点

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

六 线程异常

  如果一个线程出现除零错误或者是野指针这种类似的异常,就会导致线程崩溃,但同时,对应的进程也会崩溃。

  线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

七 线程作用

  • 合理的使用多线程,能提高CPU密集型程序的执行效率。
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。

 八 线程和进程

  1. 进程是操作系统资源分配的基本单位
  2. 线程是CPU调度的基本单位
  3. 线程共享进程数据,但也拥有自己的一部分数据。

  线程拥有的数据:线程ID,一组寄存器(线程上下文),栈,errno,信号屏蔽字,调度优先级。

  进程的多个线程共享同一地址空间,因此代码区数据区都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。除此之外,各线程还共享以下进程资源和环境:

   文件描述符表,每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数),当前工作目录,用户 id 和组 id

 九 线程控制 

9.1 pthread线程库

  因为 Linux 中没有专门为线程设计一个内核数据结构,所以内核中并没有很明确的线程的概念,而是用进程模拟的线程,只有轻量级进程的概念。这就注定了 Linux 中不会给我们直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用!

  可是我们用户需要线程的接口,所以在用户和系统之间,Linux 开发者们给我们开发出来一个 pthread 线程库,这个库是在应用层的,它是对轻量级进程的接口进行了封装,为用户提供直接线程的接口!虽然这个是第三方库,但是这个库是几乎所有的 Linux 平台都是默认自带的!所以在 Linux 中编写多线程代码,需要使用第三方库 pthread 线程库!

  这样一来,就不必在Linux中进行线程和进程的区分,但是在我们程序员眼中,就有了线程和进程两个概念,并且可以使用线程。

  注意:因为pthread线程库是一个第三方库,最后我们在编译的时候需要加上 -lpthread 指定库名称。

9.2 线程的创建

pthread_create函数

函数原型:

 #include <pthread.h>int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);


   其中,第一个函数是一个输出型参数,一旦我们创建好一个线程,我们是需要线程ID的,该参数就是带出线程id

  个参数attr为线程的属性,一般不用关心,设置为nullptr即可

   第三个参数是一个函数指针类型,当我们创建线程时,我们一般是想让线程去执行当前进程的一部分代码和数据,这个函数指针就是让我们的线程去执行的部分代码,当我们传入一个指针时,线程一启动就会转到执行该指针指向的内容。同时,它的类型还是void*类型,因为void*类型可以接收或者返回任意指针类型,这样就可以支持泛型。

  第四个参数arg是一个输入型参数,当线程创建成功的时候,新线程内部执行的函数如果需要参数,就用arg传入函数,也就是说 该参数是给第三个参数函数指针传入的。

  而函数的返回值,如果我们创建成功就返回0;如果失败会返回错误码,而没有设置 errno.

示例代码:

 这里我们创建一个线程,并给他传入数字1,让新线程打印数字1,同时让新线程和主线程打印自己的pid。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
using namespace std;void* pthread_handl(void* arg){while(1){printf("我是一个线程,我的pid为:%d,传入数据为:%d\n",getpid(),*(int*)arg);sleep(1);}}int main(){pthread_t id;int i=1;pthread_create(&id,NULL,pthread_handl,(void*)&i);while(1){printf("我是一个主线程,我的pid为:%d\n",getpid());sleep(1);}return 0;
}

9.3 线程的查看 

如上图,我们以前写的代码中是不可能出现两个死循环的,但是使用创建线程之后就可以了,这就说明它们是不同的执行流。而它们的 pid 是一样的,就说明它们是同一个进程。

  那我们该如何查看不同的线程呢?他们归属于同一个进程,根据上图也可以看出,单纯的用pid是无法区分出线程的,因此,我们引入了线程标识符  --  LWP

 在linux中,我们可以通过  ps - aL 查看线程的LWP。

如下图示例:

(还是以前的代码)

  在我们右侧终端中,正在查看两个执行流,我们上面循环打印 了方便观察,我们看到 pid 是一样的,但是 LWP 的准确定义到底 是什么呢?

   在 Linux 中没有具体的线程概念,只有轻量级进程的概念,所以 CPU 在调度时,不仅仅只要看 pid,更重要的是每一个轻量级进程也要有自己对应的标识符,所以轻量级进程就有了 LWP (light weight process)这样的标识符,所以 CPU 是按照 LWP 来进行调度的!

  但是我们如果杀掉上面任意一个执行流的 LWP,默认整个进程都会被终止,这就是线程的健壮性差的原因。

  如果我们定义一个函数,或者全局变量,分别在两个执行流中执行,它们都可以读取到该函数和全局变量,如下代码:  

  

#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<string>
using namespace std;const int N=10;void Print(const string &s){cout<<s<<endl;
}void* pthread_handl(void* attr){while(1){Print("子线程测试打印函数");cout<<" 子线程得到N:"<<N<<endl;sleep(1);}
}int main(){pthread_t tid;pthread_create(&tid,nullptr,pthread_handl,nullptr);while(1){Print("父线程测试打印函数");cout<<"父线程得到N:"<<N<<endl;sleep(1);}return 0;
}

9.4 线程的等待

  那么创建线程后是主线程先运行还是新线程先运行呢?不确定,要看CPU先调度谁,那么肯定的是主线程是最后退出的!因为主线程退了整个进程就退出了,所以主线程要进行线程等待!如果主线程不进行线程等待,会导致类似于僵尸进程的问题!而 pthread_join() 就是进行线程等待的接口。 

  其中第一个第一个参数,为等待线程的 id

第二个参数 retval 我们先不管,其实我们给线程分配的函数,它的返回值是直接写入 pthread 库中的,而 retval 也是被封装在库中,所以我们可以根据 retval 读取到函数的返回值,也就是说这个 retval 就是一个输出型参数!首先我们需要定义一个 void* 类型的变量,然后将这个变量取地址当作 pthread_join 的第二个参数传入即可!

下面我们简单写一个程序:

#include<iostream>
#include<unistd.h>
#include<pthread.h>using namespace std;void *pthread_handl(void *arg){int cnt = 5;while(cnt--){cout << "我是新线程"  << endl;sleep(1);}return (void*)1234;
}int main(){pthread_t tid;pthread_create(&tid, nullptr, pthread_handl, nullptr);void* retval;pthread_join(tid, &retval);cout << "进程退出, retval = " << (long long)retval << endl;return 0;
}

我们可以看到当新线程在运行的时候,主线程并没有直接运行结束,而是进行阻塞等待,同时我们也看到函数返回值1234.

9.5 线程的退出

  注意,我们在单个线程退出时,使用的exit等函数,会使整个进程一起退出,因为线程始终是进程的一部分,这种停止进程的函数不可能缩小范围去停止单个线程。

  那么除了在函数中直接 return 终止线程外,还有什么方法吗?有的,pthread_exit() 接口就是用来终止线程的:

例如:

  

#include<iostream>
#include<unistd.h>
#include<pthread.h>using namespace std;void *pthread_handl(void *arg){int cnt = 5;while(cnt--){cout << "我是新线程"  << endl;sleep(1);}pthread_exit((void*)1234);
}int main(){pthread_t tid;pthread_create(&tid, nullptr, pthread_handl, nullptr);void* retval;pthread_join(tid, &retval);cout << "进程退出,子线程退出码为:retval = " << (long long)retval << endl;return 0;
}

  

 9.6 线程的取消

   pthread_cancel() 也可以取消一个线程,参数就是目标线程的 id

 

#include<iostream>
#include<unistd.h>
#include<pthread.h>using namespace std;void *pthread_handl(void *arg){int cnt = 5;while(cnt--){cout << "我是新线程"  << endl;sleep(1);}pthread_exit((void*)1234);
}int main(){pthread_t tid;pthread_create(&tid, nullptr, pthread_handl, nullptr);void* retval;sleep(3);pthread_cancel(tid);pthread_join(tid, &retval);cout << "进程退出,子线程退出码为:retval = " << (long long)retval << endl;return 0;
}

如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉, pthread_join 第二个参数 retval 所指向的单元里存放的是常数PTHREAD_ CANCELED,也就是 -1.

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

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

相关文章

基于java ssm springboot女士电商平台系统

基于java ssm springboot女士电商平台系统源码文档设计 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言 文末…

WebPack自动吐出脚本

window.c c; window.res ""; window.flag false;c function (r) {if (flag) {window.res window.res "${r.toString()}" ":" (e[r] "") ",";}return window.c(r); }代码改进了一下&#xff0c;可以过滤掉重复的方…

web基础05-jQuery

目录 一、jQuery 1.概述 2.原生js与jQuery对比 3.特点 4.使用 &#xff08;1&#xff09;入口函数 &#xff08;2&#xff09;语法 &#xff08;3&#xff09;jQuery选择器 5.方法 &#xff08;1&#xff09;获取属性值&#xff1a; &#xff08;2&#xff09;删除属…

WinForm 修改TableControl背景和标签

在界面设计中&#xff0c;TableControl控件经常使用。默认设置中&#xff0c;TabControl的背景和标签样式。接下来我们将学习如何修改TableControl的标签选项、修改TabControld的背景色或背景图片。页面效果如下&#xff1a; 简述原理 TableControl项目属性DrawMode&#xff0c…

【开源工程】数字孪生工厂~工业上楼解决方案

飞渡科技数字孪生轻工厂管理平台&#xff0c;基于数字孪生技术驱动的智能&#xff0c;结合物联网IOT实现的联接&#xff0c;以及大数据分析生成的融合共享数据&#xff0c;实现生产过程的智能化监控和管理&#xff0c;实现设备之间的互联互通和协同工作。 通过智能算法&#xf…

前端报错404,nginx正常、gateway没有转发请求

问题描述&#xff1a;前端报错 404 Not Found 原因&#xff1a;nacos中对应服务没有上线&#xff0c;下线后&#xff0c;可以启动本地服务&#xff0c;然后在测试上调试代码。&#xff01;&#xff01; 记住重启对应服务&#xff0c;也不会自动上线。

简单了解TCP/IP四层模型

什么是计算机网络&#xff1f; 计算机网络我们可以理解为一个巨大的城市地图&#xff0c;我们想从A地前往B地&#xff0c;其中要走的路、要避开的问题都交给计算机网络解决&#xff0c;直到我们可以正常的到达目的地&#xff0c;那么我们会把其中的过程抽象成一个网络模型&…

【计算机考研】408究竟有多难?

408的难点在于他涉及的范围太广了&#xff0c;备考408&#xff0c;你要准备四门课程&#xff0c;分别是数据结构&#xff0c;计算机组成原理&#xff0c;操作系统和计算机网络。 这四门课程的书加起来很厚&#xff0c;需要复习的知识点很多&#xff0c;虽然408有考纲&#xff…

PostgreSQL - 查看表膨胀空间

目录 使用pgstattuple插件查看表膨胀空间 死元组&膨胀系数清理 查看表占用磁盘空间大小是如何组成的 什么是fms和vm&#xff1f; 什么是TOAST&#xff1f; 查看表和其关联的TOAST表的oid的关系 方法一 方法二 参考文档 使用pgstattuple插件查看表膨胀空间 select…

<Linux> 初识线程

目录 前言&#xff1a; 一、什么是线程 &#xff08;一&#xff09;基本概念 &#xff08;二&#xff09;线程理解 &#xff08;三&#xff09;线程与进程的关系 &#xff08;四&#xff09;简单实用线程 &#xff08;五&#xff09;重谈虚拟地址空间 1. 页表的大小 2…

检测虚拟机环境的常见技术

下面列出检测 VMware 虚拟机的常见技术&#xff1a; #include <iostream> #include <windows.h> #include <sysinfoapi.h> #include <comdef.h> #include <Wbemidl.h> #include <ShlObj.h> #include <LM.h> #include <TlHelp32.…

【C++】了解一下STL

个人主页 &#xff1a; zxctscl 如有转载请先通知 STL 1. 什么是STL2. STL的版本3. STL的六大组件4. STL的重要性5. 如何学习STL6. STL的缺陷 1. 什么是STL STL(standard template libaray-标准模板库)&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件…

Php和h5等静态文件的服务容器化部署(下)

一、接着上文 上文介绍了php/h5程序的部署过程&#xff0c;最后是通过slb把不同的服务暴露给外部。 本文试着把外部的配置交待清楚&#xff0c;包括&#xff1a; kong配置ingress配置 部署逻辑图见下&#xff1a; 总结&#xff1a; 去掉slb&#xff0c;引入ingress组件。…

蓝桥杯真题讲解:接龙序列

蓝桥杯真题讲解&#xff1a;接龙序列 一、视频讲解二、暴力代码三、正解代码 一、视频讲解 蓝桥杯真题讲解&#xff1a;接龙序列 二、暴力代码 // 暴力代码&#xff1a;DFS&#xff08;2^n&#xff09; #include<bits/stdc.h> #define endl \n #define deb(x) cout &…

零基础自学C语言|自定义类型:结构体

✈结构体类型的声明 前面我们在学习操作符的时候&#xff0c;已经学习了结构体的知识&#xff0c;这里稍微复习一下。 &#x1f680;结构体回顾 结构是一些值的集合&#xff0c;这些值称为成员变量。结构的每个成员可以是不同类型的变量。 &#x1fa82;结构的声明 例如&a…

李彦宏:程序员职业将不复存在,会说话就能当程序员;ChatGPT 日耗电超 50 万度丨 RTE 开发者日报 Vol.161

开发者朋友们大家好&#xff1a; 这里是 「RTE 开发者日报」 &#xff0c;每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE &#xff08;Real Time Engagement&#xff09; 领域内「有话题的新闻」、「有态度的观点」、「有意思的数据」、「有思考的文章」、…

中国社会科学院与美国杜兰大学金融管理硕士——二月二,抬头皆是惊喜

在繁忙的都市生活中&#xff0c;每个人都在为自己的未来打拼&#xff0c;寻找着属于自己的那片天空。二月二&#xff0c;龙抬头&#xff0c;象征着春天的到来&#xff0c;万物复苏。在这个特殊的日子里&#xff0c;对于那些追求学术与职业双重成就的人来说&#xff0c;&#xf…

AIGC——DreamTuner通过单张图片生成与该图片主题风格一致的新图像

简介 DreamTuner的能力在于从单个图像生成主体驱动的新通用方法&#xff0c;这意味着用户只需提供一张图片&#xff0c;DreamTuner就能帮助他们生成与原始图片在主题和风格上一致的新图像。 算法重要之处在于其通用性和个性化定制的能力。无论是需要根据特定主题或条件创建个…

【深度学习笔记】优化算法——学习率调度器

学习率调度器 &#x1f3f7;sec_scheduler 到目前为止&#xff0c;我们主要关注如何更新权重向量的优化算法&#xff0c;而不是它们的更新速率。 然而&#xff0c;调整学习率通常与实际算法同样重要&#xff0c;有如下几方面需要考虑&#xff1a; 首先&#xff0c;学习率的大…

cefsharp(winForm)调用js脚本,js脚本调用c#方法

本博文针对js-csharp交互(相互调用的应用) (一)、js调用c#方法 1.1 类名称:cs_js_obj public class cs_js_obj{//注意,js调用C#,不一定在主线程上调用的,需要用SynchronizationContext来切换到主线程//private System.Threading.SynchronizationContext context;//…