1.线程的概念
线程,在操作系统当中这是一个相当重要的概念,线程的存在是为了解决在一个进程当中执行多个执行分支的任务,举一个很简单的例子:当你在用某个下载软件的时候,相当于你在运行该进程,而你在通过该软件进行下载时,很明显你在同一时间并不是只能进行一个下载任务,而且你在下载的时候,你可以使用键盘,鼠标来执行其他任务,而这些任务都是在同一个进程中在跑,这就是多线程的强大之处。
其实相对于多线程而言,在我们的操作系统当中还存在着多进程的概念,而两者究竟有何区别呢?
要回答这个问题,就不得不提我们的操作系统中,线程与进程究竟有什么区别了。
对于Linux来说,每一个进程都有他们独立的地址空间,独立的PCB,他们之间的数据是相互独立的,而且对于每个进程而言,都是相当的有野心,认为自己有权利支配整个内存空间(从地址空间的地址范围是从0x00000000到0xf f f f f f f f 就能看出),而由于每个进程的地址空间是独立的,所以在创建一个进程的时候,必然要有对应的地址空间,以及其他的相关内容,光是分配它独立的地址空间这一点,对于操作系统而言就是相当大的开销,若是每一个执行分支就是一个进程的话,那么这笔开销是相当大的,况且对于多个执行分支之间必然会有信息的交流,进程间通信则是必不可少了,而我们知道进程间通信是有些复杂的,对于多个执行分支之间频繁的通信,这是相当不现实的。
而在引入多线程之后,这就完全不一样了。首先,我们的同一个进程的多个线程之间是共享同一个地址空间的,这样就为操作系统节省了一大笔开销,而且由于共享同一个地址空间,所以线程之间的数据有相当大的一部分是共享的,这也省去了进程间频繁通信所带来的麻烦。但是也由于共享地址空间这样的进制,导致某些数据被多个线程同时进行修改,从而造成程序的运行的错误(比如static变量所带来的巨大麻烦),而这也是多线程编程所绕不开的问题。
而且虽说线程之间有很多的资源是共享的,如文件描述符表(不同的线程之间共享文件偏移量),当前用户id与组id,以及每种信号的处理方式,工作目录等,但是对于线程而言,也是有一部分的资源是必须私有的:
①线程id(用于标识一个线程);
②上下文信息(在线程中断之后,用于恢复的寄存器,栈指针,程序计数器的信息);
③栈空间(每个线程必须有其独立的栈帧空间);
④errno变量,信号屏蔽字,调度优先级等;
但是话说回来,对于我们的Linux,其实并不认识所谓的线程,我们在标识一个线程的时候,同样也是用PCB(task_struct)这一模块来进行描述的,并且让它供调度器调度,也就是说共享地址空间这一机制确实存在,但是在Linux调度时看来,线程与进程并无差别,由此Linux下的线程也叫我们的“轻量级进程”。
所以,我们可以明白一点,对于一个进程来说,若是其中只有一个执行分支,那么也就可以认为这个进程中只有一个线程(主线程),供调度的只有一个线程,而对于多线程的进程而言,每一个线程都有自己的PCB,供调度的也就是这些PCB所对应的线程,由此我们可以明白,在Linux下是将一个个的进程给细分了,分成一个个对应的线程,所以Linux下的供CPU调度单位是我们的线程,而我们的进程是用来承担资源的分配的基本单位。
2.线程控制
以下所有函数均为POSIX标准下的线程库函数,用gcc编译时要加-lpthread选项,要引入pthread.h头文件。
①创建线程
函数原型:
int pthread_create(pthread_t* thread,const pthread_attr_t* attr,void* (*start_routine)(void*),void* arg);
函数功能:调用该函数之后,分成两个执行流,向下执行为主线程。
函数参数:第一个参数thread是指向线程标识符的指针,第二个参数用来设置线程状态,第三个参数用来表示线程运行函数的起始地址(返回值为void*,参数为void*的函数指针),第四个参数表示运行函数的参数。返回值:若创建线程成功返回0,否则返回错误码。
②线程等待
函数原型:
int pthread_join(pthread_t thread,void** retval);//接收的退出码类型视情况而定
函数功能:调用该函数后,主线程发生阻塞式等待,等待成功后将线程资源进行回收。
函数参数:第一个参数thread表示要等待的线程标识符,第二个参数用于接收线程的退出码(只能用于监视新线程正常退出的退出码,若是异常退出,那么整个进程就会异常退出了,其他线程(包括主线程)也自然就异常退出了)。
这里得对第二个参数的情况进行说明:(也就是线程退出的三种情况)
a.线程函数走到末尾返回,则retval指向的空间接收return的值;
b.线程被取消pthread_cancel,则retval指向的空间被置为PTHREAD_CANCELED[ (void*)-1 ];
c.线程函数走到pthread_exit退出,则retval指向的空间接收pthread_exit的参数;
返回值:若等待成功,则返回0,否则返回错误码。
③线程退出
函数原型:
void pthread_exit(void* retval);//退出码类型可自行指定
函数功能:用于终止当前线程,这里需要注意一点线程的退出不能用exit(),exit()函数是用于进程退出的。
函数参数:唯一参数retval用于接收线程退出码(这里的retval指向的变量必须是全局变量或者是动态申请的堆上空间,决不能是栈空间的变量)。
④线程取消
函数原型:
int pthread_cancel (pthread_t thread);
函数功能:对指定的线程进行终止
函数参数:唯一参数thread表示要终止的线程标识符
返回值:若成功返回0,否则返回非0错误码
3.线程分离
我们的线程的状态在任一时刻,大致分为两种:可结合的(joinable)和可分离的(detached),一个线程默认是可结合的。
首先,当一个线程处于可结合的状态时,在线程终止后,该线程所占用的资源是不会被回收的,若是没有其他线程对其调用pthread_join进行等待的话,那么就会造成这部分的资源无法被回收,则该线程处于类似于僵尸进程的状态,这将会占用我们的操作系统资源的,这种情况我们是要尽量规避的,所以对于可结合的线程,我们会在其他线程(主线程)中调用pthread_join进行等待,回收其资源;
但是,在其他的线程中调用pthread_join,不可避免的会造成阻塞式等待,这样的话在某些情况下是不想看到的,但是线程的资源又必须得回收,由此我们就可以将该线程设置为可分离的,让这个线程在终止的时候就将它所占用的资源进行释放。而为了实现这一目标,我们就得调用下面的接口:
函数原型:
int pthread_detach(pthread_t thread);
函数功能:将线程设置为可分离的(detached),线程终止时对所占用资源进行自动释放,非阻塞的
参数:唯一参数thread表示所要设置成可分离的状态的线程的线程标识符,在子线程中可以用pthread_self()进行获取,在父线程中可传入对应线程标识符
返回值:若成功,则返回0,否则返回对应错误码
这里需要注意的一点是:在调用pthread_detach成功后,再在其他线程中调用pthread_join进行等待就会失败。