【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.mzph.cn/news/55242.shtml

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

相关文章

使用StreamLold写入 Starrocks报错:Caused by org

问题描述 使用StreamLoad写入Starrocks报错&#xff0c;报这个错误:Caused by: org.apache.http.ProtocolException: Content-Length header already present 代码案例 引入依赖 <!-- Starrocks使用StreamLoad发送Http请求 --><dependency><groupId>or…

系统架构设计高级技能 · 面向服务架构设计理论与实践

系列文章目录 系统架构设计高级技能 软件架构概念、架构风格、ABSD、架构复用、DSSA&#xff08;一&#xff09;【系统架构设计师】 系统架构设计高级技能 系统质量属性与架构评估&#xff08;二&#xff09;【系统架构设计师】 系统架构设计高级技能 软件可靠性分析与设计…

【自动驾驶】TI SK-TDA4VM 开发板上电调试,AI Demo运行

1. 设备清单 TDA4VM Edge AI 入门套件【略】USB 摄像头(任何符合 V4L2 标准的 1MP/2MP 摄像头,例如:罗技 C270/C920/C922)全高清 eDP/HDMI 显示屏最低 16GB 高性能 SD 卡连接到互联网的 100Base-T 以太网电缆【略】UART电缆外部电源或电源附件要求: 标称输出电压:5-20VDC…

Jmeter 接口测试总结

背景介绍 对于 Android 项目来说&#xff0c;使用的是 Java 开发&#xff0c;网络请求接口的数量庞大且复杂&#xff0c;测试人员无法很直观的判断、得出网络请求是否存在问题。另一方面&#xff0c;为了验证请求接口是否能够在大负荷条件下&#xff0c;长时间、稳定、正常的运…

【JMeter】常用线程组设置策略

目录 一、前言 二、单场景基准测试 1.介绍 2.线程组设计 3.测试结果 三、单场景并发测试 1.介绍 2.线程组设计 3.测试结果 四、单场景容量/爬坡测试 1.介绍 2.线程组设计 3.测试结果 五、混合场景容量/并发测试 1.介绍 六、稳定性测试 1.介绍 2.线程组设计 …

C++中的const成员变量和成员函数

在类中&#xff0c;如果你不希望某些数据被修改&#xff0c;可以使用const关键字加以限定。const 可以用来修饰成员变量和成员函数。 const成员变量 const 成员变量的用法和普通 const 变量的用法相似&#xff0c;只需要在声明时加上 const 关键字。初始化 const 成员变量只有…

深度学习优化入门:Momentum、RMSProp 和 Adam

目录 深度学习优化入门&#xff1a;Momentum、RMSProp 和 Adam 病态曲率 1牛顿法 2 Momentum:动量 3Adam 深度学习优化入门&#xff1a;Momentum、RMSProp 和 Adam 本文&#xff0c;我们讨论一个困扰神经网络训练的问题&#xff0c;病态曲率。 虽然局部极小值和鞍点会阻碍…

01-jupyter notebook的使用方法

一、Tab补全 在shell中输入表达式&#xff0c;按下Tab&#xff0c;会搜索已输入变量&#xff08;对象、函数等等&#xff09;的命名空间&#xff1a; 除了补全命名、对象和模块属性&#xff0c;Tab还可以补全其它的。当输入看似文件路径时 &#xff08;即使是Python字符串&…

文心一言 VS 讯飞星火 VS chatgpt (79)-- 算法导论7.4 4题

四、请用go语言&#xff0c;证明:RANDOMIZED-QUICKSORT 期望运行时间是 Ω(nlgn)。 文心一言&#xff1a; 首先&#xff0c;为了证明RANDOMIZED-QUICKSORT的期望运行时间是Ω(nlg n)&#xff0c;我们需要证明在最坏的情况下&#xff0c;该算法的运行时间是O(nlg n)。然后&…

网络:RIP协议

1. RIP协议原理介绍 RIP是一种比较简单的内部网关协议&#xff08;IGP协议&#xff09;&#xff0c;RIP基于距离矢量的贝尔曼-福特算法(Bellman - Ford)来计算到达目的网络的最佳路径。最初的RIP协议开发时间较早&#xff0c;所以在带宽、配置和管理方面的要求也较低。 路由器运…

dockerfile镜像及Harbor私有仓库搭建的应用

目录 搭建私有仓库harbordockerfile构建镜像1&#xff0c;先创建一个目录2&#xff0c;编写dockerfile3&#xff0c;构建4&#xff0c; 验证镜像5&#xff0c;标记镜像6&#xff0c;上传镜像 搭建私有仓库harbor 首先安装容器编排工具&#xff1a;docker compose 我使用的是离…

信创国产系统麒麟arm架构中nginx安装过程

前言 在事业单位或国企&#xff0c;信创项目在步步推进&#xff0c;下面将在国产系统通信arm架构中nginx的安装过程记录分享出来&#xff0c;希望帮助到有需要的小伙伴。 1、nginx下载 1.1、在线下载 进入指定目录&#xff0c;如/usr/local&#xff0c;执行如下命令&#x…

LoRA继任者ReLoRA登场,通过叠加多个低秩更新矩阵实现更高效大模型训练效果

论文链接&#xff1a; https://arxiv.org/abs/2307.05695 代码仓库&#xff1a; https://github.com/guitaricet/peft_pretraining 一段时间以来&#xff0c;大模型&#xff08;LLMs&#xff09;社区的研究人员开始关注于如何降低训练、微调和推理LLMs所需要的庞大算力&#xf…

什么是异步编程?什么是回调地狱(callback hell)以及如何避免它?

聚沙成塔每天进步一点点 ⭐ 专栏简介⭐ 异步编程⭐ 回调地狱&#xff08;Callback Hell&#xff09;⭐ 如何避免回调地狱1. 使用Promise2. 使用async/await3. 模块化和分离 ⭐ 写在最后 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 记得点击上方或者右侧链接订…

前端需要理解的浏览器知识

1 浏览器架构 浏览器是多进程多线程的应用程序&#xff0c;多进程可以避免相互影响和减少连环崩溃的几率&#xff1a; 浏览器&#xff08;主&#xff09;进程&#xff1a;主要负责界⾯显示、⽤户交互、⼦进程管理、存储等功能。内部会启动多个线程分别处理不同的任务。⽹络进…

pandas由入门到精通-数据清洗-缺失值处理

pandas-02-数据清洗&预处理 A.缺失值处理1. Pandas缺失值判断2. 缺失值过滤2.1 Series.dropna()2.2 DataFrame.dropna()3. 缺失值填充3.1 值填充3.2 向前/向后填充文中用S代指Series,用Df代指DataFrame 数据清洗是处理大型复杂情况数据必不可少的步骤,这里总结一些数据清…

安卓图形显示系统

Android图形显示系统 Android图形显示系统是Android比较重要的一个子系统&#xff0c;和很多其他子系统的关联紧密。 Android图形系统比较复杂&#xff0c;这里我们从整体上理一遍&#xff0c;细节留待后期再去深入。Android图形系统主要包括以下几个方面&#xff1a; - 渲染…

玩转git第7章节,本地git的用户名和密码的修改

一 本地git的用户名和密码 1.1 本地用户名和密码修改 1.本地用户名修改 2.凭据管理 3.进行修改密码 1.2 代码提交操作

206. 反转链表 (简单系列)

给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a; 输入&#xff1a;head [1,2] 输出&#xff1a;[2,1] 示例 3&#xff1a; 输…