• c语言系统编程十二:Linux多任务编程之线程


    Linux多任务编程之线程

    一 线程概述

    1.1 概述

    1. 线程是轻量级的进程(LWP);
    2. 在Linux环境下线程的本质仍然是进程;
    3. 为了让进程完成一定的工作,进程必须至少包含一个线程;
    4. 进程是系统分配资源的基本单位;
    5. 线程是cpu执行调度的基本单位;
    6. 线程依赖于进程,线程共享进程的资源,线程独立的资源有(计数器,一组寄存器,栈);
    7. 进程结束,当前进程的所有线程都将立刻结束。

    在这里插入图片描述

    1.2 线程函数列表安装

    在这里插入图片描述

    1.3 线程的特点

    类Unix系统中,早期没有线程的概念,80年代才引入,借助进程机制实现出了线程的概念,因此在这类系统中,进程和线程关系密切:

    1. 线程是轻量级进程,也有PCB,创建线程使用的底层函数和进程一样,都是clone;
    2. 从内核里看进程和线程是一样的,都有各自不同的PCB;
    3. 进程可以蜕变成线程;
    4. 在Linux下,线程是最小的执行单位;进程是最小的分配资源单位;

    1.4 线程共享的资源

    1. 文件描述符表
    2. 每种信号的处理方式
    3. 当前工作目录
    4. 用户ID和组ID 内存地址空间(.text/.data/.bss/heap/共享库)

    1.5 线程不共享的资源

    1. 线程id
    2. 处理器现场和栈指针(内核栈)
    3. 独立的栈空间(用户空间栈)
    4. errno变量
    5. 信号屏蔽字
    6. 调度优先级

    1.6 在linux中查看指定进程的LWP号(线程id)

    ps -Lf pid
    
    • 1

    在这里插入图片描述

    查看进程号 pthread_self

    2.1 需要的头文件和函数原型

    #include 
    pthread_t pthread_self(void);
    
    • 1
    • 2

    2.2 函数功能

    获取线程号
    
    • 1

    2.3 参数

    • 1

    2.4 返回值

    调用函数的线程的线程id
    
    • 1

    2.5 实例

    #include 
    #include 
    #include 
    int main(int arg,char *args[])
    {
            printf("当前线程的线程id是%ld\n", pthread_self());
            getchar();
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述

    三 线程的创建 pthread_create

    3.1 需要的头文件和函数原型

    #include 
    int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
    
    • 1
    • 2

    3.2 函数功能

    创建一个线程
    
    • 1

    3.3 参数

    thread: 线程标识符地址
    attr:线程属性结构体地址,通常设置为 NULL
    start_routine:线程函数的入口地址
    art:传给线程函数的参数
    
    • 1
    • 2
    • 3
    • 4

    3.4 返回值

    成功:0
    失败:非0
    
    • 1
    • 2

    3.5 实例

    3.5.1 实例一

    #include 
    #include 
    #include 
    #include 
    int main(int arg,char *args[])
    {
            void * func(void *arg)
            {
                    while (1)
                    {
                            printf("在子线程%ld中\n", pthread_self());
                            sleep (1);
                    }
                    return NULL;
    
            }
            pthread_t thread1;
            int res = pthread_create(&thread1, NULL, func, NULL);
            printf("res=%d\n",res);
            printf("当前主线程的线程id是%ld\n", pthread_self());
            printf("创建的子线程id是%ld\n", thread1);
            getchar();
            return 0;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    3.5.2 实例二 给函数传参数

    #include 
    #include 
    #include 
    #include 
    int main(int arg,char *args[])
    {
            void * func(void *arg)
            {
                    while (1)
                    {
                            printf("在子线程%ld中\n", pthread_self());
                            printf("传来的参数是%s\n", (char *)arg);
                            sleep (1);
                    }
                    return NULL;
    
            }
            pthread_t thread1;
            int res = pthread_create(&thread1, NULL, func, "test123");
            printf("res=%d\n",res);
            printf("当前主线程的线程id是%ld\n", pthread_self());
            printf("创建的子线程id是%ld\n", thread1);
            getchar();
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    在这里插入图片描述

    3.5.3 实例三 在线程函数中接收传来的参数时,如果用指针变量接收参数,由于参数就一份,那么不同线程都使用同一个线程函数时相互有影响;如果用普通变量来接收参数,由于是复制了一份原始参数来自己使用,所以多个线程使用同一线程函数时相互间不会影响

    1. 用指针变量接收参数,多个线程间相互影响
    #include 
    #include 
    #include 
    #include 
    void *func1(void *args)
    {
            int *p = (int *)args;
            while(1)
            {
                    printf("fun1中子线程%ld中参数为%d\n",pthread_self(), *p);
                    *p = *p+1;
                    sleep(1);
            }
            return NULL;
    }
    int main(int arg,char *args[])
    {
            pthread_t  lwp1;
            pthread_t  lwp2;
            int param1 = 1;
            pthread_create(&lwp1, NULL,func1,¶m1);
            pthread_create(&lwp2, NULL,func1,¶m1);
            pthread_join(lwp1,NULL);
            pthread_join(lwp2,NULL);
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述

    1. 用普通变量来接收参数,多个线程间相互不影响
    #include 
    #include 
    #include 
    #include 
    void *func1(void *args)
    {
    	int a = *(int *)args;
    	while(1)
    	{
    		printf("fun1中子线程%ld中参数为%d\n",pthread_self(), a);
    		a++;
    		sleep(1);
    	}
    	return NULL;
    }
    int main(int arg,char *args[])
    {
    	pthread_t  lwp1;
    	pthread_t  lwp2;
    	int param1 = 1;
    	pthread_create(&lwp1, NULL,func1,¶m1);
    	pthread_create(&lwp2, NULL,func1,¶m1);
    	pthread_join(lwp1,NULL);
    	pthread_join(lwp2,NULL);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    在这里插入图片描述

    3.5.4 实例四 当需要多个参数传给线程函数时用结构体

    #include 
    #include 
    #include 
    #include 
    typedef struct person
    {
    
    	char name[32];
    	int age;
    } PERSON;
    void* func(void *args)
    {
    	PERSON man = *(PERSON *)args;
    	while(1)
    	{
    		printf("我的名字叫%s\n",man.name);
    		printf("我的年龄是%d\n",man.age);
    		sleep(1);
    	}
    	return NULL;
    }
    int main(int arg,char *args[])
    {
    	pthread_t lwp1;
    	PERSON person = {"tony",39};
    	pthread_create(&lwp1, NULL, func, &person);
    	pthread_join(lwp1,NULL);	
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    四 回收线程资源 pthread_join 和 pthread_detach

    4.1 进程自己负责回收线程资源 pthread_join

    会阻塞直到回收了要回收的线程

    4.1.1 需要的头文件和函数原型

    #include 
    int pthread_join(pthread_t thread, void **retval);
    
    • 1
    • 2

    4.1.2 函数功能

    等待线程结束(此函数会阻塞),并回收线程资源,类似进程的wait()函数。如果线程已经结束,那么该函数会立刻返回
    
    • 1

    4.1.3 参数

    thread:被等待的线程号
    retval:用来存储线程退出状态的指针的地址
    
    • 1
    • 2

    4.1.4 返回值

    成功:0
    失败:非0
    
    • 1
    • 2

    4.2 线程分离,让系统负责回收线程资源 pthread_detach

    不会阻塞,回收线程资源的任务交给了系统;
    一般情况下,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取他的状态为止。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收他占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。也就是说,如果已经对一个线程调用了pthread_detach就不能再调用pthread_join了。
    
    • 1
    • 2

    4.2.1 需要的头文件和函数原型

    #include 
    int pthread_detach(pthread_t thread);
    
    • 1
    • 2

    4.2.2 功能

    使调用线程与当前进程分离,分离后不代表线程不依赖于当前进程,线程分离的目的是将线程资源的回收工作交由系统自动完成,也就是说当被分离的线程结束后,系统会自动回收他的资源。所以,此函数不会阻塞。
    
    • 1

    4.2.3 参数

    thread:线程号
    
    • 1

    4.2.4 返回值

    成功:0
    失败:非0
    
    • 1
    • 2

    4.2.5 实例

    #include 
    #include 
    #include 
    #include 
    #include 
    void *func1(void *args)
    {
    	int a  = *(int *)args;
    	while(1)
    	{
    		printf("func1中子线程%ld中参数为%d\n",pthread_self(), a);
    		sleep(1);
    	}
    	return NULL;
    }
    void *func2(void *args)
    {
    	char name[32] = "";
    	memcpy(name, (char *)args ,strlen((char *)args));
    	while(1)
    	{
    		printf("func2中子线程%ld中参数为%s\n",pthread_self(), name);
    		sleep(1);
    	}
    	return NULL;
    
    }
    int main(int arg,char *args[])
    {
    	pthread_t  lwp1;
    	pthread_t  lwp2;
    	int param1 = 1;
    	char param2[32] = "tony";
    	int t1, t2, t3, t4;
    	t1 = pthread_create(&lwp1, NULL,func1,¶m1);
    	printf("t1=%d\n", t1);
    	t2 = pthread_create(&lwp2, NULL,func2,param2);
    	printf("t2=%d\n", t2);
    	t3 = pthread_detach(lwp1);
    	printf("t3=%d\n", t3);
    	t4 = pthread_detach(lwp2);
    	printf("t4=%d\n", t4);
    	getchar();
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46

    在这里插入图片描述

    五 线程的退出和取消

    在进程中我们可以调用exit或_exit函数来结束进程,在一个线程中我们可以通过以下三种方式在不终止整个进程的情况下停止线程的控制流。线程从执行函数中返回;线程调用pthread_exit退出线程;线程可以被同一进程中的其他线程取消。
    
    • 1

    5.1 线程的退出

    5.1.1 需要的头文件和函数原型

    #include 
    void pthread_exit(void *retval);
    
    • 1
    • 2

    5.1.2 功能

    退出调用线程。一个进程中的多个线程是共享该进程的数据段,因此,通常线程退出后所占用的资源并不会释放
    
    • 1

    5.1.3 实例

    #include 
    #include 
    #include 
    #include 
    #include 
    void *func(void *args)
    {
            int a  = *(int *)args;
            while(1)
            {
                    printf("func中子线程%ld中参数为%d\n",pthread_self(), a);
                    a++;
                    if(a==5)
                            pthread_exit(NULL);
                    sleep(1);
            }
            return NULL;
    }
    int main(int arg,char *args[])
    {
            pthread_t  lwp1;
            pthread_t  lwp2;
            int param1 = 1;
            pthread_create(&lwp1, NULL,func,¶m1);
            pthread_create(&lwp2, NULL,func,¶m1);
            pthread_detach(lwp1);
            pthread_join(lwp2,NULL);
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    在这里插入图片描述

    5.2 线程的取消

    可以取消自己;也可以取消当前进程中的其他线程;
    
    • 1

    5.2.1 需要的头文件和函数原型

    #include 
    int pthread_cancel(pthread_t thread);
    
    • 1
    • 2

    5.2.2 功能

    杀死(取消)线程

    5.2.3 参数

    thread:要取消的线程id
    
    • 1

    5.2.4 返回值

    成功:0
    失败:出错编号
    
    • 1
    • 2

    5.2.5 注意事项

    线程的取消并不是实时的,而是有一定的延时,需要等待线程到达某个取消点(检查点)。类似于玩游戏存档,必须到达指定的场所才能存储进度。杀死线程也不是立刻就能完成,必须要到达取消点。取消点:是线程检查是否被取消,并按请求进行动作的一个位置。通常是一些系统调用creat,open,close,read等。
    
    • 1

    5.2.6 实例

    #include 
    #include 
    #include 
    #include 
    void * pthread_fun01(void * arg)
    {
            int i = 0;
            while(1)
            {
                    printf("%s---------i=%d\n", (char *)arg, i++);
                    sleep(1); //取消点
            }
            return NULL;
    }
    int main(int arg,char *args[])
    {
            // 创建线程
            pthread_t tid1;
            pthread_create(&tid1, NULL, pthread_fun01, "任务A");
            //线程分离
            pthread_detach(tid1);
            printf("5秒后结束任务A\n");
            sleep(5);
            pthread_cancel(tid1);
            getchar();
            return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    在这里插入图片描述

    六 线程的属性

    Linux下线程的属性是可以根据实际项目需要,进行设置,之前我们讨论的线程都是采用默认线程属性来创建的,默认属性已经可以解决绝大多数开发遇到的问题了。如果我们对程序的性能提出更高要求那么需要设置线程属性,比如可以通过设置线程栈的大小来降低内存的使用,增加最大线程个数
    
    • 1

    6.1 线程属性的结构体成员

    typedef struct
    {
            int etachstate;  //线程的分离状态
            int schedpolicy;  //线程调度策略
            struct sched_param schedparam;  //线程的调度参数
            int  inheritsched;  //线程的继承性
            int  scope;   //线程的作用域
            size_t guardsize;  //线程栈末尾的警戒缓冲区大小
            int  stackaddr_set;  //线程的栈设置
            void*  stackaddr;  //线程栈的位置
            size_t  stacksize;  //线程栈的大小
    } pthread_attr_t;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    6.2 线程属性初始化

    线程属性初始化函数为pthread_attr_init, 这个函数必须在pthread_create函数之前调用。之后需要用pthread_attr_destroy函数来释放资源。线程属性主要包括如下属性:作用域、栈尺寸、栈地址、优先级、分离状态、调度策略和参数。默认的属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
    注意:应先初始化线程属性,再pthread_create创建线程
    初始化线程属性函数:int pthread_attr_init(pthread_attr_t *attr);
    返回值:成功:0;失败:错误号
    
    • 1
    • 2
    • 3
    • 4

    6.3 销毁线程属性所占用的资源

    int pthread_attr_destroy(pthread_attr_t *attr);
    返回值:成功:0;失败:错误号
    
    • 1
    • 2

    6.4 设置线程的分离状态

    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    参数:
    	attr:已经初始化的线程属性
    	detachstate:分离状态 PTHREAD_CREATE_DETACHED(分离线程)
    										 PTHREAD_CREATE_JOINABLE(非分离线程)
    注意:如果设置一个线程为分离线程,而这个线程运行又非常快,他很可能在pthread_create函数返回之前就终止了,他终止以后就可能将线程号和系统资源移交给其他线程使用,这样调用pthread_create的线程就得到了错误的线程号。要避免这种情况可以采用一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timedwait函数,让这个线程等待一会,留出足够的时间让函数pthread_create返回。设置一段等待时间,是在多线程编程里常用的方法。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.5 获取线程的分离状态

    int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstat);
    参数:
    	attr:已经初始化的线程属性
    	detachstate:分离状态 PTHREAD_CREATE_DETACHED(分离线程)
    										 PTHREAD_CREATE_JOINABLE(非分离线程)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    6.6 设置线程的栈地址

    当进程栈地址空间不够时,指定新建线程使用由malloc分配的堆空间作为栈空间。
    int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
    返回值:成功:0;失败:错误号
    
    • 1
    • 2
    • 3

    6.7 获取线程的栈地址

    int pthread_attr_getstack(pthread_attr_t *attr, void **stackaddr, size_t *stacksize);
    参数:
    	attr:指向一个线程属性的指针
    	stackaddr:返回获取的栈地址
    	stacksize:返回获取的栈大小
    返回值:成功:0;失败:错误号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.8 设置线程的栈大小

    当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。
    int pthead_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
    参数:
    	attr:指向一个线程属性的指针
    	stacksize:返回线程的堆栈大小
    返回值:成功0;失败:错误号
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6.9 获取线程的栈大小

    int pthead_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
    参数:
    	attr:指向一个线程属性的指针
    	stacksize:返回线程的堆栈大小
    返回值:成功0;失败:错误号
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    6.10 线程属性总和案例

    #include 
    #include 
    #include 
    #include 
    #include 
    #define SIZE 0x100000
    
    void *th_fun(void *arg)
    {
    	while(1)
    		sleep(1);
    }
    
    int main(int arg,char *args[])
    {
    	pthread_t tid;
    	int err, detachstate, i=1;
    	pthread_attr_t attr;
    	size_t stacksize;
    	void *stackaddr;
    
    	pthread_attr_init(&attr);
    	pthread_attr_getstack(&attr, &stackaddr, &stacksize);
    	pthread_attr_getdetachstate(&attr, &detachstate);
    
    	if(detachstate == PTHREAD_CREATE_DETACHED)
    		printf("thread detached\n");
    	else if(detachstate == PTHREAD_CREATE_JOINABLE)
    		printf("thread join\n");
    	else
    		printf("thread unknown\n");
    
    	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
    	while(1)
    	{
    		stackaddr = malloc(SIZE);
    		if(stackaddr == NULL)
    		{
    			perror("malloc");
    			exit(1);
    		}
    		stacksize = SIZE;
    		pthread_attr_setstack(&attr, stackaddr, stacksize);
    		err = pthread_create(&tid, &attr, th_fun, NULL);
    		if(err != 0)
    		{
    			printf("err=%d\n", err);
    			printf("%s\n", strerror(err));
    			exit(1);
    		}
    		printf("%d\n", i++);
    	}
    	pthread_attr_destroy(&attr);
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    在这里插入图片描述

    在这里插入图片描述

    七 创建多线程

    #include 
    #include 
    #include 
    #include 
    #include 
    typedef struct
    {
    	int time;
    	char task_name[32];
    } MSG;
    
    void *deal_fun(void *arg)
    {
    	MSG msg = *(MSG *)arg;
    	int i = 0;
    	for(i=msg.time; i>0; i--)
    	{
    		printf("%s剩余时间%d\n", msg.task_name, i);
    		sleep(1);
    	}
    	return NULL;
    }
    
    int main(int arg,char *args[])
    {
    	while(1)
    	{
    		MSG msg;
    		printf("输入新增的任务名:");
    		fgets(msg.task_name, sizeof(msg.task_name), stdin);
    		msg.task_name[strlen(msg.task_name)-1]=0;
    		printf("输入运行时间:");
    		scanf("%d", &msg.time);
    		getchar();//获取换行符
    		pthread_t tid;
    		pthread_create(&tid, NULL, deal_fun, (void *)&msg);
    		pthread_detach(tid);//线程分离
    	}
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    在这里插入图片描述

  • 相关阅读:
    韩国锂电池工厂火灾:行业安全警钟再次敲响
    Python文件高阶操作:复制、删除、移动、压缩文件夹
    Vue2:使用Vant UI实现网易云评论页上拉和下拉刷新
    element中Notification组件(this.$notify)自定义样式
    C#复杂XML反序列化为实体对象两种方式
    LeetCode_动态规划_中等_368.最大整除子集
    python>>numpy(第二讲)
    软件测试|MySQL WHERE条件查询详解:筛选出需要的数据
    公司招人:34岁以上两年一跳的不要,开出工资以为看错了
    java.lang.Float类下isInfinite(float v)方法具有什么功能呢?
  • 原文地址:https://blog.csdn.net/qq_33808440/article/details/125659192