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

news2024/11/13 16:12:08

文章目录

  • 多线程概念
    • 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.coloradmin.cn/o/932063.html

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

相关文章

Pygame编程(9)font模块

Pygame编程&#xff08;9&#xff09;font模块 函数示例 函数 pygame.font.init 初始化字体模块init() -> None pygame.font.quit 反初始化字体模块quit() -> None pygame.font.get_init True,如果字体模块已初始化get_init() -> bool pygame.font.get_default_font …

VUE笔记(九)vuex

一、vuex的简介 1、回顾组件之间的通讯 父组件向子组件通讯&#xff1a;通过props实现 子组件向父组件通讯&#xff1a;通过自定义事件($emit)方式来实现 兄弟组件之间的通讯&#xff1a;事件总线&#xff08;$eventBus&#xff09;、订阅与发布方式来实现 跨级组件的通讯…

django+MySQL计算机毕设之图片推荐系统(报告+源码)

图片推荐系统是在的数据存储主要通过MySQL。用户在使用应用时产生的数据通过Python语言传递给数据库。通过此方式促进图片推荐信息流动和数据传输效率&#xff0c;提供一个内容丰富、功能多样、易于操作的平台。述了数据库的设计&#xff0c;系统的详细设计部分主要论述了几个主…

win开机自启jar包

下载winsw工具 只需下载图中红框的工具 https://github.com/winsw/winsw/releases 文件配置 将下载的文件与jar文件放置在一起&#xff0c;两个文件名修改为服务名 编辑xml文件 注意不要出现中文&#xff0c; 标签内的jar文件地址要改为自己目录 <service><!-- I…

日本某地发生了一件谋杀案,警察通过排查确定杀人凶手必为4个嫌疑犯的一个题目详解(逻辑类型题2)

题目内容 题目日本某地发生了一件谋杀案&#xff0c;警察通过排查确定杀人凶手必为4个嫌疑犯的一个。 以下为4个嫌疑犯的供词&#xff1a; A说&#xff1a;不是我。 B说&#xff1a;是C。 C说&#xff1a;是D。 D说&#xff1a;C在胡说 已知3个人说了真话&#xff0c;1个…

VUE笔记(十)Echarts

一、Echarts简介 1、什么是echarts ECharts是一款基个基于 JavaScript 的开源可视化图表库 官网地址&#xff1a;Apache ECharts 国内镜像&#xff1a;ISQQW.COM x ECharts 文档&#xff08;国内同步镜像&#xff09; - 配置项 示例&#xff1a;echarts图表集 2、第一个E…

腾讯音乐财务前景疲软,股价上涨势头难以持续

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 市场对腾讯音乐第二季度财报反应积极 猛兽财经之前在2023年6月12日发表的一篇文章中就曾分析过腾讯音乐&#xff08;TME&#xff09;股票&#xff0c;我们当时分析了腾讯音乐2023年第一季度的财务业绩。根据S P Capital IQ…

[halcon] 局部图片保存 gen_circle 和 gen_rectangle2 对比 这怕不是bug吧

背景 我想实现一个功能&#xff0c;获取图片中瑕疵的位置&#xff0c;将瑕疵周边的一块区域抠图并保存。 上代码 一开始我代码这么写的&#xff1a; gen_circle (Rectangle, Row[i], Column[i], 256) reduce_domain(Image,Rectangle,GrayEllipse) crop_domain(GrayEllipse,…

Mybatis中实现动态SQL

目录 1 背景2 if判断2 choose(when,otherwise)选择分支进入3 where4 set5 trim6 foreach循环遍历7 小结 1 背景 不建议使用&#xff0c;但是有些需求又不得不用回来温习下。动态SQL语句,也就意味着SQL语句不在是一成不变的而是具有多样性. 2 if判断 if的用法还是跟平常差不多…

[论文阅读笔记26]Tracking Everything Everywhere All at Once

论文地址: 论文 代码地址: 代码 这是一篇效果极好的像素级跟踪的文章, 发表在ICCV2023, 可以非常好的应对遮挡等情形, 其根本的方法在于将2D点投影到一个伪3D(quasi-3D)空间, 然后再映射回去, 就可以在其他帧中得到稳定跟踪. 这篇文章的方法不是很好理解, 代码也刚开源, 做一…

Git工作流

实际开发项目使用到的分支: main&#xff1a;生产环境&#xff0c;也就是你们在网上可以下载到的版本&#xff0c;是经过了很多轮测试得到的稳定版本。 release&#xff1a; 开发内部发版&#xff0c;也就是测试环境。 dev&#xff1a;所有的feature都要从dev上checkout。 fea…

【C51 GPIO的原理和内部结构】

51单片机项目基础篇 中篇&#xff1a;介绍GPIO1、认识GPIO2、GPIO 结构框图与工作原理2.1、P0端口结构框图与工作原理2.1.1、剖析组成 P0 口的每个单元的作用2.1.2、 P0 口做为 I/O 口及地址/数据总线使用时的具体工作过程 2.2、P1 端口结构框图与工作原理2.3、P2 端口结构框图…

求生之路2社区服务器sourcemod安装配置搭建教程centos

求生之路2社区服务器sourcemod安装配置搭建教程centos 大家好我是艾西&#xff0c;通过上文我们已经成功搭建了求生之路2的服务端。但是这个服务端是纯净的服务端&#xff0c;就是那种最纯粹的原版。如果想要实现插件、sm开头的命令等功能&#xff0c;需要安装这个sourcemod。…

JavaScript(笔记)

目录 Hello World JavaScript 的变量 JavaScript 动态类型 隐式类型转换 JavaScript 数组 JavaScript 函数 JavaScript 中变量的作用域 对象 DOM 选中页面元素 事件 获取 / 修改元素内容 获取 / 修改元素属性 获取 / 修改 表单元素属性 获取 / 修改样式属性 新…

如何可以管理监督员工工作微信?

自从微信管理系统研发上线之后&#xff0c;为了各企业带来了福音。 很多用户企业都是这样评论微信管理系统的&#xff1a;员工的所有微信聊天记录后台都可以清楚明了的看到&#xff0c;聊天记录都是永久保存的&#xff0c;不担心员工在手机上把聊天记录删除&#xff0c;杜绝员…

基于黑猩猩算法优化的BP神经网络(预测应用) - 附代码

基于黑猩猩算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于黑猩猩算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.黑猩猩优化BP神经网络2.1 BP神经网络参数设置2.2 黑猩猩算法应用 4.测试结果&#xff1a;5.Matlab代…

MyBatis 的关联关系配置 一对多,一对一,多对多 关系的映射处理

目录 一.关联关系配置的好处 二. 导入数据库表&#xff1a; 三. 一对多关系&#xff1a;-- 一个订单对应多个订单项 四.一对一关系&#xff1a;---一个订单项对应一个订单 五.多对多关系&#xff08;两个一对多&#xff09; 一.关联关系配置的好处 MyBatis是一…

Java——它要求用户输入一个整数(实际上是一个字符串),然后计算该整数的平方值,并将结果输出。

这是一个Java程序&#xff0c;它要求用户输入一个整数&#xff08;实际上是一个字符串&#xff09;&#xff0c;然后计算该整数的平方值&#xff0c;并将结果输出。程序的基本流程如下&#xff1a; 首先&#xff0c;声明并初始化变量data和result&#xff0c;它们的初始值都为…

咸鱼之王俱乐部网站开发

我的俱乐部 最新兑换码 *注意区分大小写&#xff0c;中间不能有空格&#xff01; APP666 HAPPY666 QQ888 QQXY888 vip666 VIP666 XY888 app666 bdvip666 douyin666 douyin777 douyin888 happy666 huhushengwei888 taptap666 周活动 宝箱周 宝箱说明 1.木质宝箱开启1个…

Netty入门学习和技术实践

Netty入门学习和技术实践 Netty1.Netty简介2.IO模型3.Netty框架介绍4. Netty实战项目学习5. Netty实际应用场景6.扩展 Netty 1.Netty简介 Netty是由JBOSS提供的一个java开源框架&#xff0c;现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具&…