【Linux】多线程概念线程控制

文章目录

  • 多线程概念
    • Linux下进程和线程的关系
    • pid本质上是轻量级进程id,换句话说,就是线程ID
    • Linux内核是如何创建一个线程的
    • 线程的共享和独有
    • 线程的优缺点
  • 线程控制
    • POSIX线程库
    • 线程创建
    • 线程终止
    • 线程等待
    • 线程分离

多线程概念

Linux下进程和线程的关系

在《程序员的自我修养》这本书中,对Linux下的多线程做了这样的描述:

Windows对进程和线程的实现如同教科书一样标准,Windows内核有明确的线程和进程的概念。在Windows API可以使用明确的API:CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操纵它们。但对于Linux来说,线程并不是一个通用的概念。
Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程的概念。
Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程

可以给出以下结论:

  • 线程是依附于进程才能存在的,如果没有进程,则线程不会单独存在
  • 多线程的存在是为了提高整个程序的运行效率的
  • 线程也被称为执行流,一个线程是执行代码的一个执行流,因为线程在执行用户写的代码,程序员创建的线程被称之为“工作线程”
  • Linux内核当中没有线程的概念,只有轻量级进程(LWP),线程是C库中的概念

pid本质上是轻量级进程id,换句话说,就是线程ID

  • 在task_struct结构体当中:
    • pid_t pid:轻量级进程id,也被称为线程id,不同的线程拥有不用的pid
    • pid_t tgid:轻量级进程组id,也被称为进程id,一个进程当中的线程拥有相同的tgid
  • 为什么在进程概念的介绍中,说pid就是进程 id?
    • 线程因为主线程的pid和tgid相等,而我们当时进程中只有一个主线程。所以我们的pid就等于tgid。所以将pid成为进程id也就是现在的tgid。

Linux内核是如何创建一个线程的

其本质就是再在当前进程组中创建一个task_struct结构体,它拥有着和主线程不同的pid,指向同一块虚拟进程地址空间。

在这里插入图片描述

线程的共享和独有

独有:

在进程虚拟地址空间的共享区当中,调用栈,寄存器, 线程ID,errno,信号屏蔽字, 调度优先级独有

  • 调用栈独享
    在这里插入图片描述

  • 寄存器独享:

    当操作系统调度进程的时候一定是以task_struct结构体调度,而task _struc结构体是以双向链表存储,而操作系统调度时是从就绪队列中调度已经就绪的进程,在这里也就是轻量级进程-线程,当调度时一定会有其他线程被切出,而它切出时寄存器中存储的就是当前要执行的指令,所以要用结构体中上下文信息保存

  • 线程ID独享:每个线程就是一个轻量级进程,所以它有自己的pid

  • errno独享:当线程在执行出错时会返回一个errno,这个errno属于当前自己的线程错误

  • 信号屏蔽字独享:阻塞位图

  • 调度优先级独享:每个进程/线程在执行时被调度的优先顺序

共享:

共享:文件描述符表,用户id,用户组id,信号处理方式,当前进程的工作目录

线程的优缺点

优先:

  • 多线程的程序,拥有多个执行流,合理使用(要保证结果运行结构正确,例如多个进程并发执行就可能会出现同时更改一块内存,从而出现运行结果错误,要控制线程的访问时序问题), 可以提高程序的运行效率
  • 多线程程序的线程切换比多进程程序快,付出的代价小 (因为这些线程指向同一个进程虚拟地址空间,有些可以共享的数据,比如:全局变量就能在线程切换的时候,不进行切换可以充分发挥多核CPU并行(并行就是有多个CPU,每个CPU执行一个线程,各自执行各自的)的优势
  • 计算密集型的程序,可以进行拆分,让不同的线程执行计算不一 样的事情(比如我们要从1加到1亿我们可以让多个进程来各自计算其中一段加法,可以更快的得出结果)
  • I/O密集型的程序,可以进行拆分, 让不同的线程执行不同的I/O操作,可以不用串型运行, 提高程序运行效率。比如:我们要从多个文件中读取内容,如果我们只有一个进程的话,那就只能从一个文件中读取之后,在从下一个文件中读取,这样的串行运行,但是当我们有多个进程,就可以让多个进程从多个文件中同时读取。但也不是所有问题都可以拆分成多个进程去分开解决,一个女人花十个月可以生出一个孩子,但是十个女人不能再一个月生出一个孩子(《程序员的自我修养》)
    再比如:scanf是一个阻塞函数,假如printf函数前面有scanf需要被执行,这样,在scanf没有完成的时候,就不能往下执行printf,但是我们让两个线程来分别来执行scanf和printf,这样,就不存在被scanf阻塞,而后面的程序无法执行的问题了
    在这里插入图片描述

缺点:

  • 编写代码的难度更加高(当多个线程访问同一个程序的时候我们需要控制线程访问的先后顺序,要不然就可能出现问题)
  • 代码的(稳定性)鲁棒性要求更加高,一个线程崩溃,会导致整个进程退出。当多个线程在运行时,而CPU资源少的情况下一定是有一线程访问不到CPU资源的,那这时就一定要有线程被切换出来,将CPU资源让出来,这时一旦有线程霸占CPU资源占着不放的话,此时这个得不到CPU资源的线程就有可能崩溃,一旦它崩溃就会导致整个进程退出
  • 线程数量并不是越多越好,线程的切换是需要时间的,所以一个程序的线程数量一定是我们依照一个机器的配置(CPU数量)而经过测量来得出,创建多少个线程合适

在这里插入图片描述

  • 缺乏访问控制,多个线程同时访问一个空间,如果不加以控制,可能会导致程序产生二义性结果

线程控制

POSIX线程库

  • 线程相关的函数构成的函数库,绝大多数函数是以pthread_开头的
  • 使用这些线程函数需要引入头文件pthread.h
  • 编译含有多线程函数的源文件时,要加上编译命令-lpthread选项

线程创建

函数:

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

参数:

  • thread:线程标识符,是一个输出型参数,本质上是线程独有空间的首地址
  • attr:线程的属性信息,一般不设置属性信息,传递NULL,采用默认的线程属性;如果要设置属性信息,一般我们关心下列属性信息:调用栈的大小、分离属性、调度策略、分时策略、调度优先级等等
  • start_routine:函数指针,线程执行的入口函数(线程执行起来的时候,从该函数开始运行,注意:不是从main函数开始运行),当前线程执行时都是从线程入口函数开始执行
  • arg:传递给线程入口函数的参数,也就是给start_routine传递参数
    返回值:
  • 成功返回0
  • 失败返回<0
    线程创建代码演示:
    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* mythread_start(void* arg){6   printf("I am work thread\n");
W>  7 }8 9 int main(){10   pthread_t tid;11   int ret = pthread_create(&tid, NULL, mythread_start, NULL);                                12   if(ret < 0){13     perror("pthread_create");14     return 0;15   }16   return 0;17 }

注意在makefile文件中链接线程库

执行结果:

在这里插入图片描述

很遗憾我们没看到应该存在的输出,这是什么原因呢?

因为线程都是需要操作系统进行调度的,我们在main函数中创建出来一个线程,但是我们的线程还没被调度,main线程就执行结束返回了,main函数返回就意味着进程结束了,进程结束了我们的线程就不存在了,自然不会再给出任何打印。

那我们想看到现象要怎么做呢?很容易想到,让main函数晚一点退出,给工作线程充足的时间能被操作系统调度,我们让main函数在返回前执行sleep(2);

再执行:可以看到工作线程执行了它的代码

为了观察一下线程堆栈和函数堆栈,我们索性让main函数和线程入口函数都进入睡眠状态,修改后代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* mythread_start(void* arg){6   while(1){7     sleep(1);8     printf("I am work thread\n");9   }10 }11 12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, mythread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   while(1){20     sleep(1);                                                                                21   }22   return 0;23 }

我们看看此时的调用堆栈:

在这里插入图片描述
可以用top -H -p [pid]查看进程状态信息;

我们试着给线程传递一下局部变量,代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 5 void* mythread_start(void* arg){6   int* i = (int*)arg;7   printf("I am work thread %d\n", *i);
W>  8 }9 10 int main(){11   pthread_t tid;12   for(int i=0; i<5; ++i){13     int ret = pthread_create(&tid, NULL, mythread_start, (void*)&i);               14     if(ret < 0){15       perror("pthread_create");16       return 0;17     }18   }19   sleep(1);20   return 0;21 }

观察一下程序执行结果:

在这里插入图片描述

我们的预期是打印0-4的数字,但是执行几次发现,首先每次执行结果并不一样,其次并不按照我们预期的结果进行打印,这是怎么回事呢?是因为线程是抢占式执行的,可能是我们将所有的线程创建出来,再去执行线程的代码,或者说一边创建一边执行代码, 线程的执行顺序不确定,某个线程访问数据的时间也不确定,导致了我们上述那么多种执行结果,还有一种结果是访问数据5,i是我们for循环种的局部变量,如果for循环退出后还有线程去访问i,这是十分危险的,因为i已经被释放了,此时再对它进行访问,就有可能导致错误。

解决上面的方式有两种一种是在main函数中创建一个变量,只要main函数存在,其他那个变量就存在。而main函数退出线程也就退出了,不存在非法访问。这种是解决非法访问的问题。

另一种方式是在堆上申请空间,这样保证每个进程访问的数据是自己对应的数据

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 #include<stdlib.h>5 6 void* mythread_start(void* arg){7   int* p = (int*)arg;8   printf("I am work thread%d\n", *p);                                              9   free(p);
W> 10 }11 12 int main(){13   pthread_t tid;14   for(int i=0; i<5; i++){15     int *p = (int*)malloc(sizeof(int));16     *p = i;17     int ret = pthread_create(&tid, NULL, mythread_start, (void*)p);18     if(ret < 0){19       perror("pthread_create");20       return 0;21     }22   }23   sleep(1);24   return 0;25 }

执行结果:

在这里插入图片描述

总结:

  • 不要给线程传递临时变量,因为传递临时变量当临时变量销毁时,线程拿到的是临时变量的地址,还可以访问那块被释放的空间,容易造成进程崩溃
  • 线程入口函数传递参数的时候,传递堆区空间,释放堆区空间的时候,让线程自己释放

线程终止

线程终止的方法:

1、从线程入口函数种return返回
2、pthread_exit(void* retval)函数,retval:线程退出时, 传递给等待线程的退出信息;作用:
谁调用谁退出,主线程调用主线程终止,工作线程调用工作线程终止
3、pthread_cancel(pthread_t)参数是一个线程标识符,想要取消哪个线程,就传递哪个线程的标识符

补充一个函数:pthread_t pthread_self(void):返回调用此函数的线程id

代码演示:

  • 创建一个线程,然后在main函数中终止这个线程,为了防止是进程结束,而导致线程也结束我们在main函数中加一个死循环

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   pthread_cancel(tid);20   while(1){21     sleep(1);22     printf("I am main pthread\n");23   }24   return 0;25 } 

执行结果:

在这里插入图片描述

能看到线程并没有立即终止,而是执行了一下线程种的命令然后才终止

  • 观察结束主线程,而不结束工作线程,会出现什么现象

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){                       8     sleep(1);                     9     printf("I am work thread-l\n");10   }                               11 }                                 12 int main(){                       13   pthread_t tid;                  14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){                    16     perror("pthread_create");     17     return 0;                     18   }                               19   getchar();//设置阻塞,当接受到字符后主线程将结束                                      20   pthread_cancel(pthread_self());//结束主线程         21   while(1){                    22     sleep(1);                  23     printf("I am main pthread\n");24   }                            25   return 0;26 }

设置阻塞的目的是为了查看进程id,以观察进程

执行结果:

getchar之前的状态:

在这里插入图片描述
getchar之后的状态:

在这里插入图片描述
用ps aux | grep tex查看前后对比:

在这里插入图片描述

可以得出结论:主线程先退出,工作线程没退出,主线程变成僵尸进程

  • 验证pthread_cancle函数,结束一个线程时,它会执行下一行命令

代码思路:将while循环去掉,让线程退出的下一句代码是return 0,观察程序状况

代码如下:

    2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   getchar();//设置阻塞,当接受到字符后主线程将结束20   pthread_cancel(pthread_self());//结束主线程21   //while(1){22   //  sleep(1);23   //  printf("I am main pthread\n");24   //}                                                                                     25   return 0;26 }

执行结果:

在这里插入图片描述

可以发现这次进程直接退出了,主线程也不是僵尸状态了,这时因为当我们执行pthread_cancle函数时,结束一个线程时,他会执行下一行命令,这时我们将主线程退出了,它在退出前执行了return 0,就会使得整个进程结束,那么此时工作线程也就退出了

  • 观察主线程先退出变成僵尸进程后,工作线程执行完后主线程的状态

代码思路:让主线程退出,然后工作线程等待10s之后退出

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   int count = 30;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }
W> 11 }12 int main(){                                                                                      13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   //getchar();//设置阻塞,当接受到字符后主线程将结束20   pthread_cancel(pthread_self());//结束主线程21   while(1){22     sleep(1);23     printf("I am main pthread\n");24   }25   return 0;26 }

执行结果分析:

在这里插入图片描述

当主线程退出而工作线程不退出时,我们是无法看到进程的调用栈信息的

在这里插入图片描述
总结:

  • 当我们执行pthread_cancle函数时,结束一个线程时,他会执行pthread_cancle函数下一行命令,然后再结束线程
  • 当主线程退出后,工作线程如果依然在执行,主线程就会处于僵尸状态,而当工作线程执行完毕之后退出,整个进程也随之结束

线程等待

线程在创建出来的时候,属性默认是joinable属性,意味着线程在退出的时候需要其他执行流(线程)来回收线程的资源(主要是退出线程使用到的共享区当中的空间)

接口:

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

功能:若线程A调用了该函数等待B线程,A线程会阻塞,直到B线程退出后,A线程才会解除阻塞状态
参数:

  • pthread_t : 线程标识符,要等待哪一个线程,就传递哪个线程的标识符
  • retval : 保存的是一个常数,退出线程的退出信息
线程退出方式*retval保存的东西
return入口函数返回值
pthread_exit函数pthread_exit函数参数
pthread_cancel函数PTHREAD_CANCEL宏定义

返回值:成功返回0,失败返回错误码

代码思路:让工作线程等待30s退出,然后在主线程中等待工作线程退出

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 5 void* pthread_start(void *arg){6   int count = 30;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   pthread_join(tid, NULL);                                                                       20   return 0;21 }

执行分析:

在这里插入图片描述

线程分离

分离线程是将线程标记成已分离,其属性从joinable变成detach,对于detach属性的线程终止后,系统会自动回收其资源,不用任何线程回收其资源

接口:

int pthread_detach(pthread_t thread);

功能:将线程标记为已分离,目的是当分离的线程终止时,其资源会自动释放,防止产生僵尸进程,防止内存泄漏

参数pthread_t:需要标记分离的线程标识符

调用pthread_detach函数的位置可以是:

  • 在主线程中调用分离创建出来的线程,即主线程标记分离工作线程;
  • 在工作线程的线程入口函数中调用,即自己标记分离自己;
    线程分离的实质就是将线程的属性设置为detach
  • 工作线程退出,然后不回收它的退出状态信息

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   int count = 10;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }
W> 11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   //pthread_join(tid, NULL);20   while(1){21     sleep(1);22   }23   return 0;24 }

执行结果分析:

在这里插入图片描述

可以看到它运行完直接退出了,也没有变成僵尸状态

  • 将工作线程设置为分离状态,观察

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   pthread_detach(pthread_self());                                                                7   int count = 30;8   while(count--){9     sleep(1);10     printf("I am work thread-l\n");11   }
W> 12 }13 int main(){14   pthread_t tid;15   int ret = pthread_create(&tid, NULL, pthread_start, NULL);16   if(ret < 0){17     perror("pthread_create");18     return 0;19   }20   //pthread_join(tid, NULL);21   while(1){22     sleep(1);23   }24   return 0;25 }

执行分析:

在这里插入图片描述
在这里插入图片描述

结论:无论其他线程等待不等待工作线程退出,回收它的退出状态信息,工作线程都不会变为僵尸状态。

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

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

相关文章

MySql015——使用子查询

一、创建customers表 ######################## # Create customers table ######################## use study;CREATE TABLE customers (cust_id int NOT NULL AUTO_INCREMENT,cust_name char(50) NOT NULL ,cust_address char(50) NULL ,cust_city char…

9.oracle中sign函数

在Oracle/PLSQL中, sign 函数返回一个数字的正负标志. 语法如下&#xff1a;sign( number ) number 要测试标志的数字. If number < 0, then sign returns -1. If number 0, then sign returns 0. If number > 0, then sign returns 1. 应用于: Oracle 8i, Oracle …

Idea配置Remote Host

一、打开RemoteHost窗口 双击shift打开全局搜索 搜索Tools→Deployment→Browse Remote Host或 idea项目顶部Tools→Deployment→Browse Remote Host 二、添加服务 右侧边栏打开RemoteHost&#xff0c;点击三个点&#xff0c;起个名字&#xff0c;选择type为SFTP&#xff…

Jenkins工具系列 —— 配置邮箱 每个job下动态设置临时发送人

文章目录 安装插件添加邮箱认证邮箱申请&#xff08;以QQ邮箱网页为例&#xff09;jenkins添加邮箱认证 jenkins设置邮箱相关信息配置全局邮件单个JOB邮箱配置 安装插件 点击 左侧的 Manage Jenkins —> Plugins ——> 左侧的 Available plugins 添加邮箱认证 邮箱申请…

前端基础(ES6 模块化)

目录 前言 复习 ES6 模块化导出导入 解构赋值 导入js文件 export default 全局注册 局部注册 前言 前面学习了js&#xff0c;引入方式使用的是<script s"XXX.js">&#xff0c;今天来学习引入文件的其他方式&#xff0c;使用ES6 模块化编程&#xff0c;…

01、前端使用 thymeleaf 后,视图页面找不到---Cannot resolve MVC View ‘xxxxx前端页面‘

Cannot resolve MVC View ‘xxxxx前端页面’ 没有找到对应的mvc的前端页面。 代码&#xff1a;前端这里引入了 thymeleaf 模板 解决&#xff1a; 需要添加 thymeleaf 的依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>s…

智慧充电桩物联网方案架构

智慧充电桩物联网采用“云-管-边-端”的边缘计算物联网架构&#xff0c;融合5G、AI、Wi-Fi 6等技术&#xff0c;实现充电基础设施由数字化向智能化演进。智慧充电桩物联网方案架构设计&#xff0c;如下图所示&#xff1a; 云端&#xff1a; 物联网平台具备广泛协议的南向接入…

smartsofthelp 5.0 最专业的数据库优化工具,数据库配置优化,数据库高并发优化,SQL 语句优化...

下载地址:百度网盘 请输入提取码 SQL操作返回历史记录&#xff1a; 2023-08-21 20:42:08:220 输入&#xff1a;select version as 版本号 2023-08-21 20:42:08:223 输出&#xff1a;当前数据库实例版本号&#xff1a;Microsoft SQL Server 2012 - 11.0.2100.60 (X64) …

2023年 Java 面试八股文(25w字)

0.Java八股文上&#xff08;25w字&#xff09;2.3w 1.集合容器 2.Java基础链接 目录 一.Java 基础面试题1.Java概述Java语言有哪些特点&#xff1f;Java和C有什么关系&#xff0c;它们有什么区别&#xff1f;JVM、JRE和JDK的关系是什么&#xff1f;**什么是字节码?**采用字…

使用Pillow对图像进行变换

使用Pillow对图像进行变换 from PIL import Image, ImageEnhance# 原图 image Image.open("1.jpg") image.show()# 镜像 mirrored_image image.transpose(Image.FLIP_LEFT_RIGHT) mirrored_image.show() mirrored_image.save(mirror_image.jpg)# 旋转 rotated_imag…

【PHP面试题82】system和exec是用来做什么的?有什么区别

文章目录 &#x1f680;一、前言&#xff0c;PHP中system和exec命令的作用&#x1f680;二、system()函数&#x1f680;三、exec()函数&#x1f680;四、区别和应用场景&#x1f50e;4.1 使用system()函数的应用场景&#x1f50e;4.2 使用exec()函数的应用场景&#x1f50e;4.3…

几个nlp的小任务(生成任务(摘要生成))

几个nlp的小任务生成任务——摘要生成 安装库选择模型加载数据集展示数据集数据预处理 tokenizer注意特殊的 token处理组成预处理函数调用map,对数据集进行预处理微调模型,设置参数设置数据收集器,将处理好的数据喂给模型封装测评方法将参数传给 trainer,开始训练安装库 选…

RS485保护电路

今天给大家分享485接口的EMC设计&#xff0c;希望对电路设计&#xff0c;及相关软件开发的人员有帮助。 一、原理图 1. RS485接口6KV防雷电路设计方案 &#xff08;RS485接口防雷电路&#xff09; 接口电路设计概述&#xff1a; RS485用于设备与计算机或其它设备之间通讯&…

【AI底层逻辑】——篇章7(下):计算资源软件代码共享

续上篇... 目录 续上篇... 三、计算资源 1、第一阶段&#xff1a;数据大集中 2、第二阶段&#xff1a;资源云化 ①“云”的分类 ②虚拟化技术 ③边缘计算的普及 四、软件代码共享 总结 往期精彩&#xff1a; 三、计算资源 AlphaGo算法论文虽然已经发表&#xff0c;但…

macOS - DOSbox

文章目录 关于 DOSbox安装使用启动设置启动盘、查看文件 debug 关于 DOSbox 官网&#xff1a; https://www.dosbox.com/文档&#xff1a;https://www.dosbox.com/wiki/Basic_Setup_and_Installation_of_DosBox下载&#xff1a; https://www.dosbox.com/download.php https://s…

【Jenkins】持续集成部署学习

【Jenkins】持续集成部署学习 【一】Jenkins介绍【二】Docker安装Gitlab【1】首先准备一台空的虚拟机服务器【2】安装服务器所需的依赖【3】Docker的安装【4】阿里云镜像加速【5】安装Gitlab 【三】Gitlab的使用&#xff08;1&#xff09;Gitlab创建项目&#xff08;2&#xff…

3.3.2:SUM作为一般函数及聚合函数的应用

【分享成果&#xff0c;随喜正能量】我们很多道友没受过什么苦&#xff0c;或受不了一句话、一点气&#xff0c;总想悠悠自在成佛。或是念上几十部经就想换取什么&#xff0c;法宝是无价的&#xff01;你拿有价来换&#xff0c;不但换不到&#xff0c;还丧失了功德。应当不退初…

【SpringBoot学习笔记02】静态资源

Spring Boot 通过 MVC 的自动配置类 WebMvcAutoConfiguration 为这些 WebJars 前端资源提供了默认映射规则&#xff0c;部分源码如下。 jar包&#xff1a; JAR 文件就是 Java Archive File&#xff0c;顾名思意&#xff0c;它的应用是与 Java 息息相关的&#xff0c;是 Java 的…

IP编址数据转发(md版)

IP编址&数据转发 一、IP编址1.1、二进制、十进制和十六进制1.2、进制之间的转换1.3、IP编址1.4、子网掩码1.5、二进制和十进制转换1.6、IP地址分类1.7、IP地址类型1.8、地址规划 二、VLSM与CIDR2.1、有类IP编址的缺陷2.2、变长子网掩码 VLSM2.3、缺省情况下的掩码2.4、子网…

vscode远程调试

安装ssh 在vscode扩展插件搜索remote-ssh安装 如果连接失败&#xff0c;出现 Resolver error: Error: XHR failedscode 报错&#xff0c;可以看这篇帖子vscode ssh: Resolver error: Error: XHR failedscode错误_阿伟跑呀的博客-CSDN博客 添加好后点击左上角的加号&#xff0…