23-创建线程的一些细节

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_35733751/article/details/82776022

1. 回忆进程

  经过前面的学习,我们知道进程是一个可执行程序的运行实例,每一个运行着的程序都可以看做是一个独立的进程,会占用系统资源,在内存中有对应的代码空间和数据空间,由操作系统调度运行,创建,分配系统资源,完成任务并销毁等等。


2. 什么是线程

  在linux早期还没有线程概念的时候,一个进程就对应一个程序,且一个进程某一时刻只能干一件事情。有了多线程的概念后,一个进程可以包含多个线程,这样进程在某一时刻就能够做很多事情,每个线程处理各自独立的任务。

  操作系统会在多个线程之间调度切换,每个线程都是共享进程中的资源,相对来说占用的资源较少,加入线程的最主要目的就是为了更好地支持多处理器,并且减少进程上下文切换的开销,提高效率。


3. 线程标识

  就像每个进程都有一个进程id一样,每个线程也有一个线程id,进程id是整个系统唯一的,但线程id不一样,线程id只有在它所属的进程上下文中才有意义。


4.创建线程

   一般程序运行起来就会产生一个进程,默认情况下该进程中有一个线程,叫做主线程,在创建新的线程前,程序的行为和传统的进程并没什么区别。

   pthread_create函数用于创建一个新的线程。

函数原型:

#include <pthread.h>
typedef void *(*stat_routine)(void *);
int pthread_create(pthread_t *thread , const pthread_attr_t *attr , start_routine th_fn , void *arg);

返回值说明:成功返回0, 失败返回错误号(POSIX线程所有函数返回0表示成功,返回其他值表示失败)。

参数说明:
  参数thread:保存创建的线程ID,其pthread_t类型在当前Linux中可理解为无符号的长整形,但是在不同linux实现中pthread_t的类型可能存在差异。

  参数attr:指定新线程的各种属性,类型为pthread_attr_t,通常传NULL,表示使用线程默认属性。

  参数th_fn:是一个函数指针,指向线程主函数,start_routine是用来声明线程主函数类型为void* th_fn(void* arg) ,一旦新创建的线程被调度,就会进入该函数开始执行,该函数运行结束,线程也随之结束。

  参数arg:表示线程主函数执行期间的参数,类型为void *,这意味着可以将任意对象的指针传递给线程主函数,如果需要传递多个参数可以将arg设计成一个结构体。


5. 创建线程实验一

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

//线程主控函数
void *tfn(void *arg){
        puts("I am pthread");
        return NULL;
}

int main(void) {
        pthread_t tid;
        //创建线程
        int ret = pthread_create(&tid, NULL, tfn, NULL);   
        if(ret != 0){
                fprintf(stderr , "pthread_create error: %s\n", strerror(ret));
                exit(-1);
        }
        //sleep的目的是为了防止主控线程先于其他线程退出,导致其他线程也提前结束
        sleep(1);
        puts("I am main pthread");
        return 0;
}

编译并运行,注意链接pthread库
gcc -o pthread_create pthread_create.c -lpthread
./pthread_create

程序执行结果如图:
在这里插入图片描述

  有同学可能会很奇怪为什么要sleep 1秒,这是因为:如果任意一个线程调用了exit或_exit,整个进程的所有线程都会终止,从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,会在main函数return之前延时1秒,但这只是一种缓兵之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行


6. pthread_self和 pthread_equal

  进程内部的每个线程都有一个唯一标识,即线程id,pthread_self函数是用来获取线程id,其作用类似于进程中 getpid() 函数。

#include <pthread.h>
pthread_t pthread_self(void);	   

返回值:成功返回0


  线程id是用pthread_t数据类型来表示的,是一个无符号长整形,在可移植的操作系统就不能把它当做整数来处理。

  因为在不同的操作系统中,pthread_t数据类型实现是不一样的,在linux中可能是使用无符号长整形(unsigned long int)表示pthread_t数据类型,但在Solaris中可能使用无符号整型,而FreeBSD可能使用一个指向pthread_t结构的指针类型来表示pthread_t数据类型。

  因此必须使用一个函数来对两个线程id进行比较,pthread_equal函数的作用就是用来比较两个线程的id是否相同。

#include <pthread.h>
int pthread_equal(pthread_t tid1 , pthread_t tid2);

返回值:如果两个线程id相同返回非0,不相等返回0

参数说明:参数tid1和参数tid2为比较的两个线程id


7. 创建线程实验二

循环创建多个线程,每个线程打印自己是第几个被创建的线程。

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

//线程主函数
void *tfn(void *arg){
        int i = (int)arg;
        sleep(i);
        //依次打印创建的每个线程
        printf("I am pthread%d , pthread_id = %lu\n",i, pthread_self());   
        return NULL;
}

int main(int argc, char *args[]){
        if(argc < 2){
                printf("argc < 2\n");
                return -1;
        }
        pthread_t tid;
        int size = atoi(args[1]);
        int i;
        int ret;
        for(i = 0; i < size; ++i){
                ret = pthread_create(&tid,NULL,tfn,(void *)i);   //创建多个线程
                if(ret != 0){
                        //错误处理
                        fprintf(stderr , "pthread_create error",  strerror(ret));
                }
        }
        sleep(size); 
        //主线程
        printf("I am main pthread , pthread_id = %lu\n" , pthread_self());
        pthread_exit(NULL);
}

编译并运行:

gcc -o create_pthread2 create_pthread2.c -lpthread
./create_pthread2 6

程序运行结果如图:
在这里插入图片描述

思考一个问题:将pthread_create函数参4修改为(void )&i, 将线程主函数内改为 i=((int *)arg) 是否可以?


我们对实验二的代码做以下修改:
//线程主函数
void *tfn(void *arg){
		......
	 	int i=*((int *)arg);
	 	......
}

int main(void)
{
	......
	//对pthread_create函数的参数i取地址
	ret = pthread_create(&tid,NULL,tfn,(void *)&i);   //创建多个线程
	......
}

程序执行结果:
在这里插入图片描述

   从程序执行结果来看,显然是不行,因为(void *)&i是对i取地址操作,在循环创建线程的时候有可能几乎创建出来多个线程,多个线程读取到的i值是一样的,所以在打印输出的是多个线程id一样的线程(因为线程的创建是非常快的,也就是说第一个进程创建完,第二个进程又接着创建,此时第二个进程读取到的i值和第一个进程的i值是一样的,然后i值才++)。


8. 关于pthread_create函数的细节

  pthread_create成功返回后,新创建的线程的id被填写到参数thread所指向的内存单元。线程id的类型是thread_t,它只在当前进程中保证是唯一的,在不同的系统中thread_t这个类型可能有不同的实现,它可能是一个整数值,也可能是一个结构体,也可能是一个地址。

   另外,线程主函数的函数体返回值是void *,是一个万能指针类型,那么我们必须在线程主控函数必须返回一个指针类型,一般来说直接返回return NULL 实际等效于return (void *)0。


9. 关于线程的执行顺序

   通过前面两个实验相信你对线程有了一些基本的了解。

   线程是属于进程的,线程运行在进程空间,同一进程所产生的多个线程共享同一进程内存空间,这也是为什么进程间的内存地址空间是独立不共享的,而线程间的内存地址空间是共享的原因。

   当进程退出时,该进程下所属的多线程都会强制清除并退出。

   因为一个进程至少需要一个线程作为执行体,也就是主线程,进程管理的资源(cpu,内存)将线程分配到某个cpu上执行。所以当一个程序运行产生一个进程,但真正运行的实际上是线程而不是进程,进程只是一个空间的概念,相当于一个指挥者的角色,调度管理线程的运行和资源的分配

  所以cpu先把时间片先分配给进程,然后进程再把时间片分配过下属的线程。比如:当前程序运行时,cpu分配了100纳秒的时间片给进程,而进程又把这100纳秒时间片分配给了当前进程下的其他线程(多线程是轮流调度的),因此不管是cpu分配给进程,还是进程分配给其他线程,这个工作是由调度算法程序来做的。


   现在回到实验一为什么sleep的问题。

   pthread_create函数并不保证线程的执行顺序,一般程序运行时都有一个进程,在该进程下还有一个主控线程,主控线程在调用pthread_create创建子线程时,如果子线程一直没有得到cpu的执行权,而主控线程又先于子线程结束,那么该进程下的所有线程都会强制退出,也就没有机会去执行线程主控函数了。所以为了防止这种情况发生,一般来说我们会让主控线程sleep,等待其他线程执行完毕,然后主控线程再退出程序。

猜你喜欢

转载自blog.csdn.net/qq_35733751/article/details/82776022