【线程概念和线程控制】

目录

  • 1 :peach:线程概念 :peach:
    • 1.1 :apple:什么是线程?:apple:
    • 1.2 :apple:线程的优点和缺点:apple:
    • 1.3 :apple:页表的大小:apple:
    • 1.4 :apple:线程异常和用途:apple:
    • 1.5 :apple:进程VS线程:apple:
  • 2 :peach:线程控制:peach:
    • 2.1 :apple:POSIX线程库:apple:
    • 2.2 :apple:创建线程:apple:
    • 2.3 :apple:线程退出:apple:
      • :lemon:pthread_join:lemon:
      • :lemon:pthread_exit:lemon:
      • :lemon:pthread_cancel:lemon:
    • 2.4 :apple:分离线程:apple:
    • 2.5 :apple:理解线程独立栈:apple:


1 🍑线程概念 🍑

1.1 🍎什么是线程?🍎

教材观点是这样的:线程是一个执行分支,执行力度比进程更细,调度的成本更低。
Linux内核观点:进程是系统分配资源的基本单位,线程是CPU调度的基本单位。

这两种说法都是正确的,但是我们究竟该如何理解线程呢?

在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。一切进程至少都有一个执行线程。线程在进程内部运行,本质是在进程地址空间内运行。在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

Linux中其实本质上并不存在线程,而是将轻量级进程作为线程。其本质就是OS并没有给线程创建自己独立的地址空间,而是与进程共用一套地址空间,那么这也就注定了线程中绝大部分资源是可以共享的。这样设计的好处是复用了PCB的那一套设计,线程的TCB可以用进程的PCB模拟出来,这样的设计更加简单并且维护效率更加高效,像服务器等开发选择Linux的原因就是因为Linux可以在长时间的服务中运行。Windows中的线程才算的上是一种严格的线程,Windows的线程并没有复用进程的方法,而是创造了真正意义上的线程。

我们可以用一张图来表示进程与线程的关系:
在这里插入图片描述

1.2 🍎线程的优点和缺点🍎

线程的优点

  • 1️⃣创建一个新线程的代价要比创建一个新进程小得多。
  • 2️⃣与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
  • 3️⃣线程占用的资源要比进程少很多。
  • 4️⃣能充分利用多处理器的可并行数量。
  • 5️⃣在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
  • 6️⃣计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
  • 7️⃣I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点:

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

1.3 🍎页表的大小🍎

这里再补充一个小知识点:多线程中页表关系是怎样维护建立的呢?

我们知道,在X86的环境下,我们最多可以拥有232种虚拟地址,而将虚拟地址转化成物理地址的页表大小应该为多少呢?
如果按照每一个虚拟地址都建立一个对应映射的话,假设用一个四字节的整形变量int来维护,那么不算其他的,只算虚拟地址到物理地址的映射,那么至少得需要232 *8(大约32GB),那我们操作系统还玩不玩了,这样设计肯定是不合理的。

实际上,操作系统将每一个32比特位的虚拟地址做了如下划分:
在这里插入图片描述这样进行页表大小计算时我们用的是前20个比特位,也就是220 ,然后通过下面方式进行页表映射:
在这里插入图片描述
那么最后12位到哪里去了呢?最后12位的虚拟地址是我们将虚拟地址转化位物理地址的偏移量,这个偏移量的大小恰好是212 (4KB),这个4KB是操作系统管理物理内存的单位,相信大家对于4KB一点儿也不陌生,因为我们讲解文件系统的时候磁盘与内存进行交互的单位也是以4KB位单位。这里为什么要使用4KB的大小进行交互而不是以字节进行交互呢?因为一个很著名的原理:局部性原理。通俗的来说,局部性原理就是预测未来CPU高速缓存的命中情况来提升效率。

所以通过这种方式我们只用了220 量级的大小空间来完成页表的建立,最多也就几MB而已,更何况并不是所有地址都会被用到,实际用到的地址可能只有几十字节大小。所以这种方式可以解决操作系统如何为页表分配合适的空间问题。
那么实际操作系统是如何分配资源给对象的呢?比如我们使用malloc一个资源是立马就会给你开空间的吗?显然不是这样的,操作系统为了高效是不会直接立马给你开空间的,而是产生一个缺页中断,当你真正使用该空间时才会去开空间。

1.4 🍎线程异常和用途🍎

线程异常:

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

这里我们可以简单的验证一下,大家先可以先看看时如何创建线程的,后面我们会详细的讲解:
比如下面的我们让线程1出错:
Makefile:

mytest:Test.cppg++ -o $@ $^ -std=c++11 -lpthread.PHONY:clean
clean:rm -rf mytest

Test.cpp:


#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;void *Run1(void *argv)
{int cnt = 4;while (true){cout << "I am t1,is running" << endl;sleep(1);if (--cnt == 0){char *str = "abcd";*str = 'Q';}}
}
void* Run2(void* argv)
{while(true){cout<<"I am t2,is running"<<endl;sleep(1);}
}
int main()
{pthread_t t1,t2;pthread_create(&t1,nullptr,Run1,nullptr);pthread_create(&t2,nullptr,Run2,nullptr);while(true){cout<<"I am main,is running"<<endl;sleep(1);}return 0;
}

这里面值得注意的细节:创建线程时要引入头文件<pthread.h> ;链接时为了能够找到库,在Makefile种要指定库的名称-lpthread,要查找指定进程的所有线程可以使用下面命令:ps -aL | grep 进程名称;想要显示更加详细信息可以使用下面命令:ps -aL | head -1 && ps -aL | grep 进程名称 ;为了方便观察我们可以使用下面的命令脚本:while :;do ps -aL | head -1 && ps -aL | grep mytest;echo "************************************";sleep 1;done
我们来运行下观察下结果:
在这里插入图片描述从图片中我们不难发现当其中一个线程崩溃而导致整个进程(所有线程)都挂掉了。

线程用途:

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

1.5 🍎进程VS线程🍎

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:
    1️⃣线程ID
    2️⃣一组寄存器
    3️⃣
    4️⃣errno
    5️⃣信号屏蔽字
    6️⃣调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1️⃣文件描述符表
2️⃣每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3️⃣当前工作目录
4️⃣用户id和组id

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


2 🍑线程控制🍑

2.1 🍎POSIX线程库🍎

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头;
  • 要使用这些函数库,要通过引入头文<pthread.h>;
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项.

2.2 🍎创建线程🍎

我们先来看看库中的基本介绍:
在这里插入图片描述

功能:创建一个新的线程
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg)
参数:
thread:返回线程ID;
attr:设置线程的属性,attr为nullptr表示使用默认属性;
start_routine:是个函数地址,线程启动后要执行的函数;
arg:传给线程启动函数的参数;
返回值:成功返回0;失败返回错误码

这里再补充一个错误检查的知识点:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。pthread函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回。
  • pthread同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小。

我们观察上面创建线程的接口中,第一个参数是线程的id,这个的数值与之前我们在用监视脚本看的线程PID不太一样,现阶段可以理解为同一线程的两种不同的身份形式(比如我们在学校的学生证和处于社会中的身份证类似);第二个参数我们一般设置为空;第三个是一个参数为void*,返回值为void的函数指针;第四个参数是一个void的对象,我们一般是将线程的信息通过该参数传递进去的。

我们根据上面介绍就可以写出如下代码:

#include<iostream>
#include<unistd.h>
using namespace std;void* Run(void* args)
{const char* name=static_cast<char*> (args);while(true){cout<<name<<"is running"<<endl;sleep(1);}return nullptr;
}int main()
{pthread_t pids[5];for(int i=0;i<5;++i){char name[26];snprintf(name,sizeof(name),"pthread%d:",i+1);pthread_create(pids+i,nullptr,Run,name);}while(true){cout<<"I am is main thread,is running"<<endl;sleep(1);}return 0;
}

当我们运行时:
在这里插入图片描述
为啥跟我们预计的不太一样呀?我们想要的是打印pthread1 pthread2……这样的数据呀,为啥打印出来的都是pthread5呢?
这其实与我们传入的数组有关:
在这里插入图片描述
我们在这里传入的是数组名,也就是首元素地址,我们传给线程创建的参数并不是一个缓冲区而是一个数组的地址,由于创建线程是先将线程创建出来,并不会立马去执行线程中的代码,而我们每次传入的地址(数组名)是相同的,所以最后一个线程的数据就被保存到了数组中,当我们并发执行线程中的代码时读到的就是数组中的数据(也就是最后一次修改数组中的数据)。那我们如何解决这种现象呢?
我们可以在堆上开辟空间,这样我们每次new出来的地址是不同的,所以就不会出现覆盖的情况了。

比如我们可以这样修改:
在这里插入图片描述
当我们再次运行时:
在这里插入图片描述
这里面打印顺序并不是1 2 3 4 5那样的原因是因为线程的调度也是不确定的,谁先调度完全是由调度器所决定的。

其实上面传入的对象大家可以更具需求设置的更加完善一些,我们可以封装一个类,让多线程帮助我们完成不同的任务,我这里就不再多写了,大家有兴趣可以根据自己的需求下去完善。

线程ID及进程地址空间布局:

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
  • 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID.

2.3 🍎线程退出🍎

🍋pthread_join🍋

但是其实上面的代码中还存在一个很严重的问题,我们在学习进程中知道,父进程会wait子进程,否则就可能造成了内存泄漏。线程也是一样的,已经退出的线程,其空间没有被释放,仍然在进程的地址空间内;创建新的线程不会复用刚才退出线程的地址空间。主线程必须要回收其他线程的资源,否则就会造成内存泄漏,那回收其他线程的接口是啥呢?
我们来看看官网对pthread_join的介绍:
在这里插入图片描述

功能:等待线程结束
原型:
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

第一个参数比较好理解,那么第二个参数是一个二级指针,接受的是一个线程的返回值,那么我们知道创建线程的参数里面有一个函数指针,该函数指针的返回值为void*,而这个返回值就可以传递给join的第二个参数使用,比如我们看看下面的代码:

void *Run(void *args)
{const char *name = static_cast<char *>(args);cout << "thread1 is running" << endl;sleep(2);return (void *)11;
}int main()
{pthread_t p1;pthread_create(&p1, nullptr, Run, nullptr);cout << "I am is main thread,is running" << endl;sleep(2);void* ret=nullptr;pthread_join(p1,&ret);cout<<"new pthread exit   "<<ret<<endl;return 0;
}

当我们运行时:
在这里插入图片描述
我们发现我们通过返回值返回的11被join给接收到了。

🍋pthread_exit🍋

除了使用return 这种方式,我们还可以使用哪种方式终止线程呢?我们还可以使用pthread_exit接口来处理:
在这里插入图片描述

功能:线程终止
原型:
void pthread_exit(void *value_ptr);
参数:
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

当我们这样使用时:
在这里插入图片描述
我们可以来观察下运行结果:
在这里插入图片描述

🍋pthread_cancel🍋

除了上面我们讲解的这两种方式外,我们还可以使用pthread_cancel取消一个执行中的线程:

功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:成功返回0;失败返回错误码

假如我们想要取消自己呢?我们如何得到自己的pid,我们可以使用pthread_self():
在这里插入图片描述
我们下面来看看线程取消的基本用法:

void *threadRun(void* args)
{const char*name = static_cast<const char *>(args);int cnt = 5;while(cnt){cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;sleep(1);}pthread_exit((void*)11); // PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");sleep(3);pthread_cancel(tid);void *ret = nullptr;pthread_join(tid, &ret);cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;return 0;
}

当我们直接运行时:
在这里插入图片描述

不难发现程序3s后直接退出了,其实也很好理解,因为我们退出的是主线程,所以肯定会直接退出的。

所以我们可以总结线程退出有三种方式:

  • 1️⃣从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit。
  • 2️⃣线程可以调用pthread_ exit终止自己。
  • 3️⃣一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

调用pthread_join函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  • 1️⃣如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  • 2️⃣ 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED(-1)。
  • 3️⃣ 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  • 4️⃣ 如果对thread线程的终止状态不感兴趣,可以传nullptr给value_ ptr参数。

2.4 🍎分离线程🍎

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

所以此时我们可以使用pthread_detach:
在这里插入图片描述
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

我们来验证下:

void* Run(void* args)
{pthread_detach(pthread_self());const char* name=static_cast<char*> (args);cout<<name<<" is running"<<endl;return nullptr;
}int main()
{pthread_t p1;pthread_create(&p1,nullptr,Run,(void*)"thread1");int ret=pthread_join(p1,nullptr);if(ret==0)cout<<"wait success"<<endl;elsecout<<"wait fail "<<endl;return 0;
}

当我们运行时:
在这里插入图片描述
为什么是运行success呀?不是说joinable和分离是冲突的吗?按道理这里应该会join失败的呀。
这是由于执行时是先执行的join,此时线程还没有被分离,自然就能够join成功了,我们可以像下面这样写,就会join失败:
在这里插入图片描述
当我们再次运行时:
在这里插入图片描述

2.5 🍎理解线程独立栈🍎

首先我们来看看一张图:
在这里插入图片描述通过之前动静态库的知识我们知道,pthread库是加载到共享区的,那么也就决定了进程中所有线程都是可以访问得到该库的。但是从上图我们看见了有一个主线程栈的空间,这个空间又是为谁准备的呢?
其实这个空间是为主线程准备的,我们之前讲过其余线程中的栈是相互独立的,而这个独立栈的空间就开辟在共享区中,也就是独立栈的空间其实是由库帮助我们开辟的。上图右边第一个struct_pthread又是什么鬼呢?这个是管理共享区中线程的一种数据结构,类似于进程中的PCB。至于什么是局部存储,我们可以来写一个程序看看:

int g_val=20;void* Run(void* args)
{const char* name=static_cast<char*> (args);while(true){cout<<"g_val:"<<g_val<<"&g_val:"<<&g_val<<endl;sleep(1);}
}int main()
{pthread_t pids[5];for(int i=0;i<5;++i){char* name=new char[32];snprintf(name,32,"pthread%d:",i+1);pthread_create(pids+i,nullptr,Run,name);}for(int i=0;i<5;++i){pthread_join(pids[i],nullptr);}return 0;
}

当我们运行时:
在这里插入图片描述
这也符合我们的预期,因为全局变量是所有线程共享的,但是当我们在全局变量前加上了__pthread后:
在这里插入图片描述当我们运行时:
在这里插入图片描述我们惊奇的发现居然地址不一样了,这其实就是将g_val分别保存了一份在各自的独立栈中。至于为什么打印出来的数据无规律是因为多线程并发访问的问题,我们后面在详细讲解。


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

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

相关文章

D. Rating System

Problem - D - Codeforces 思路&#xff1a;我们先将输入数据做一个前缀和&#xff0c;能够得到它的变化&#xff0c;然后我们能够发现我们只需要找到两个点&#xff0c;第一个点-第二个点最大即可&#xff0c;因为假如说我们现在到了一峰 // Problem: D. Rating System // Con…

计算机网络概述(一)

因特网概述 网络&#xff0c;互联网与因特网的区别联系&#xff1a; 以上是使用有线和无线链路连接的两个网络。那么&#xff0c;要让这两个网络连接起来&#xff0c;就需要路由器。若干个网络通过多个路由器互联起来&#xff0c;就称为了互联网。 因特网是当今世界上最大的互…

Centos安装指定docker版本和docker-compose

目录 一. 直接安装Docker最新镜像源 1. 卸载旧版本的Docker&#xff1a; 2. 安装依赖包&#xff1a; 3. 添加Docker源&#xff1a; 4. 安装Docker&#xff1a; 5. 启动Docker服务&#xff1a; 6. 验证Docker是否安装成功&#xff1a; 二、指定Docker版本安装 1. 查看…

mac电脑 flv转mp4怎么转

mac电脑 flv转mp4怎么转&#xff1f;相信大家平时在电脑上下载视频的时候遇到过这样一个尴尬的事情&#xff0c;下载下来的视频不能被直接打开播放&#xff0c;而是需要使用专门的播放器才能打开查看&#xff0c;例如flv就是这样一种视频格式。大家都知道视频文件的格式种类非常…

Java版spring cloud 本工程项目管理系统源码-全面的工程项目管理

​ ​工程项目管理系统是指从事工程项目管理的企业&#xff08;以下简称工程项目管理企业&#xff09;受业主委托&#xff0c;按照合同约定&#xff0c;代表业主对工程项目的组织实施进行全过程或若干阶段的管理和服务。 如今建筑行业竞争激烈&#xff0c;内卷严重&#xff0c…

计算机中CPU、内存、缓存的关系

CPU&#xff08;Central Processing Unit&#xff0c;中央处理器&#xff09; 内存&#xff08;Random Access Memory&#xff0c;随机存取存储器&#xff09; 缓存&#xff08;Cache&#xff09; CPU、内存和缓存之间有着密切的关系&#xff0c;它们共同构成了计算机系统的核…

【Unity编辑器扩展】编辑器代码一键添加按钮响应事件

此功能能是基于UI变量代码生成工具的改良扩展&#xff1a;【Unity编辑器扩展】UI变量代码自动生成工具(编辑器扩展干货/大幅提高效率)_ui代码自动生成_TopGames的博客-CSDN博客 工具效果预览&#xff1a; UGUI的Button按钮在编辑面板添加响应事件非常繁琐&#xff0c;需要拖个…

微服务:Springboot集成Hystrix实现熔断、降级、隔离

文章目录 前言知识积累Springboot集成Hystrix1、maven依赖引入2、application开启feign的hystrix支持&#xff08;客户端配置限流降级熔断&#xff09;3、入口类增加EnableFeignClients EnableHystrix 开启feign与hystrix4、feign调用增加降级方法服务端配置限流降级熔断(选择使…

JavaWeb——基于Spring Boot的图书数字化管理系统的设计与实现

课程设计总结 1 概述 1.1 项目开发背景 随着信息技术的快速发展&#xff0c;数字化管理已经成为各行各业提高效率和管理水平的重要手段。在图书管理领域&#xff0c;数字化管理系统可以有效地提高管理效率&#xff0c;提供更好的用户体验。本项目旨在开发一个基于Spring…

Vue使用keep-alive设置哪些组件可以被缓存,哪些不被缓存

需求&#xff1a;当一个项目中&#xff0c;不是所有的组件页面都需要缓存起来&#xff0c;因为有些页面是不需要的 <keep-alive><router-view v-if"$route.meta.keepAlive"></router-view> </keep-alive><router-view v-if"!$rout…

关于Context和ContextImpl还有ContextWrapper的关系

关于Context和ContextImpl还有ContextWrapper的关系 1.Context和ContextImpl还有ContextWrapper的关系 ​ 图一.Context和ContextImpl还有ContextWrapper的关系示意图 1.1.ContextImpl是Context的实现类 从Context和ContextImpl的源代码中,可以看出Context是一个抽象类,具体…

机器学习概括

文章目录 一、机器学习是什么&#xff1f;二、模型训练YouTube流量预测1. 先写一个具有未知参数的函数&#xff08;Function&#xff09;2. 定义损失&#xff08;从训练数据进行计算&#xff09;3.最优化4.结果分析 Back to framework1.带有未知数的函数&#xff1a;2.定义损失…

K8S应用流程安全(镜像安全 配置管理 访问安全)

应用流程安全 1 应用流程安全1.1 镜像安全1.1.1 构建原则1.1.2 Dockerfile实践1.1.3 构建进阶1.1.4 镜像检测1.1.5 仓库升级1.1.6 高可用仓库1.1.7 镜像策略 1.2 配置管理1.2.1 配置基础1.2.2 YAML安全1.2.3 kustomize1.2.4 基础实践1.2.5 功能复用1.2.6 配置定制1.2.7 补丁实践…

当你按下键盘A键

CPU 里面的内存接口&#xff0c;直接和系统总线通信&#xff0c;然后系统总线再接入一个 I/O 桥接器&#xff0c;这个 I/O 桥接器&#xff0c;另一边接入了内存总线&#xff0c;使得 CPU 和内存通信。再另一边&#xff0c;又接入了一个 I/O 总线&#xff0c;用来连接 I/O 设备&…

el-table 动态合并不定项多级表头

我们的需求是根据不同的厂配不同的多级表头,每个表头有需要合并的项,并且不确定 如图所示 对表格进行循环操作,此处不赘述,最下方有全部代码 表头是单独写在js方便后期更改,然后引入js文件,然后根据情况去调取 // 获取表头getHeader(nv) {this.factoryCodes nv;this.heade…

【UE4 C++】根据指定路径生成静态网格体

在上一篇博客中&#xff08;【UE C】蓝图调用C函数&#xff09;&#xff0c;我们用C创建了一个蓝图函数库&#xff0c;本篇文章在这个蓝图函数库基础上增加一个方法&#xff0c;该方法只需输入一个文件目录路径&#xff0c;就可在场景中生成该目录下得所有静态网格体。&#xf…

第6集丨JavaScript 使用原型(prototype)实现继承——最佳实战3

目录 一、原型继承与属性拷贝1.1 功能说明1.2 功能测试 二、多重继承2.1 功能实现2.2 功能测试 三、寄生式继承四、构造器借用4.1 简单实现4.2 进化版4.2.1 功能实现4.2.2 案例测试 五、借用构造器和原型复制六 综合案例6.1 需求说明6.2 代码实现 一、原型继承与属性拷贝 1.1 功…

css之:is()、:where()和:has()伪元素的运用、使用、important

文章目录 简介1、:is()2、:where()3、:has() 简介 :is()、:where()和:has()伪元素是CSS中用于样式化元素的非常强大的工具。它们是在CSS选择器Level4规范中引入的。它们允许我们将样式应用于符合特定条件的任何元素&#xff0c;例如元素的类型、元素的位置和元素的后代。 1、:i…

【高并发】高并发架构实战:从需求分析到系统设计

Yan-英杰的主页 悟已往之不谏 知来者之可追 C程序员&#xff0c;2024届电子信息研究生 很多软件工程师的职业规划是成为架构师&#xff0c;但是要成为架构师很多时候要求先有架构设计经验&#xff0c;而不做架构师又怎么会有架构设计经验呢&#xff1f;那么要如何获得架构设…

前端渲染模式CSR,SSR,SSG,ISR,DPR

目录 一、客户端渲染——CSR&#xff08;Client Side Rendering&#xff09; 二、服务器端渲染——SSR&#xff08;Server Side Rendering&#xff09; 三、静态站点生成——SSG&#xff08;Static Site Generation&#xff09; 四、增量静态生成——ISR&#xff08;Increm…