linux文件编程_线程

news2024/11/28 11:42:00

1. 基本概念

1.1. 进程与线程的概念

典型的UNIX/linux进程可以看成是只有一个控制线程,一个进程在同一时刻只做一件事情,有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。

一个进程至少包含一个线程

进程 进程是程序执行时的一个实例,即它是程序已经执行到何种程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。

前边说过由用户所编辑指令的c文件在没有运行起来的时候是一个程序,然后经过编译变成一个可执行文件(.exe / a.out),最终将可执行文件运行起来就变成了一个进程。进程本身占用系统资源,当启动了进程以后,在用户空间中会占用数据段、代码段和堆栈段。所以进程对资源开销比较大。

线程 线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

1.2. 进程与线程的区别

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,最好用线程。

举一个例子:12306在节假日的期间访问人数变多,所有的用户查询车次等操作会向12306这个系统发送请求,那么服务器相应完这个请求会给用户返回一个界面。如果服务器端对每一个请求都启动一个新的进程去处理的话,那么这个系统的开销是很庞大的。而且进程间相互切换也同样比较耗费资源,如果在用户非常庞大的情况下,使用进程去处理大并发的请求,那么这个设计是十分不合理的,容易导致系统的瘫痪。那么就引入线程来处理这种大并发的请求。

1.3. 使用线程的好处:

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:

提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。

使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

1.4. 线程的概念

要点:进程是资源管理的最小单位,线程是程序执行的最小单位。

举个例子:现在假如创建了一个word,然后对新建的word进行编辑,保存等操作。如果这些操作都去启动一个进程去做的话,那显然不太现实,资源开销太大。实际上在word编辑的过程中是启动了多个线程去处理这些操作,进程只有新建word的这一个进程。

由此可见:线程是属于进程的,一个进程默认至少有一个线程,后续有多少个进程取决于用户后续的操作。可以说调用进程就工作,就是在启动线程去工作的。一个线程就是进程里的一个工作流,一个进程可以有多个工作流,所以线程是程序执行的最小单位。

每个进程都有自己的数据段、代码段和堆栈段,线程隶属于某个进程,它包含独立的栈和CPU寄存器状态,但是它共享这个进程所有的用户空间,因为它本身属于这个进程。包括打开的文件、内存页面、信号标识及动态分配内存等。

对于线程的资源开销几乎可以忽略不计,可以减小进程上下文切换的开销。

2. 线程相关函数

多线程开发在linux平台上已经有成熟的 pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。

线程操作有:创建,退出,等待。

互斥锁操作有:创建,销毁,加锁和解锁。

条件操作有 :创建,销毁,触发,广播和等待。

其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。详细请见下表:

2.1. 线程创建函数pthread_create()原型和头文件:
/*
	Linux下 man pthread_create查看手册
*/
#include <pthread.h>  //头文件
 
int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, 
                   void *(*start_rtn)(void *), void *restrict arg);
 
int 	函数返回值,成功pthread_create()返回0;当出现错误时,它返回一个错误编号,并且*thread的内容是未定义的
 
tidp		指向线程标识符的指针
attr		用来设置线程属性,如果没有则填NULL。
start_rtn	表示线程运行函数的地址
arg			线程运行函数的参数,如果没有参数则填NULL
  • 函数详解:

当 pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。

新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

2.2. 线程创建函数pthread_create()用法案例:
#include <stdio.h>
#include <pthread.h>

/*
*  	 主线程创建一个新线程th1,执行func1函数。
*  	 在func1中,打印新线程的ID和传递的数据(100)。
*    主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    int data = 100;
 
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }

        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,NULL);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    return 0;
}

2.3. 线程退出函数pthread_exit()原型和头文件:

单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:

  1. 线程只是从启动例程中返回,返回值是线程的退出码。
  2. 线程可以被同一进程中的其他线程取消。
  3. 线程调用pthread_exit。
/*
	Linux下 man pthread_exit查看手册
*/
#include <pthread.h>
 
int pthread_exit(void *retval);
 
int				函数返回值,成功pthread_exit()返回0;当出现错误时,它返回一个错误编号
    
void *retval	表示线程退出状态,是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函				数访问到这个指针。

函数详解:

在线程中禁止调用exit函数,否则会导致整个进程退出,取而代之的是调用pthread_exit函数,这个函数是使一个线程退出,如果主线程调用pthread_exit函数也不会使整个进程退出,不影响其他线程的执行。 需要注意一点,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了,栈空间就会被回收。 如果在主线程中使用pthread_exit函数,会导致子线程还在,内存无法被回收,成为僵尸进程。引入pthread_join函数。

2.4. 线程等待函数pthread_join()原型和头文件:
/*
	Linux下 man pthread_join查看手册
*/
#include <pthread.h>
 
int pthread_join(pthread_t thread, void **retval);
 
int 				函数返回值,如果执行成功pthread_join返回0,当出现错误时,它返回一个错误编号
    
pthread_t thread	线程ID
void **retval		存储线程结束状态,整个指针和pthread_exit的参数是同一块内存地址
  • 函数详解:

调用这个函数的线程将一直阻塞,直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。

可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。

如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。

2.5. 线程脱离函数pthread_detach()原型和头文件:
/*
	Linux下 man pthread_detach查看手册
*/
#include <pthread.h>
 
int pthread_detach(pthread_t thread);
 
int 				函数返回值,如果成功pthread_detach返回0,当出现错误时,它返回一个错误编号
    
pthread_t thread	需要脱离的线程(标识符)  
  • 函数详解:

一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。 当一个可汇合的线程终止时: 它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。 当一个脱离的线程终止时:像守护进程一样,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。 如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。

2.6. 获取线程ID函数pthread_self()原型和头文件:
/*
	Linux下 man pthread_self查看手册
*/
#include <pthread.h>
 
pthread_t pthread_self(void);
 
pthread_t	函数返回值,返回调用线程的ID
  • 函数详解:

对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用pthread_self函数获取一个线程的ID 此函数可与pthread_detach函数配合使用,想让自己脱离的线程使用,就如以下语句:

pthread_detach(pthread_self());
2.7. 线程ID比较函数pthread_equal()原型和头文件:
/*
	Linux下 man pthread_equal查看手册
*/
#include <pthread.h>
 
int pthread_equal(pthread_t tid1, pthread_t tid2);
 
int 				函数返回值,如果两个线程id相等,pthread_equal()返回一个非零值;否则返回0
 
pthread_t tid1		需要进行比较的第一个线程ID
pthread_t tid2		需要进行比较的第二个线程ID    
2.8. 子线程退出主线程获取子线程的退出状态(一):
#include <stdio.h>
#include <pthread.h>

/*
*  	 主线程创建一个新线程th1,执行func1函数。
*  	 在func1中,打印新线程的ID和传递的数据(100)。
*    主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
     static int ret = 10;
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
   
    pthread_exit((void*)&ret);
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    int data = 100;
   int *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }

        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,(void**)&pret);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    //
    printf("mat : t1 quit :%d \n",*pret);
    return 0;
}
  • static char *str = "th1 is run out"; 是为了确保这个字符串的值在整个程序运行期间都有效。当线程退出时,返回这个字符串的地址是安全的,因为它的值不会消失。
  • static 让一个变量的值在整个程序运行期间保持有效,即使它在函数内部定义。这样在多线程或函数间调用时,变量的值不会丢失

2.9. 子线程退出主线程获取子线程的退出状态(二):
void* func1(void *arg)
{

    static char *str = "th1 is run out";                           //必须使用static修饰,否则值会发生错误
    // static int ret = 10;
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
   
    pthread_exit((void*)str);
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    int data = 100;
   char *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }

        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,(void**)&pret);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    //
    printf("mat : t1 quit :%s \n",pret);
    return 0;
}  

2.10. 传递给线程运行函数的参数为一个结构体:
void* func1(void *arg)
{

    static char *str = "th1 is run out";                           //必须使用static修饰,否则值会发生错误
    // static int ret = 10;
     struct Test *p =(struct Test *)arg
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:年龄 = %d\n",p->data);  
    printf("th1:姓名 = %s\n",p->str); 
   
    pthread_exit((void*)str);
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
   
   struct Test data = {100,"小刘"};
   
   char *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }

        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,(void**)&pret);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    //
    printf("mat : t1 quit :%s \n",pret);
    return 0;
}  

3. 互斥锁相关函数

3.1.1. 示例:线程的共享资源
  • 双线程
/*
*  	 主线程创建一个新线程th1,执行func1函数。
*  	 在func1中,打印新线程的ID和传递的数据(100)。
*    主线程打印其自己的ID,然后等待th1结束。
*/
void* func1(void *arg)
{
     static int ret = 10;
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
   
    pthread_exit((void*)&ret);
}
void* func2(void *arg)
{
     static int ret = 10;
    printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th2:data = %d\n",*((int *)arg));  // 转换成int data
   
    pthread_exit((void*)&ret);

}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    pthread_t th2;      //线程标识符
    int data1 = 100;
    int data2 = 100;
   int *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data1);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }
    ret = pthread_create(&th2, NULL, func2, (void*)&data2);             
    if(ret == 0){
        printf("main:创建th2线程成功\n");
    }
        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,(void**)&pret);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    //
    //
    printf("mat : t1 quit :%d \n",*pret);
    printf("mat : t2 quit :%d \n",*pret);
    return 0;
}

注意:由于两个线程使用的是同一个内存,没有使用先后执行函数,线程有着竞争的关系,所以运行之后每次的结果顺序是不一样的。

/*
*  	 主线程创建一个新线程th1,执行func1函数。
*  	 在func1中,打印新线程的ID和传递的数据(100)。
*    主线程打印其自己的ID,然后等待th1结束。
*/
int g_data =0;

void* func1(void *arg)
{
     static int ret = 10;
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
   

	while(1){
		printf("t1:%d\n",g_data++);
		sleep(1);
	}
}
void* func2(void *arg)
{
     static int ret = 10;
    printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th2:data = %d\n",*((int *)arg));  // 转换成int data
   	while(1){
		printf("t2:%d\n",g_data++);
		sleep(1);
	}
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    pthread_t th2;      //线程标识符
    int data1 = 100;
    int data2 = 100;
    int *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data1);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }
    ret = pthread_create(&th2, NULL, func2, (void*)&data2);             
    if(ret == 0){
        printf("main:创建th2线程成功\n");
    }
        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
	while(1){
		printf("man:%d\n",g_data++);
		sleep(1);
	}
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,NULL);                                        
    pthread_join(th2,NULL);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
  
    return 0;
}

th1的退出条件是基于g_data的值,但由于g_data被多个线程修改,它可能会在未达到3时就被其他线程影响。因此,th1可能不会按照预期正常退出。

上边的全局变量g_data就是一个共享资源,每一个线程都能访问这个资源,并且对这个资源进行修改。虽然极大的方便了线程之间的通信,但是这种方法访问数据的方式不安全,可以使用后边的互斥和同步来解决。(或者使用局部变量,尽量减少全局变量的使用)

/*
*  	 主线程创建一个新线程th1,执行func1函数。
*  	 在func1中,打印新线程的ID和传递的数据(100)。
*    主线程打印其自己的ID,然后等待th1结束。
*/
int g_data =0;

void* func1(void *arg)
{
     static int ret = 10;
    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data = %d\n",*((int *)arg));  // 转换成int data
   

	while(1){
		printf("t1:%d\n",g_data++);
		sleep(1);
		if(g_data++ == 3){
		pthread_exit(NULL);
	}
	}
}
void* func2(void *arg)
{
     static int ret = 10;
    printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th2:data = %d\n",*((int *)arg));  // 转换成int data
   	while(1){
		printf("t2:%d\n",g_data++);
		sleep(1);
	}
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    pthread_t th2;      //线程标识符
    int data1 = 100;
    int data2 = 100;
   int *pret = NULL;
    //	int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
    //	void *(*start_routine) (void *), void *arg);

    //创建了一个th1线程							//转换成无类型的地址    
    ret = pthread_create(&th1, NULL, func1, (void*)&data1);             
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }
    ret = pthread_create(&th2, NULL, func2, (void*)&data2);             
    if(ret == 0){
        printf("main:创建th2线程成功\n");
    }
        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());    
 
	while(1){
		printf("man:%d\n",g_data++);
		sleep(1);
	}
    //	int pthread_join(pthread_t thread, void **retval);
    
    pthread_join(th1,NULL);                                        
    pthread_join(th2,NULL);                                        
    //等待th1线程终止,但是不获取th1线程终止状态
    //
    //
    return 0;
}

互斥量(mutex)从本质上来说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去等待它重新变为可用。在这种方式下,每次只有一个线程可以向前运行。

在设计时需要规定所有的线程必须遵守相同的数据访问规则。只有这样,互斥机制才能正常工作。操作系统并不会做数据访问的串行化。如果允许其中的某个线程在没有得到锁的情况下也可以访问共享资源,那么即使其它的线程在使用共享资源前都获取了锁,也还是会出现数据不一致的问题。

互斥变量用pthread_mutex_t数据类型表示。在使用互斥变量前必须对它进行初始化,可以把它置为常量PTHREAD_MUTEX_INITIALIZER(只对静态分配的互斥量),也可以通过调用pthread_mutex_init函数进行初始化。如果动态地分配互斥量(例如通过调用malloc函数),那么在释放内存前需要调用pthread_mutex_destroy。

3.1. 创建互斥锁函数pthread_mutex_init()原型和头文件:
#include <pthread.h>
 
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
 
int 										函数返回值,若成功返回0,失败返回错误编号
    
pthread_mutex_t *restrict mutex				是指想要初始化的互斥锁的指针
const pthread_mutexattr_t *restrict attr    用于指定互斥锁属性,如果为NULL则使用默认属性(缺省属性)
3.2. 互斥锁属性

互斥锁的属性在创建锁的时候指定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现不同。当前(glibc2.2.3,linuxthreads0.9)有四个值可供选择:

  • PTHREAD_MUTEX_TIMED_NP,缺省值,也就是普通锁。当一个线程加锁以后,其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。这种锁策略保证了资源分配的公平性。
  • PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。
  • PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,则返回EDEADLK,否PTHREAD_MUTEX_TIMED_NP类型动作相同。这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
  • PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
3.2.1. 销毁互斥锁函数pthread_mutex_destroy()原型和头文件:
#include <pthread.h>
 
int pthread_mutex_destroy(pthread_mutex_t *mutex);
 
int 						函数返回值,若成功返回0,失败返回错误编号
    
pthread_mutex_t *mutex		是指想要销毁的互斥锁指针
3.2.2. 加锁pthread_mutex_lock()和解锁pthread_mutex_unlock()函数原型和头文件:
#include <pthread.h>
 
int pthread_mutex_lock(pthread_mutex_t *mutex);		//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);	//解锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);	//测试加锁
 
int 						函数返回值,若成功返回0,否则返回错误编号
    
pthread_mutex_t *mutex		想要操作的互斥锁指针

如果线程不希望被阻塞,它可以使用pthread_mutex_trylock尝试对互斥量进行加锁。如果调用pthread_mutex_trylock时互斥量处于未锁住状态,那么pthread_mutex_trylock将锁住互斥量,不会出现阻塞并返回0,否则pthread_mutex_trylock就会失败,不能锁住互斥量,而返回EBUSY。

3.3. 线程与互斥锁应用实例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

int g_data =0;
pthread_mutex_t mutex;      //定义互斥量

void* func1(void *arg)
{

    printf("th1:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
    printf("th1:data1 = %d\n",*((int *)arg));  // 转换成int data
    
    int ret1 = pthread_mutex_lock(&mutex);  //	加锁
    
	if(ret1 != 0){
  	      printf("加锁失败\n");
		}
    while(1){
    	
		printf("th1: data11 = %d\n",g_data++);
		sleep(1);

		if(g_data++ == 3){
				ret1 = pthread_mutex_unlock(&mutex);  //	解锁
				printf("th1退出============\n");
					if(ret1 != 0){
						printf("解锁失败\n");
					}
				pthread_exit(NULL);	
		}
    }

}
void* func2(void *arg)
{
    
        
	printf("th2:%ld是新线程ID\n",(unsigned long)pthread_self());    //获取新线程的ID
	printf("th2:data22 = %d\n",*((int *)arg));  // 转换成int data
   
   
    while(1){
    	
		printf("th2: data = %d\n",g_data);
		int ret2 = pthread_mutex_lock(&mutex);  //加锁:
    
		if(ret2 != 0){
				printf("加锁失败\n");
			}
			g_data++;

		ret2 = pthread_mutex_unlock(&mutex);  //加锁
			if(ret2 != 0){
				printf("解锁失败\n");
			}
		sleep(1);
	}
}
int main()
{
    int ret;
    pthread_t th1;      //线程标识符
    pthread_t th2;      //线程标识符
    int data1 = 100;
    int data2 = 100;
   int *pret = NULL;

     ret = pthread_mutex_init(&mutex,NULL);                          //创建互斥锁
    if(ret != 0){
        printf("创建互斥锁失败\n");
    }

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

    //创建了一个th1线程							//转换成无类型的地址
    ret = pthread_create(&th1, NULL, func1, (void*)&data1);
    if(ret == 0){
        printf("main:创建th1线程成功\n");
    }
    ret = pthread_create(&th2, NULL, func2, (void*)&data2);
    if(ret == 0){
        printf("main:创建th2线程成功\n");
    }
        //获取主线程的ID
    printf("main:线程ID = %ld\n",(unsigned long)pthread_self());

	while(1){
		printf("man:%d\n",g_data);
		sleep(1);
	}
    //	int pthread_join(pthread_t thread, void **retval);

    pthread_join(th1,NULL);
    pthread_join(th2,NULL);
    //等待th1线程终止,但是不获取th1线程终止状态
	
	ret = pthread_mutex_destroy(&mutex);   	//条件变量销毁  
    if(ret != 0){
        printf("main:条件变量销毁失败\n");

    }
    return 0;
}

  • 线程1 (func1) 会在 g_data 达到3时退出,线程2 (func2) 会一直运行,不断修改 g_data
  • 主线程 (main) 也会持续运行,打印 g_data 的值。
  • 线程1先夺得互斥锁,直到线程1满足退出条件,解锁后,线程2的主要代码部分才开始运行。
  • 代码分析

首先主控线程创建了两个新的线程,然后在主控线程中打印g_data的值,此时g_data的值为0,主控线程休眠一秒,此时子进程2抢占到了CPU,执行子进程2的程序,子进程2同样打印了g_data的值,此时g_data的值没有发生变化。子进程2休眠一秒,子进程1抢占到CPU,此时执行子进程1的代码,子进程1首先加互斥锁,然后执行g_data++会先打印后会它的值加加,此时g_data的值已经变为1。子进程休眠一秒后被主控线程捕获,此时打印g_data的值为1,然后休眠一秒,此时可能子进程2抢占到了CPU,继续接着之前的代码执行,子进程2尝试去上锁,但此时这把锁已经被子进程1获取,所以子进程2就被阻塞,继而接着去执行子进程1的程序,直到子进程检测到g_data的值变为3后解锁并将整个进程退出。

3.4. 死锁

死锁是指两个或多个执行单元(如进程、线程或任务)在等待彼此持有的资源时,导致它们都无法向前推进执行的一种现象。

产生死锁的条件:

  • 互斥条件:至少有一个资源每次只能被一个执行单元使用。
  • 占有和等待条件:一个执行单元至少持有一个资源,并等待获取其他执行单元持有的资源。
  • 不剥夺条件:执行单元已获得的资源,在未使用完之前,不能被其他执行单元强行剥夺。
  • 循环等待条件:存在一种执行单元资源的循环等待链。

简单来说,死锁就是目前有两把锁,它们各自都想拿到对方的那把锁,但是拿不到最终导致线程一直阻塞的现象。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

int g_data = 0;
pthread_mutex_t mutex;		//定义互斥锁类型
pthread_mutex_t mutex2;


void *pthread_func1(void  *arg)
{
	pthread_mutex_lock(&mutex);		//加锁,对共享资源进行保护
	sleep(1);
	pthread_mutex_lock(&mutex2);
	while(1)
	{
		printf("tid1: %d\n",g_data++);
		sleep(1);
		if(g_data == 3)
		{
			printf("tid1 quit==========\n");
			exit(0);
			pthread_mutex_unlock(&mutex);		//解锁,使得其他线程能够获取到这个锁
			pthread_exit(NULL);
		}
	}
}

void *pthread_func2(void *arg)
{
	pthread_mutex_lock(&mutex2);
	sleep(1);
	pthread_mutex_lock(&mutex);

	while(1)
	{
		printf("tid2: %d\n",g_data);
		sleep(1);
		pthread_mutex_lock(&mutex);
		g_data++;
		pthread_mutex_unlock(&mutex);
	}
}

int main()
{
	pthread_t tid1,tid2;
	int  ret;

	pthread_mutex_init(&mutex,NULL);	//对互斥锁进行初始化
	pthread_mutex_init(&mutex2,NULL);

	if((ret = pthread_create(&tid1,NULL,pthread_func1,NULL)) == 0)
	{
		printf("main: create new thread successfully\n");
	}

	if((ret = pthread_create(&tid2,NULL,pthread_func2,NULL)) == 0)
	{
		printf("main: create new thread successfully\n");
	}

	while(1)
	{
		printf("main: %d\n",g_data);
		sleep(1);
	}
	
	pthread_mutex_destroy(&mutex);		//对互斥锁进行销毁
	pthread_mutex_destroy(&mutex2);
	return 0;
}

什么是死锁,就是有两个锁 1,2锁,线程a先定义加了个锁1,之后加了个锁2,线程b先定义加了个锁2,之后加了个锁1,a线程锁1加完之后,没有解锁,等带锁2,结果锁2被线程2加锁了,没有解锁。两个线程就互相等待对方的锁,就相互卡死了,导致程序陷入死锁。

4. 条件变量相关函数

条件变量:本身不是锁,但可以造成线程阻塞,通常与互斥锁配合使用; 条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待 "条件变量的条件成立 "而挂起;另一个线程使 "条件成立 "(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。

4.1. 创建条件变量函数pthread_cond_init()原型和头文件:
#include <pthread.h>
 
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
 
int 								函数返回值,若成功返回0,失败返回错误编号
    
pthread_cond_t *restrict cond		想要初始化的条件变量的指针
pthread_condattr_t *restrict attr	用于指定互斥锁属性,如果为NULL则使用默认属性(缺省属性)
4.2. 销毁条件变量函数pthread_cond_destroy()原型和头文件:
#include <pthread.h>
 
int pthread_cond_destroy(pthread_cond_t *cond);
 
int 					函数返回值,若成功返回0,失败返回错误编号
    
pthread_cond_t *cond	想要销毁的条件变量的指针
4.3. 条件变量等待函数pthread_cond_wait()原型和头文件(无条件等待):
#include <pthread.h>
 
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
 
int 								函数返回值,若成功返回0,失败返回错误编号
    
pthread_cond_t *restrict cond		条件变量指针
pthread_mutex_t *restrict mutex		互斥锁指针

pthread_cond_wait等待条件变为真。如果在给定的时间内条件不能满足,那么会生成一个代表一个出错码的返回变量。传递给pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传给函数。函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁,这两个操作都是原子操作。这样就关闭了条件检查和线程进入休眠状态等待条件改变这两个操作之间的时间通道,这样线程就不会错过条件的任何变化。pthread_cond_wait返回时,互斥量再次被锁住。

4.4. 条件变量等待函数pthread_cond_timedwait()原型和头文件(计时等待):
#include <pthread.h>
 
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, 
                           struct timespec *restrict abstime);
 
int 								函数返回值,若成功返回0,失败返回错误编号
    
pthread_cond_t *restrict cond		条件变量指针
pthread_mutex_t *restrict mutex		互斥锁指针
    
struct timespec *restrict abstime	是一个绝对时间,Linux中常用的时间结构有struct timespec 和 struct timeval
    
#include <sys/time.h>	//需要包含的头文件
 
struct timespec
{
        time_t tv_sec;        /* Seconds. */
        long   tv_nsec;       /* Nanoseconds. */
};
struct timeval {
        time_t tv_sec;  
        suseconds_t tv_usec;
};
    
/*这里面的超时时间是一个绝对值,也就是距离1970-1-1 日的时间值,而不是一个时间段。比如说当前时间为2024-6-8 14:43:00.100,我们想通过这个函数设置最大超时为2500ms,那么就需要设置abstime时间为2024-6-8 14:43:02.600.*/
 
/*abstime参数设置实例*/
struct timespec abstime;
struct timeval now;
gettimeofday(&now, NULL);
abstime.tv_sec = now.tv_sec + 5;
abstime.tv_nsec = now.tv_usec * 1000;

pthread_cond_timedwait函数的工作方式与pthread_cond_wait函数类似,只是多了一个abstime。abstime指定了等待的时间,它是通过timespec结构指定

4.5. 条件变量触发函数pthread_cond_signal和pthread_cond_broadcast原型和头文件:
#include <pthread.h>
 
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
 
int 					函数返回值,若成功返回0,失败返回错误编号
    
pthread_cond_t *cond	条件变量指针
4.6. 条件变量与互斥量结合案例:
  • 当线程2操作全局变量num为3时,线程1执行
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
 
pthread_mutex_t mutex;      //互斥锁
pthread_cond_t cond;        //条件变量
int num = 0;                //共享变量
 
void* func1(void *arg)
{
    printf("th1:线程ID = %ld\n",(unsigned long)pthread_self());     //获取th1线程ID
    printf("th1:参数 = %d\n",*((int *)arg));                        //打印参数
 
    while(1){
        pthread_cond_wait(&cond, &mutex);                            //条件变量等待
        printf("th1:num = %d\n",num);                                //打印共享变量
        printf("th1 run=============================\n"); 
        num = 0;                                                     //重置共享变量
        sleep(1);                                                    //睡眠1秒
    }
    pthread_exit(NULL);                                              //线程退出
}
 
void* func2(void *arg)
{
    printf("th2:线程ID = %ld\n",(unsigned long)pthread_self());     //获取th2线程ID
    printf("th2:参数 = %d\n",*((int *)arg));                        //打印参数                                   
 
    while(1){
        printf("th2:num = %d\n",num);                               //打印共享变量
        pthread_mutex_lock(&mutex);                                 //加锁
        num++;                                                      //加1
        if(num == 3){
            pthread_cond_signal(&cond);                             //条件变量通知
        }
        pthread_mutex_unlock(&mutex);                               //解锁
        sleep(1);                                                   //睡眠1秒
    }
    
    pthread_exit(NULL);                                              //线程退出
}
 
int main()
{
    int ret;
    int data = 100;
    pthread_t th1;  //th1线程标识符
    pthread_t th2;  //th2线程标识符
 
    //int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
    ret = pthread_mutex_init(&mutex, NULL);                     //互斥锁初始化  
    if(ret != 0){
        printf("main:mutex初始化失败\n");
    }
 
    //int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
    ret = pthread_cond_init(&cond, NULL);                        //条件变量初始化   
    if(ret != 0){
        printf("main:条件变量初始化失败\n");
    }
 
    //int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void *), void *restrict arg);
    ret = pthread_create(&th1, NULL, func1, (void *)&data);     //创建th1线程
    if(ret == 0){                                                       
        printf("main:th1线程创建成功\n");                               
    }
 
    ret = pthread_create(&th2, NULL, func2, (void *)&data);     //创建th2线程
    if(ret == 0){
        printf("main:th2线程创建成功\n");
    }
 
    pthread_join(th1,NULL);                                    //等待th1线程退出
    pthread_join(th2,NULL);                                    //等待th2线程退出
 
    //int pthread_mutex_destroy(pthread_mutex_t *mutex);
    ret = pthread_mutex_destroy(&mutex);                        //互斥锁销毁    
    if(ret != 0){
        printf("main:mutex销毁失败\n");
    }
 
    //int pthread_cond_destroy(pthread_cond_t *cond);
    ret = pthread_cond_destroy(&cond);                          //条件变量销毁  
    if(ret != 0){
        printf("main:条件变量销毁失败\n");
    }
    return 0;
}

编译结果

代码分析:当主控线程创建新线程后,系统可能会给线程1分配时间片,此时线程1调用函数pthread_cond_wait等待条件满足,但此时条件不满足,所以进入到阻塞状态。系统转而去执行线程2,当经过三轮的执行后,满足条件,线程2调用pthread_cond_signal函数唤醒线程1执行,然后线程1重新将g_data的值赋值为0,以此类推的执行。

4.7. 静态初始化互斥锁和条件变量:

对于互斥锁与条件变量的初始化,也可以用静态初始化即使用宏,如:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER          //互斥锁
                    == 
    pthread_mutex_init(&mutex, NULL); 
pthread_cond_t cond   = PTHREAD_COND_INITIALIZER           //条件变量   
                    == 
    pthread_cond_init(&cond, NULL); 

这部分代码是在C语言中使用pthread库来创建互斥锁和条件变量的实例。互斥锁用于保护共享资源,以确保在同一时间只有一个线程可以访问它。条件变量用于在线程之间的同步,允许一个线程在满足特定条件之前等待,或者当条件发生变化时通知其他线程。

代码中使用了PTHREAD_MUTEX_INITIALIZER和PTHREAD_COND_INITIALIZER宏,它们分别用于静态地初始化互斥锁和条件变量,确保它们被正确地设置为默认值。

总的来说,这部分代码的主要功能是初始化了一个互斥锁和一个条件变量,以便后续在多线程中使用。


点个赞吧老铁!!!

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

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

相关文章

Web安全 - 文件上传漏洞(File Upload Vulnerability)

文章目录 OWASP 2023 TOP 10导图定义攻击场景1. 上传恶意脚本2. 目录遍历3. 覆盖现有文件4. 文件上传结合社会工程攻击 防御措施1. 文件类型验证2. 文件名限制3. 文件存储位置4. 文件权限设置5. 文件内容检测6. 访问控制7. 服务器配置 文件类型验证实现Hutool的FileTypeUtil使用…

STM32使用Keil5 在运行过程中不复位进入调试模式

一、选择Options for Target进入设置 二、选择所使用的调试器&#xff0c;这里以ST-Link为例。取消勾选Load Application at Startup 可以在进入调试模式的时候不会从新加载程序&#xff01;从而不破坏现场 三、点击Setting进入 四、取消勾选Reset after Connect 使得调试器连接…

探索 aMQTT:Python中的AI驱动MQTT库

文章目录 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库背景介绍aMQTT是什么&#xff1f;如何安装aMQTT&#xff1f;简单库函数使用方法场景应用常见问题及解决方案总结 探索 aMQTT&#xff1a;Python中的AI驱动MQTT库 背景介绍 在物联网和微服务架构的浪潮中&#xff0c;MQ…

Redis:string类型

Redis&#xff1a;string类型 string命令设置与读取SETGETMSETMGET 数字操作INCRINCRBYDECRDECRBYINCRBYFLOAT 字符串操作APPENDSTRLENGETRANGESETRANGE 内部编码intembstrraw 在Redis中&#xff0c;字符串string存储的是二进制&#xff0c;以byte为单位&#xff0c;输入的二进…

ICPC-day1(NTT)

NTT经典例题 CCPC-Winter-Camp-day6-A——NTT经典例题 对于上面格式&#xff0c;如果想求出每个i的值可以使用卷积求出&#xff0c;因为阶乘j和阶乘i-j相乘的值为(i(i-j))i 补充一个二次剩余定理 P5491 【模板】二次剩余 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) //#in…

【MySQL】DML数据操作语句和基本的DQL语句

目录 一、Mysql对数据的增删改 1. 增加数据 2. 修改数据&#xff08;UPDATE语句&#xff09; 3. 删除 3.1 delete、truncate、drop区别 二、DQL语言&#xff08;重点&#xff09; 1. 单表查询 1.1 最简单的查询 1.2 从表中获取数据 1.3 字段名起别名 1.4 添加字段 1…

[20231103消息] 大模型商业化模式详解:烧钱之后如何挣钱?

距ChatGPT3.5发布已近一年&#xff0c;大模型狂热开始逐步降温&#xff1a;GPU禁运及长期烧钱的事实&#xff0c;让国内的大模型企业&#xff0c;不得不加速商业化考量。 目前&#xff0c;大模型的B端应用已经出现各种定价方法&#xff0c;包括按照时间段收费、按调用量收费以…

class 030 异或运算的骚操作

这篇文章是看了“左程云”老师在b站上的讲解之后写的, 自己感觉已经能理解了, 所以就将整个过程写下来了。 这个是“左程云”老师个人空间的b站的链接, 数据结构与算法讲的很好很好, 希望大家可以多多支持左程云老师, 真心推荐. https://space.bilibili.com/8888480?spm_id_f…

Java中Map和Set详细介绍,哈希桶的实现

大家好呀&#xff0c;前一节我们接触了二叉搜索树&#xff0c;那么紧接着&#xff0c;我们要学习一种十分重要而且也是我们在初阶数据结构中接触的最后一种数据结构—Map和Set&#xff0c;本篇博客将会详细介绍两种数据结构&#xff0c;并且针对哈希表底层实现一个哈希桶&#…

基于元神操作系统实现NTFS文件操作(三)

1. 背景 本文主要介绍DBR的读取和解析&#xff0c;并提供了基于元神操作系统的实现代码。由于解析DBR的目的是定位到NTFS磁盘分区的元文件$Root进行文件操作&#xff0c;所以只解析了少量的部分&#xff0c;其它部分可以参考相关文档进行理解。 DBR存在于磁盘分区的第一个扇区…

《数据结构》--链表【包含跳表概念】

不知道大家对链表熟悉还是陌生&#xff0c;我们秉着基础不牢&#xff0c;地动山摇的原则&#xff0c;会一点点的介绍链表的&#xff0c;毕竟链表涉及的链式存储也很重要的。在这之前&#xff0c;我们认识过顺序存储的顺序表&#xff0c;它其实就是一个特殊的数组。那链表到底是…

树莓派 AI 摄像头(Raspberry Pi AI Camera)教程

系列文章目录 前言 人们使用 Raspberry Pi 产品构建人工智能项目的时间几乎与我们生产 Raspberry Pi 的时间一样长。随着我们发布功能越来越强大的设备&#xff0c;我们能够支持的原生应用范围也在不断扩大&#xff1b;但无论哪一代产品&#xff0c;总会有一些工作负载需要外部…

SpringBoot介绍及整合Mybatis Plus

目录 SpringBoot背景及特点 SpringBoot整合Mybatis Plus SpringBoot背景及特点 SpringBoot的设计目是抛弃之前Spring、SpringMVC繁杂的配置过程&#xff0c;简化开发过程。之前的Spring框架需要大量的手动配置&#xff0c;包括XML配置文件或Java配置类&#xff0c;配置过程繁…

深入理解 Git 一个开发者的必备工具

深入理解 Git 一个开发者的必备工具 演示地址 演示地址 获取更多 获取更多 在现代软件开发中&#xff0c;版本控制系统扮演着至关重要的角色。其中&#xff0c;Git 是最流行的选择之一。无论你是新手还是有经验的开发者&#xff0c;了解 Git 的基本概念和使用方法都能大大提…

YOLO v11实时目标检测3:训练数据集格式说明

一、Yolov11简介 YOLOv11 是 YOLO 系列的最新版本&#xff0c;它不仅在目标检测方面表现出色&#xff0c;还引入了对象分割和多目标跟踪的功能。本文将介绍如何使用 YOLOv11 进行人流统计、车流统计以及跟踪的实际应用。 二、Yolo v11训练数据集格式说明 2.1 数据组织&#…

SAT分离轴定理的c++/python实现

分离轴定理的c/python实现 现在要对BEV模型检查出来的车辆做NMS&#xff0c;把3d框的平面属性获取到后&#xff0c;配合旋转角度投影到地面就是2D图形。 开始碰撞检测&#xff0c;判断是否重叠&#xff0c;保留置信度高的框就行。 原理 分离轴定理&#xff08;Separating A…

(C语言贪吃蛇)11.贪吃蛇方向移动和刷新界面一起实现面临的问题

目录 前言 实现效果 支持方向变换 修改默认效果 如何修改 总结 前言 我们上节实现了不需要按下右键就可以是贪吃蛇自发的向右移动&#xff0c;本节我们主要来解决贪吃蛇方向移动和刷新界面所遇到的问题。 实现效果 上图是我们希望实现的效果&#xff0c;我们可以自发地控…

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统

Go 进阶:Go + gin 极速搭建 EcommerceSys 电商系统 前言 本章节适合有一定基础的 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。 具体请联系作者 项目结构 项目流程图 技术栈 项目结构 项目路由 4. 项目模型 项目初始化 初始化项目文…

归并排序【C语言版-笔记】

目录 一、概念二、排序流程理解三、代码实现3.1主调函数3.2 merge函数 四、性能分析 一、概念 归并是一种算法思想&#xff0c;是将两个或两个一上的有序表合并成一个长度较大的有序表。若一开始无序表中有n个元素&#xff0c;可以把n个元素看作n个有序表&#xff0c;把它们两…

Java中数据转换以及字符串的“+”操作

隐式转换&#xff08;自动类型转换&#xff09; 较小范围的数据类型转成较大范围的数据类型 强制转换&#xff08;显式转换&#xff09; 将数据范围大的数据类型转换为数据范围小的数据类型 基本数据类型之间的转换 当需要将一个较大的数据类型&#xff08;如float或double…