多线程编程(一)——线程的创建、等待、分离、结合与终止

什么是线程?

Linux kernel中是不存在线程的,或者说没有真正意义上的线程,Linux下的线程都是用进程来模拟的,线程的实现就是多个共享数据信息的进程,我们称之为“轻量级进程”。

多线程的引入

多线程是指操作系统在单个进程内支持多个并发执行路径的能力,线程属于进程的的多个执行流。linux多线程设计包括多任务程序的设计,并发程序设计,网络程序设计,数据共享等。

Linux系统下的多线程遵循POSIX线程接口,称为POSIX thread 或者 pthread。在linux下线程函数位于libpthread共享库中,需要加上头文件pthread.h,连接时需要使用-lpthread。

Linux下pthread的实现是通过系统调用clone ( ) 来实现的,clone是linux所特有的系统调用,它的使用方式类似于fork。

线程的共享与私有

由于同一个进程的多个线程共享同一地址空间,因此Text Segment、Data Segment 都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问,除此之外,各线程共享以下进程资源和环境:

(1)文件描述符(fd)

(2)每种信号的处理方式(SIG_IGT、SIG_DEL或者自定义的信号函数)

(3)当前的工作目录

(4)用户id和组id

但是,有一些资源是各个线程私有一份的:

(1)线程id

(2)上下文,包括各种寄存器的值、程序计数器和栈指针

(3)栈空间

(4)errno变量

(5)信号屏蔽字

(6)调度优先级

线程由内核自动调度,并且内核通过一个整数ID来识别线程。同基于I/O多路复用一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟地址空间的整个内容,包括代码、数据、堆、共享库和打开的文件。

线程执行模型

多线程的执行模型在某些方面和多进程是相似的,如下图所示:

每个进程开始生命周期都是单一线程,整个线程称之为主线程(main thread)。在某一时刻,主线程创建一个对等线程(peer thread),从这个时间点开始,两个线程就并发地执行。最后,因为主线程执行一个慢速系统调用(read 或 sleep)等,控制就会通过上下文切换传递到对等线程,对等线程会执行一段时间,然后传递回主线程。

线程执行时不同于进程。因为一个线程的上下文要比一个进程的上下文小得多,线程的上下文切换要比进程的上下文切换快的多;另一方面,线程不是按照严格的父子层次来组织的,和一个进程相关的线程组成一个对等(线程)池(pool),独立于其他线程创建的线程。

主线程和其他线程的区别仅在于它总是进程中第一个执行的线程,对等(线程)池概念的主要影响是:一个线程可以杀死它的任何对等线程,或者等待它的任意对等线程终止。另外,每个对等线程都能读写相同的共享数据。

创建线程

线程通过调用 pthread_create 函数来创建其他线程,它的声明如下:

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

参数[*thread]:输出型参数,它包含新创建线程的ID。新线程可以通过调用 pthread_self 函数来获得它自己的线程ID,但仅在进程内部有效。

pthread_t pthread_self(void);

参数[*attr]:线程的属性(通常设置为NULL)。

参数[*start_routine]:函数指针指向所要创建的线程。

参数[*arg]:属于输入型参数,传递给该函数的指针。

返回值:成功返回0,失败返回错误码。

下面我们创建一个线程来验证一下:

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

void *thread_run(void *arg)
{
	printf("peer thread pid is %d,tid is %u\n",(int)getpid(),(unsigned long long)pthread_self());
	return NULL;
}
int main()
{
	pthread_t tid;
	int ret = pthread_create(&tid, NULL, thread_run, NULL);
	if(ret != 0)
	{
		printf("create thread error:%s\n",strerror(ret));
		exit(0);
	}
	printf("main thread is:%d,tid is:%u\n",(int)getpid(),(unsigned long long)pthread_self());
	sleep(1);
	return 0;
}

运行结果如下:

从执行结果可以看出,主线程与新创建线程的 pid 都是2802,即主线程与从线程从属于同一个进程,也进一步说明,在Linux下,没有真正意义上的线程。

需要注意的是 pthread_self 所获取到的是相对于进程的线程控制块的首地址,只是用来描述统一进程当中的不同的线程。而真正的内核当中的线程ID,对于多线程进程来说,每个tid实际上是不一样的。

线程终止

线程终止有三种方式:

(1)使用return可以终止一个线程,但是如果是在main函数中调用 return 的话,相当于进程退出,那么所有线程都会退出,相当于调用 exit 或者 _exit 函数。在任意一个线程中调用 exit 或 _exit 都会使所有线程退出。

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

void *thread_run1(void *arg)
{
	printf("thread1 running...\n");
	return;
}
int main()
{
	pthread_t tid;
	void *retCode;
	pthread_create(&tid, NULL, thread_run1, NULL);
	int ret_return = pthread_join(tid, &retCode);
	printf("join_ret : %d\n",ret_return);
	printf("thread return,thread id is : %u,return code is : %d\n",(unsigned long)tid,(int*)retCode);
	return 0;
}

运行结果:


(2)void pthread_exit(void *retval)

在线程中调用 pthread_exit ( ) 可以使线程自己退出。退出时的信息可以通过参数传出去,被等待它的线程获取。

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

void *thread_run2(void* arg)
{
	printf("thread2 running...\n");
	pthread_exit((void*)2);
}
int main()
{
	pthread_t tid;
	void *retCode;
	//pthread_exit
	pthread_create(&tid, NULL, thread_run2, NULL);
	pthread_join(tid,&retCode);
	printf("thread exit,thread id is:%u,return code is:%d\n",(unsigned long)tid,(int*)retCode);
	return 0;
}
运行结果:


注意:pthread_exit 或 return 返回的指针指向的内存单元必须是全局的或者用 malloc 分配出来的,不能在线程函数的栈上分配,因为当线程得到整个返回指针时,线程已经退出。

(3)int pthread_cancel ( pthread_t tid )

在一个线程中调用 pthread_cancel 函数可以终止另一个线程。假设A线程是被 pthread_cancel 异常终止的,则 pthread_join 获取的线程A的退出码就是 PTHREAD_CANCEL,这个宏的值是-1,可在 pthread.h 头文件中找到。

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

void *thread_run3(void* arg)
{
	while(1)
	{
		printf("pthread3 running...wait cancel...\n");
		sleep(1);
	}
}
int main()
{
	pthread_t tid;
	void* retCode;
	//pthread_cancel
	pthread_create(&tid, NULL, thread_run3, NULL);
	sleep(3);
	pthread_cancel(tid);
	int ret_cancel = pthread_join(tid,&retCode);
	printf("ret cancel:%d\n",ret_cancel);
	printf("thread return,thread id is:%u,return code is :%d\n",(unsigned long)tid,(int*)retCode);
	return 0;
}

运行结果:


返回值:成功返回0,失败返回错误码。

线程等待

在之前进程中,子进程结束时,需要父进程去回收资源,以及后续的处理,即确保子进程先于父进程退出,否则会出现僵尸进程,造成内存泄露等问题,进程可以调用 waitwaitpid 函数来防止僵尸进程的出现,那么线程亦是如此。

线程与进程又异曲同工之秒,或者可以直接认为它就是进程,同样,新线程的释放要先于主线程,因此,我们引入pthread_join函数。

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

参数[thread]:要等待的目标线程的id。

参数[**retval]:要等待的目标线程的退出码。线程在运行时,只能运行完且返回一个退出码,如果一个线程出错,则整个进程挂掉(原因在于进程是操作系统执行的基本单位)。对 void* 也不能解引用,因为解引用之后,无法知道其大小,因此需要使用 void** 二级指针。

当然,线程等待属于阻塞式等待,即新线程不退出,主线程就一直等待,主线程退出,则进程退出。

和Unix的 wait 函数不同,pthread_join 函数只能等待一个指定的线程终止,没有办法让 pthread_join 等待任意一个进程终止。

注意:线程可以被等待,也可以使用终止来结束新线程:

(1)使用return (void*),如果在主线程处使用就等同与exit。

(2)使用 pthread_exit(void* retval),等同于从线程退出。

(3)使用 pthread_cancel 取消这个线程,即线程允许被取消,其退出码为-1。

分离与结合线程

在任何一个时间点上,线程是可结合的(joinable)或者是分离的(detached)

一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是没有被释放的。

相反,一个分离的线程是不能被其他线程回收或者杀死的,它的存储器资源在它终止时由系统自动释放。一个可分离的线程是不能被等待的。

默认情况下,线程被创建为可结合的。为了避免存储器泄露,每个可结合线程都应该要么被其他线程显示回收,要么通过调用 pthread_detach 函数将线程分离。

如果一个可结合的线程运行结束没有被join,则它的状态类似于僵尸进程,也就是还有一部分资源没有被回收。

调用pthread_join之后,如果该线程没有运行结束,则调用者会被阻塞,当主线程退出后,就相当于进程退出了,这时候不管是可结合的或者可分离的进程都会被回收结束掉。

int pthread_detach(pthread_t thread);

pthread_detach总是返回0。分离分为新线程的分离和主线程的分离,下面就两者的分离作以阐述:

(1)新线程的分离:新线程分离后,主线程可能不知道新线程的分离,因此主线程可能会一直去join等待。

#include<pthread.h>
#include<stdlib.h>
#include<string.h>

void *pthread_run(void *arg)
{
	pthread_detach(pthread_self());
	printf("new thread->pid:%d,tid:%ld\n",getpid(),pthread_self());
	return;
}
int main()
{
	pthread_t tid;
	int err = pthread_create(&tid, NULL, pthread_run, NULL);
	if(err)
	{
		printf("%s\n",strerror(err));
		return -1;
	}
	int ret = 0;
	sleep(2);
	if(pthread_join(tid,NULL) == 0)
	{
		printf("wait sucess\n");
		ret = 0;
	}
	else
	{
		printf("wait failure\n");
		ret = 1;
	}
	return ret;
}

运行结果:


等待失败在所难免的,新线程的分离对于主线程来说是看不到的,主线程会一直去等待新线程,殊不知新线程已经被操作系统释放。

(2)主线程的分离:此时主线程已经确认与新线程分离,将不再等待新线程。

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

void *thread_run(void *arg)
{
	printf("%s\n",(char*)arg);
	return;
}
int main()
{
	pthread_t tid;
	pthread_create(&tid, NULL, thread_run, "thread run...");
	sleep(2);
	pthread_detach(tid);
	sleep(2);
	printf("join_ret:%d\n",pthread_join(tid,NULL));
	return 0;
}

运行结果:


主线程已经知道自己与新线程分离,因此将不再回收新线程,而是主线程执行完就直接退出。

线程总结

(1)线程是在进程内部运行的,实际上是在进程的地址空间上运行的。

(2)线程是进程的一个分支,创建线程的成本较低。

(3)线程是操作系统调度的基本单位,进程是系统资源分配的基本单位。

(4)线程属于轻量级的进程。

猜你喜欢

转载自blog.csdn.net/ypt523/article/details/80064872