48.Linux 线程 Thread(一)

学习目标

①了解线程的定义

②掌握常用的线程操作

③熟练通过线程属性设置线程状态

④掌握线程同步的方法

从很多Linux的书籍我们都可以这样子描述进程(process)和线程(thread)的: 进程是资源管理的最小单位,线程是程序执行的最小单位。

线程的本质是一个进程内部的一个控制序列,它是进程里面的东西,一个进程可以拥有一个进程或者多个进程。

当进程执行fork()函数创建一个进程时,将创建出该进程的一份新副本。 这个新进程拥有自己的变量和自己的PID,它的执行几乎完全独立于父进程, 这样子得到一个新的进程开销是非常大的。而当在进程中创建一个新线程时,新的执行线程将拥有自己的栈, 但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。 也就是说,它只使用当前进程的资源,而不是产生当前进程的副本。

        与进程不同,线程(thread)是系统调度分配的最小单位。与进程相比,线程没有独立的地址空间,多个线程共享一段地址空间,因此线程消耗更少的内存资源,线程间通信也更为方便,有时线程也被称为轻量级进程(Light  Weight  Process,LWP)。本章将会介绍与线程相关的知识,包括线程的概念、线程的生命周期以及与线程相关的系统调用(如创建线程、销毁线程、线程同步)等。   

1.线程概述

        早期操作系统没有线程这一概念,无论是分配资源还是调度分配,都以进程为最小的单元。

操作系统应调度一个更小的单位,以减少消耗,提高效率,由此线程应运而生。

        Linux系统中的线程借助进程机制实现,线程与进程联系密切。进程可以蜕变成线程,当一个进程中创建一个线程时,原有的进程就会变成线程,两个线程共用一段地址空间;线程有被称为轻量级进程,线程的TCB(Thread Contorl Block,线程控制块)与进程的PCB相同,因此也可以将TCB视为PCB;对内核而言,线程与进程没有区别,CPU会为每个线程与进程分配时间片,并通过PCB来调度不同的进程和线程。

 Linux系统中的线程分为三种:内核线程、用户线程和轻量级线程(TWP)

①内核线程是内核的分支,每个内核线程可处理一项特定操作。

②用户线程是完全建立在用户空间的线程,用户线程的创建、调度、销毁等操作都在用户空间完成,是一种低消耗、高效率的线程。

③轻量级线程是一种用户线程,同时也是内核线程的高级抽象,每一个轻量级线程都需要一个内核线程支持,轻量级线程与内核及CPU之间的关系。

·

一个进程的实体可以分为两大部分:线程集和资源集。

①线程集是多个线程的集合,每个线程都是进程中的动态对象 。

②资源集是进程中线程集共享资源的集合,包括地址空间、打开的文件描述符、用户信息等。

一个线程的实体包括程序、数据、TCB以及少量必不可少的用于保证线程独立运行的资源。

使用多线程编程时,程序的并发性会得到一定的提升。若一个进程细分为多个线程,那么一个进程中的多个线程可以同时在不同的CPU上运行,如此可在一定程度上减少程序的运行时间,提高程序的执行效率。

Linux系统中的每个进程都有独立的地址空间,一个进程崩溃后, 在系统的保护模式下并不会对系统中其它进程产生影响,而线程只是一个进程内部的一个控制序列, 当进程崩溃后,线程也随之崩溃,所以一个多进程的程序要比多线程的程序健壮,但在进程切换时, 耗费资源较大,效率要差一些。但在某些场合下对于一些要求同时进行并且又要共享某些变量的并发操作, 只能用线程,不能用进程。

总的来说:

  • 一个程序至少有一个进程,一个进程至少有一个线程。

  • 线程使用的资源是进程的资源,进程崩溃线程也随之崩溃。

  • 线程的上下文切换,要比进程更加快速,因为本质上,线程很多资源都是共享进程的,所以切换时, 需要保存和切换的项是很少的。

 2. 创建线程

在讲解线程编程之前,先了解一个标准:可移植操作系统接口 (Portable Operating System Interface,缩写为POSIX), POSIX是IEEE为要在各种UNIX操作系统上运行软件,而定义API接口的一系列互相关联的标准的总称, 其正式称呼为IEEEStd 1003,而国际标准名称为ISO/IEC9945,此标准源于一个大约开始于1985年的项目。 POSIX这个名称是由理查德·斯托曼(RMS)应IEEE的要求而提议的一个易于记忆的名称。 它基本上是Portable Operating System Interface(可移植操作系统接口)的缩写, 而X则表明其对Unix API的传承。

简单来说,如果应用程序使用POSIX标准的接口来调用系统函数, 那么应用程序将非常容易移植甚至直接兼容到遵循POSIX标准的系统上。

在Linux系统下的多线程遵循POSIX标准,而其中的一套常用的线程库是 pthread, 它是一套通用的线程库,是由 POSIX提出的,因此具有很好的可移植性, 我们学习的Linux多线程编程也正是使用它,使用时必须包含以下头文件:

#include <pthread.h>

除此之外在链接时需要使用库libpthread.a。因为pthread的库不是Linux系统的库, 所以在编译时要加上-lpthread 选项。

gcc pthread_cre.c -o pthread_cre -lpthread

Linux系统中创建线程的系统调用接口为pthread_create(),该函数存在于函数库pthread.h,其声明如下:

int pthread_create(pthread_t *thread,const pthread_attr_t *attr,

void *(*start_routine)(void *),void *arg);

如果调用pthread_create()函数创建线程成功,会返回0;

若线程创建失败,则直接返回errno。

此外,由于errno的值很容易被修改,线程中很少使用errno来存储错误码,也不会使用perror()直接将其打印,而是1.使用自定义变量接收errno,2.再调用strerror()将获取的错误码转换成错误信息,3.最后才打印错误信息。

pthread_create()函数中包含4个参数:

参数thread表示待创建线程的线程id指针,这是一个传入传出参数,若需要对该线程进行操作,应使用一个pthread_t *类型的变量获取该参数;(指向线程标识符的指针

②参数arr用于设置待创建线程的属性,通常传入NULL,表示使用线程的默认属性;(设置线程属性

 ③参数start_routine是一个函数指针,指向一个参数为void*、返回值也为void*的函数,该函数为待创建线程执行函数,线程创建成功后将执行该函数中的代码;(start_routine是一个函数指针,指向要运行的线程入口,即线程运行时要执行的函数代码。

④参数arg为要传入给线程执行函数的参数;(运行线程时传入的参数

返回值:若线程创建成功,则返回0

              若线程创建失败,则返回对应的错误代码。

在线程调用pthread_create()函数创建出新线程之后,当前线程会从pthread_create()函数返回并继续向下执行,新线程会执行函数指针start_routine所指的函数。

若pthread_create()函数成功返回,新线程的id会被写到thread参数所指向的内存单元

        需要注意的是,进程id的类型pid_t实质是一个正整数,在整个系统中都是唯一的;但线程id只在当前进程中保证唯一,其类型pthread_t并非是一个正整数,且当前进程调用pthread_create()后获取的thread为新线程id。

       

因此线程id不能简单地使用printf()函数打印,而应使用Linux提供的接口函数pthread_self()来获取。

 pthread_self()函数存在于函数库pthread.h中,其声明如下:

pthread_t pthread_self(void);                

下面通过一个案例来展示pthread_create()函数的用法。

fprintf是C/C++中的一个格式化库函数,其作用是格式化输出到一个流/文件中;函数原型为int fprintf( FILE *stream, const char *format, [ argument ]...),fprintf()函数根据指定的格式(format)向输出流(stream)写入数据(argument)。

案例9-1:使用pthread_create()函数创建线程,并使原线程与新线程分别打印自己的线程id。

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

void *tfn(void *arg)
{
	printf("tfn--pid=%d,tid=%lu\n",getpid(),pthread_self());
	return (void *)0;
}

int main()
{
	pthread_t tid;
	printf("main--pid=%d,tid=%lu\n",getpid(),pthread_self());
	int ret=pthread_create(&tid,NULL,tfn,NULL);
	if(ret!=0){
		fprintf(stderr,"pthread_create error:%s\n",strerror(ret));
		exit(1);
	}
	sleep(1);
	return 0;
}

执行结果进程2883中的两个线程分别打印出了各自的线程id,由此可知案例9-1实现成功

 进程拥有独立的地址空间。当使用fork()函数创建出新进程后,若其中·一个进程要对fork()之前的数据进行修改,进程中会依据“写时复制”原则,先复制一份该数据到子进程的地址空间,再修改数据。因此即便是全局变量,在进程间也是不共享的。但由于线程间共享地址空间,因此在一个线程中对全局区的数据进行修改,其他线程中访问到的也是修改后的数据

案例9-2:创建新线程,在新线程中修改原线程中定义在全局区的变量,并在原线程中打印该数据。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
int var=100;
void *tfn(void *arg)
{
	var=200;
	printf("thread\n");
	return NULL;
}
int main(void)
{
	printf("At first var=%d\n",var);
	pthread_t tid;
	pthread_create(&tid,NULL,tfn,NULL);	
	sleep(1);
	printf("after pthread_create,var=%d\n",var);
	return 0;
}

以上程序的原线程中定义了一个全局变量var,并赋值为100;随后在新线程中修改中全局变量var的值并使原线程沉睡1秒,等待原线程执行,确保原线程的打印语句在新进程功能完成最后执行;最后执行;最后在原线程中打印var的值。编译案例9-2,执行程序,终端打印结果如下

原线程中访问到的变量var的值被修改为200,说明新线程成功修改了原线程中定义的全局变量,线程之间共享全局数据。

 2.2 线程退出

线程中提供了一个用于单个线程退出的函数——pthread_exit(),该函数位于函数库pthread.h中,其声明如下:

void pthread_exit(void *retval);

在之前的案例中使用exit()和return虽然也有退出功能,但return用于退出函数,使函数返回函数调用处;exit()用于退出进程,若在线程中调用函数,那么线程所处的进程也会退出,如此势必会影响进程中线程的执行。为避免这个问题,保证程序中的线程能逐个退出,Linux系统中又提供了pthread_exit()函数的用法

        pthread_exit()函数没有返回值,其参数retval表示线程的退出状态通常设置为NULL

        参数说明:

  • retval:如果retval不为空,则会将线程的退出值保存到retval中,如果不关心线程的退出值,形参为NULL即可

下面通过一个案例来展示pthread_exit()函数的用法。

案例9-3:在一个进程中创建4个新线程,分别使用pthread_exit()函数、return、exit()使其中一个线程退出,观察其他线程的执行状况。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <unistd.h>
void *tfn(void *arg)
{
	long int i;
	i=(long int)arg;        //将void*类型的arg强转为long int类型
	if(i==2)
	     pthread_exit(NULL);
	sleep(i);              //通过i来区别每个线程  
	printf("I'm %dth thread,Thread_ID=%lu\n",i+1,pthread_self());
	return NULL;	
}

int main(int argc,char *argv[])
{
	long int n=5,i;	
	pthread_t tid;
	if(argc==2)
		n=atoi(argv[1]);
	for(i=0;i<n;i++){
        //将i转换为指针,在tfn中强制转换回整型
		pthread_create(&tid,NULL,tfn,(void *)i);
	}	
	sleep(n);
	printf("I am main,I'm a thread!\n"
		"main_thread_ID=%lu\n",pthread_self());
	return 0;
}

执行结果如下:

有执行结果可知,使用pthread_exit()函数时,只有调用该函数的线程会退出。

2.3  线程终止

        在线程操作中有一个与终止进程的函数kill()对应的系统调用,即pthread_cancel(),使用该函数可以通过向指定线程发送CANCEL信号,使一个线程强行杀死另外一个线程。pthread_cancel()函数位于函数库pthread.h中,其声明如下:

int pthread_cancel(pthread_t thread);

pthread_cancel()中的参数thread为线程id,若函数调用成功则返回0,否则返回errno。使用pthread_cancel()函数终止的线程其退出码为PTHREAD_CANCELED,该宏定义在头文件pthread.h中,其值为-1。

        与进程不同的是,调用pthread_cancel()函数杀死线程时,需要等待线程到达某个取消点,线程才会成功被终止。类似于单机游戏中只有到达城镇中的存档点时才能执行存档操作,在多线程编程中,只有到达取消点时系统才会检测是否有未响应的取消信号,并对信号进行处理。

        所谓取消点即在线程执行过程中会检测是否有未响应取消信号的点,可粗略地认为只要有系统调用(进入内核)发生,就会进入取消点,如在程序中调用read()、write()、pause()等函数时都会出现取消点。取消点通常伴随阻塞出现,用户也可以在程序中通过调用pthread_testcancel()函数创造取消点。   

下面通过一个案例来展示pthread_cancel()函数的用法。

案例9-4:在程序中使用pthread_cancel()函数使原线程终止指定线程

进程中的线程可以调用pthread_join()函数来等待某个线程的终止,获得该线程的终止状态(),并收回所占的资源, 如果对线程的返回状态不感兴趣,可以将rval_ptr设置为NULL。

int pthread_join(pthread_t tid, void **rval_ptr);
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *tfn(void *arg)
{
	while(1){
		printf("child thread...\n");
		pthread_testcancel();        //设置取消点
	}
}

int main(void)
{
	pthread_t tid;
	void *tret=NULL;
	pthread_create(&tid,NULL,tfn,NULL);
	sleep(1);
	pthread_cancel(tid);
	pthread_join(tid,&tret);
	printf("child thread exit code=%ld\n",(long int)tret);
	return 0;
}

执行结果如下:

 由执行结果可知,新线程在1秒后被原线程终止,新线程的退出码为-1,即PTHREAD_CANCELED。

pthread_exit()和pthread_cancel()都是线程机制中提供的用于终止线程的系统调用pthread_exit()使线程主动退出,pthread_cancel()通过信号使线程被动退出。需要注意的是,由于在线程机制出现之前信号机制已经出现,信号机制在创建时并未将线程考虑在内,线程与信号机制的兼容性略有不足,因此在多线程编程中应尽量避免使用信号,以免出现难以调试的错误。

2.4 线程挂起

         在进程中,可以使用wait()、waitpid()将进程挂起,以等待某个子进程结束;而在线程中,则通过pthread_join()函数挂起线程。pthread_join()函数存在于函数库pthread.h中,其函数声明如下:

int pthread_join(pthread_t thread,void **retval);

调用该函数的线程将会使自己挂起并等待指定线程thread结束。需要注意的是,该函数中指定的线程必须与调用该函数的线程处于同一进程中,且多个线程不能同时挂起等待同一个进程,否则pthread_join()将会返回错误。

        

pthread_join()调用成功将返回0,否则返回errno。

pthread_join()中的参数thread表示被等待的线程id;

参数retval用于接收thread线程执行函数的返回值指针,该指针的值与thread线程的终止方式有关: 

①若thread线程通过return返回,retval所指的存储单元中存放的是thread线程函数的返回值。     

②若thread线程被其他线程通过系统调用pthread_cancel()异常终止,retval所指向的存储单元中存放的是常量PTHREAD_CANCELED。

③若thread线程通过自调用pthread_exit()终止,retval所指向的存储单元中存放的是pthread_exit()中的参数ret_val。

④若等待thread的线程不关心它的终止状态,可以将retval的值设置为NULL

在使用线程时,一般都使用pthread_exit()函数将其终止。下面通过一个简单案例来展示含参pthread_exit()函数的用法。

案例9-5:使用pthread_exit()退出线程,为线程设置退出状态并将线程的退出状态输出。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
typedef struct{
	int a;
	int b;
}exit_t;

void *tfn(void *arg)
{
	exit_t *ret;
	ret=malloc(sizeof(exit_t));
	ret->a=100;
	ret->b=300;
	pthread_exit((void *)ret);    //线程终止
	return NULL;                  //线程返回  
}

int main(void)
{
	pthread_t tid;
	exit_t *retval;
	pthread_create(&tid,NULL,tfn,NULL);
    //调用pthread_join可以获取线程的退出状态
	pthread_join(tid,(void **)&retval);
	printf("a=%d,b=%d\n",retval->a,retval->b);
	return 0;
}

在案例9-5创建的新线程中,即调用了thread_exit()函数,又设置了关键字return;在程序的第24行中,使用pthread_join()等待新线程退出并获取线程的退出状态,若第2中5行代码打印的线程退出状态不为空,说明线程通过pthread_exit()函数退出

 由执行结果可知,第15行调用的pthread_exit()函数成功使线程退出,并设置了线程的退出状态。

进程中可以使用waitpid()函数结合循环结构使原进程等待多个进程退出,线程中的pthread_join()同样可以与循环结构结合,等待多个线程退出。

案例9-6:使用pthread_join()回收多个新线程,并使用pthread_exit()获取每个线程的退出状态

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
long int var=100;
void *tfn(void *arg)
{
	long int i;
	i=(long int)arg;
	sleep(i);
	if(i==1){
		var=333;
		printf("var=%d\n",var);
		pthread_exit((void *)var);
	}
	else if(i==3){
		var=777;
		printf("I'm %dth pthread,pthread_id=%lu\n"
			"var=%d\n",i+1,pthread_self(),var);
		pthread_exit((void *)var);
	}
	else{
		printf("I'm %dth pthread,pthread_id=%lu\n"
				"var=%d\n",i+1,pthread_self(),var);
		pthread_exit((void *)var);
	}
	return NULL;
}

int main(void)
{
	pthread_t tid[5];
	long int i;
	int *ret[5];
	for(i=0;i<5;i++)	//创建新线程
		pthread_create(&tid[i],NULL,tfn,(void *)i);
	for(i=0;i<5;i++){   //回收新线程
		pthread_join(tid[i],(void **)&ret[i]);
		printf("------------%d's ret=%d\n",i,(long int)ret[i]);
	}
	printf("I'm main pthread tid=%lu\t var=%d\n",pthread_self(),var);
	pthread_exit(NULL);
}

执行程序

 由执行结果可知,程序中创建的5个新线程都成功退出,案例9-6成功运行。

当然,原线程的退出之所以会导致其他线程退出,是因为原线程执行完毕后,main()函数中会隐式使用exit函数,而我们知道pthread_exit()函数可以只使调用该函数的线程退出。

2.5 线程分离

        在线程终止后, 其他线程会调用pthread_join()函数获取该线程的终止信息,在此之前,线程会一直保持终止状态,这种状态类似进程中的僵尸进程。虽然处于僵尸态的进程中大部分资源都已经被释放,但因为仍有少许资源残留,进程会保持僵尸态一直于系统detach()函数,对进程的这一不足做了完善。

        pthread_detach()函数将会线程从主控线程中分离,这样当线程结束后,它的退出状态不由其他线程获取,而是由该线程自身自动释放。pthread_detach()函数位于函数库pthread.h中,其声明如下:

int pthread_detach(pthread_t thread);

参数thread为待分离线程的id,若该函数调用成功则返回0,否则返回errno

需要注意的是,pthread_join()不能终止已处于detach状态的线程,若对处于分离态的线程调用pthread_join()函数,函数将会调用失败并返回EINVAL。

下面通过一个案例来展示pthread_detach()函数的用法。

案例9-7:使用pthread_detach()函数分离新线程,使新线程自动回收

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
void *tfn(void *arg)
{
	int n=5;
	while(n--){
		printf("pthread tfn n=%d\n",n);
		sleep(1);
	}
	return (void *)7;
}
int main(void)
{
	pthread_t tid;
	void *ret;
	pthread_create(&tid,NULL,tfn,NULL);
	pthread_detach(tid);
	int retvar=pthread_join(tid,(void **)&ret);
	if(retvar!=0){
		fprintf(stderr,"pthread_join error %s\n",strerror(retvar));
	}
	else{
		printf("pthread exit with %ld\n",(long int)ret);
	}
	return 0;
}

执行结果如下:

pthread exit with 1407334567

结合程序解析结果,对程序进行分析。第6~14行代码定义了tfn()函数。在第19行代码中,tfn()函数作为新线程的执行函数被传递给pthread_create()函数。第20行代码调用pthread_detach()函数将第19行代码创建的新线程从当前线程中分离。第21行代码调用pthread_join()函数将新线程挂起,新线程终止后,pthread_join()函数中的参数ret将获取线程的终止状态。因为程序中为新线程的执行函数tfn()设置了返回值“(void*)7”,所以若函数pthread_join()函数调用成功,pthread_join()函数的参数ret等于tfn()函数的返回值。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值