一:Linux下的线程
Linux是一种“多进程单线程”的操作系统,Linux本身只有进程的概念,而其所谓的“线程”本质上在内核里仍然是进程。Linux系统内核支持轻量级进程,即用进程模拟线程。
进程成为分配系统资源的实体,那么Linux下的“多线程”如上图,在一个进程的地址空间有多个控制流,即同一进程的多个线程在同一块地址空间上运行(共享同一块进程资源和环境)
多线程的控制流可以长期并存,操作系统会在各线程之间调度和切换,就像在多个进程之间调度和切换一样。
各线程共享以下进程资源和环境:
1:文件描述符表
2:每种信号的处理方式(SIG_IGN,SIG_DFI或者自定义的信号处理函数)
3:当前工作目录
4:用户id和组id
私有以下资源:
1:线程id
2:上下文,包括各种寄存器的值,程序计数器和栈指针
3:栈空间
4:errno变量
5:信号屏蔽字
6:调度优先级
Linux下的线程库函数是由POSIX标准定义的,称为POSIX thread或者pthread。在Linux上线程函数位于libpthread共享库中,因此在编译时要加上-lpthread选项。
二:进程和线程的区别
进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,他的执行需要系统分配资源创建实体之后才能执行。
线程的特点就是在不需要独立资源的情况下就可以运行,因此会极大地节省资源的开销,以及处理时间。
1. 相同点:
二者都可以独立调度,并且二者都具有各自的实体,是系统独立管理的对象个体。在多任务程序中,子进程(子线程)的调度一般与父进程(父线程)平等竞争。
2. 实现方式的差异:
进程是资源分配的基本单位,线程是调度的基本单位。进程的个体间是完全独立的,而线程间是彼此依存的。多进程环境中,任何一个进程的终止,不会影响到其他进程。而多线程环境中,父线程终止,全部子线程被迫终止(没有了资源),而任何一个子线程终止一般不会影响其他线程,除非子线程执行了exit()系统调用,任何一个子线程执行exit(),全部线程同时灭亡!
从系统实现的角度讲,进程的实现是调用fork系统调用:
pid_t fork(void);
线程的实现采用线程库函数,常用POSIX线程库
int pthread_create(pthread_t *thread,pthread_attr_t *attr,
void *(*start_rountine)(void *),void *arg);
参数说明:
thread:指向pthread_create类型的指针,用于引用新创建的线程。
attr:用于设置线程的属性,一般不需要特殊的属性,所以可以简单地设置为NULL。
start_routine:传递新线程所要执行的函数地址。
arg:新线程所要执行的函数的参数。
返回值:
a.调用如果成功,则返回值是0;如果失败则返回错误代码。
b.每个线程都有自己的线程ID,以便在进程内区分。线程ID在pthread_create调用时回返给创建线程的调用者;一个线程也可以在创建后使用pthread_self()调用获取自己的线程ID:
pthread_self(void);
vfork()也是一个系统调用,用来创建一个新的进程,它创建的进程并不复制父进程的资源空间而是共享,也就是说vfork()实现的是一个接近线程的实体,只是用进程的方式来管理它。
并且vfork()的子进程和父进程的运行时间是确定的:子进程“结束”后父进程才运行。这个“结束”并非子进程完成退出的意思,而是进程返回时。
一般采用vfork()的子进程,都会紧接着执行execv启动一个全新的进程,该进程的进程空间与父进程完全独立不相干,所以不需要复制父进程资源空间。此时,execv返回时父进程就认为子进程“结束”了,自己开始运行。实际上子进程继续在一个完全独立的空间运行着。
3:多任务程序设计模式的区别
由于进程间是独立的,所以在设计多进程程序时,需要做到资源独立管理时就很方便,而线程就比较麻烦了。
比如多任务的TCP程序服务端,父进程执行accept()一个客户端连接请求之后会返回一个新建立的连接的描述符DES,此时如果fork()一个子进程,将DES带入到子进程的空间去处理该连接请求,父进程继续accept()等待别的客户端连接请求,这样设计非常简练,而且父进程可以用同一变量(val)保存accept()的返回值,因为子进程会复制val到自己的空间,父进程再覆盖此前的值不影响子进程的工作。
但是如果换成多线程,父线程就不能复用一个变量val多次执行accept()了,因为子线程没有复制val的存储空间,而是使用父线程的。如果子进程在读取val时父线程接受了另外一个客户端请求覆盖了该值,则子进程无法继续处理上一次的连接任务了。(改进办法是子线程立马复制val的值在自己的栈区)。
另外:多进程环境间完全独立,要实现通信的话就得采用进程间的通信方式,他们通常是耗时间的!而线程则不用任何手段数据都是共享的。(注意多个子线程在同时执行写入操作时需要实现互斥)。
4:实体间(进程间,线程间,进线程间)通信方式的不同
进程间的通信方式有:
A .共享内存 B.消息队列 C.信号量 D.命名管道 E. 匿名管道 F. 信号 G. 文件 H. socket
线程间的通信方式有:
A.可沿用进程的通信方式 B.互斥量 C.自旋锁 D.条件变量 D.读写锁 E.线程信号 G.全局变量
注意:线程间的通信用的信号不能采用进程间的信号,因为信号是基于进程为单位的,而线程是共属于同一进程空间的。故要采用线程信号!
而且:进程间采用的通信方式要么需要切换内核上下文,要么要与外设访问(命名管道,文件)。所以速度会比较慢,而线程采用自己特有的通信方式的话,基本都在自己的进程空间内完成,不存在切换,所以通信速度会比较快!
另外, 进程与线程之间的穿插通信的方式,除信号以外其他进程间通信方式都可采用。
5:控制方式的异同
进程与线程的身份标识id管理方式不一样,进程的id为pid_t类型,实际为一个int 型变量(有限的),每个进程有自己的PCB,对于进程的管理都是通过PID来实现的。
每创建一个进程,内核中就会创建一个结构体来存储该进程的全部信息(Task_struct)每一个存储进程信息的节点也都保存着自己的PID。需要管理该进程时就通过这个ID来实现。当子进程结束要回收时(子进程调用exit()或代码执行完),需要通过wait()系统调用来进行,未回收的消亡进程会成为僵尸进程,其进程实体已经不复存在,但会虚占PID资源,因此有必要回收。
线程的ID是一个long 型的变量:
它的范围大得多,管理方式也不一样,一般在本进程空间内作用就可以了。
对于线程而言,若要主动终止需要调用pthread_exit(),主线程需要调用pthread_join()来回收(前提是该线程没有被detached)。发送线程信号也是通过线程ID实现的。
6:资源管理方式的异同
进程本身是资源分配的基本单位,因而它的资源都是独立的,如果有多进程间的共享资源,就要用到进程间的通信方式了,比如共享内存。共享数据就放在共享内存去,大家都可以访问,为保证数据写入的安全,加上信号量一同使用。一般而言,共享内存都是和信号量一起使用。消息队列则不同,由于消息的收发是原子操作,因而自动实现了互斥,单独使用就是安全的。
线程间要使用共享资源不需要用共享内存,直接使用全局变量即可,或者malloc()动态申请内存。显得方便直接。而且互斥使用的是同一进程空间内的互斥量,所以效率上也有优势。
实际中,为了使程序内资源充分规整,也都采用共享内存来存储核心数据。不管进程还是线程,都采用这种方式。原因之一就是,共享内存是脱离进程的资源,如果进程发生意外终止的话,共享内存可以独立存在不会被回收(是否回收由用户编程实现)。进程的空间在进程崩溃的那一刻也被系统回收了。虽然有coredump机制,但也只能是有限的弥补。共享内存在进程down之后还完整保存,这样可以拿来分析程序的故障原因。同时,运行的宝贵数据没有丢失,程序重启之后还能继续处理之前未完成的任务,这也是采用共享内存的又一大好处。
总结之,进程间的通信方式都是脱离于进程本身存在的,是全系统都可见的。这样一来,进程的单点故障并不会损毁数据,当然这不一定全是优点。比如,进程崩溃前对信号量加锁,崩溃后重启,然后再次进入运行状态,此时直接进行加锁,可能造成死锁,程序再也无法继续运转。再比如,共享内存是全系统可见的,如果你的进程资源被他人误读误写,后果肯定也是你不想要的。所以,各有利弊!
7).个体间辈分关系的迥异
进程的辈分关系森严,在父进程没有结束前,所有的子进程都尊从父子关系,也就是说A创建了B,则A与B是父子关系,B又创建了C,则B与C也是父子关系,A与C构成爷孙关系,也就是说C是A的孙子进程。在系统上使用pstree命令打印进程树,可以清晰看到备份关系。
多线程间的关系没有那么严格,不管是父线程还是子线程创建了新的线程,都是共享父线程的资源,所以,都可以说是父线程的子线程,也就是只存在一个父线程,其余线程都是父线程的子线程。
8).进程池与线程池的技术实现差别
我们都知道,进程和线程的创建时需要时间的,并且系统所能承受的进程和线程数也是有上限的,这样一来,如果业务在运行中需要动态创建子进程或线程时,系统无法承受不能立即创建的话,必然影响业务。综上,聪明的程序员发明了一种新方法——池。
在程序启动时,就预先创建一些子进程或线程,这样在需要用时直接使唤。这就是老人口中的“多生孩子多种树”。程序才开始运行,没有那么多的服务请求,必然大量的进程或线程空闲,这时候一般让他们“冬眠”,这样不耗资源,要不然一大堆孩子的口食也是个负担啊。对于进程和线程而言,方式是不一样的。另外,当你有了任务,要分配给那些孩子的时候,手段也不一样。下面就分别来解说。
进程池
首先创建了一批进程,就得管理,也就是你得分开保存进程ID,可以用数组,也可用链表。建议用数组,这样可以实现常数内找到某个线程,而且既然做了进程池,就预先估计好了生产多少进程合适,一般也不会再动态延展。就算要动态延展,也能预估范围,提前做一个足够大的数组。不为别的,就是为了快速响应。本来做进程池的目的也是为了效率。
接下来就要让闲置进程冬眠了,可以让他们pause()挂起,也可用信号量挂起,还可以用IPC阻塞,方法很多,分析各自优缺点根据实际情况采用就是了。
然后是分配任务了,当你有任务的时候就要让他干活了。唤醒了进程,让它从哪儿开始干呢?肯定得用到进程间通信了,比如信号唤醒它,然后让它在预先指定的地方去读取任务,可以用函数指针来实现,要让它干什么,就在约定的地方设置代码段指针。这也只是告诉了它怎么干,还没说干什么(数据条件),再通过共享内存把要处理的数据设置好,这也子进程就知道怎么做了。干完之后再来一次进程间通信然后自己继续冬眠,父进程就知道孩子干完了,收割成果。
最后结束时回收子进程,向各进程发送信号唤醒,改变激活状态让其主动结束,然后逐个wait()就可以了。
线程池
线程池的思想与上述类似,只是它更为轻量级,所以调度起来不用等待额外的资源。
要让线程阻塞,用条件变量就是了,需要干活的时候父线程改变条件,子线程就被激活。
线程间通信方式就不用赘述了,不用繁琐的通信就能达成,比起进程间效率要高一些。
线程干完之后自己再改变条件,这样父线程也就知道该收割成果了。
整个程序结束时,逐个改变条件并改变激活状态让子线程结束,最后逐个回收即可。
三:线程控制代码
线程创建
在进程被创建时,系统会为其创建一个主线程,而要在进程中创建新的线程,则可以调用pthread_create函数:
#include <pthread.h>
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数说明:
thread:指向pthread_create类型的指针,用于引用新创建的线程。
attr:用于设置线程的属性,一般不需要特殊的属性,所以可以简单地设置为NULL。
start_routine:传递新线程所要执行的函数地址。
arg:新线程所要执行的函数的参数。
返回值:
调用如果成功,则返回值是0;如果失败则返回错误代码。
每个线程都有自己的线程ID,以便在进程内区分。线程ID在pthread_create调用时回返给创建线程的调用者;一个线程也可以在创建后使用pthread_self()调用获取自己的线程ID:
pthread_self (void);
线程退出
线程的退出方式有三种:
(1)执行完成后隐式退出;
(2)由线程本身显示调用pthread_exit 函数退出;
pthread_exit (void * retval);
//retval 是void* 类型,和线程函数返回值用法一样
//其他线程可以调用pthread_join获得这个指针
(3)被其他线程用pthread_cance函数终止:
pthread_cancel (pthread_t thread);
注意
pthread_exit或者return返回的指针所指向的内存单元必须是全局或者使用malloc分配的,不能在线程函数的栈上分配,因为其他线程得到这个返回指针时线程函数已近退出了。
线程等待
如果一个线程要等待另一个线程的终止,可以使用pthread_join函数,该函数的作用是调用pthread_join的线程将被挂起直到线程ID为参数thread的线程终止:
pthread_join (pthread_t thread, void** retval);
thread线程以不同的方法终止,通过pthread_join得到的终.止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ptr所指向的单元里存放的是thread线程函数的返回值。
如果thread线程被别的线程调用pthread_cancel异常终掉,value_ptr所指向的单元.里存放的是常数PTHREAD_CANCELED(pthread库中一般是-1)。
如果thread线程是.自.己调.用pthread_exit终.止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ptr参数。
下面是线程创建/终止/等待代码:
这里写代码片#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
void* pthread_run(void* arg)
{
printf("new pthread, thread is : %u,\
pid : %d\n",pthread_self(),getpid());
// return NULL;
// pthread_exit((void*)123);
//exit(123);
//return(void*)123;
}
int main()
{
pthread_t tid;
int res = pthread_create(&tid,NULL,pthread_run,NULL);
if(res!=0){
printf("create thread error!info is : %s\n",strerror(res));
exit(res);
}
// while(1){
// printf("main thread, thread is : %u, pid : %d\n",\
// pthread_self(),getpid());
// sleep(1);
// exit(1);
// }
pthread_cancel(tid);
void *ret ;
pthread_join(tid,&ret);
printf("join new thread success,ret : %d\n",(int)ret);
return 0;
}
注释部分可以自行放开测试!
线程分离
在任何⼀一个时间点上,线程是可结合的(joinable)或者是分离的(detached)。⼀一个可结合的线程能够被其他线程收回其资源和杀死。在被其他线程回收之前,它的存储器资源(例如栈)是不释放的。相反,⼀一个分离的线程是不能被其他线程回收或杀死的,它的存储器 资源在它终⽌止时由系统⾃自动释放。
一般情况下,线程终止后,其终止状态⼀一直保留到其它线程调用pthread_join获取它的状态为止。 但是线程也可以被置为detach 状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调将返回EINVAL。 对一个尚未detach的线程调用pthread_join或pthread_detach都可以把该线程置为detach状态,也 就是说,不能对同一线程调用两次pthread_join,或者如果已经对一个线程调用 了pthread_detach就不能再调⽤用pthread_join了。
int pthread_detach(pthread_t thread);
默认情况下,线程被创建成可结合的。为了避免存储器泄漏,每个可结合线程都应该要么被显示地回收,即调用pthread_join;要么通过调用pthread_detach函数被分离。
如果一个可结合线程结束运行但没有被join,则它的状态类似于进程中的Zombie Process(僵尸进程),即还有一部分资源没有被回收,所以创建线程者应该调用pthread_join来等待线程运行结束,并可得到线程的退出代码,回收其资源。
由于调用pthread_join后,如果该线程没有运行结束,调用者会被阻塞,在有些情况下我们并不希望如此。例如,在Web服务器中当主线程为每个新来的连接请求创建一个子线程进行处理的时候,主线程并不希望因为调用pthread_join而阻塞(因为还要继续处理之后到来的连接请求),这时可以在子线程中加入代码
pthread_detach(pthread_self())
或者父线程调用
pthread_detach(thread_id)
(非阻塞,可立即返回)
这将该⼦子线程的状态设置为分离的(detached),如此一来,该线程运行结束后会自动释放所有资源。
#include <pthread.h>
#include <stdio.h>
#include <string.h>
void * thread_run(void* arg)
{
printf("thread_run is running ! %s\n",(char*)arg);
sleep(2);
pthread_detach(pthread_self());
}
int main()
{
pthread_t tid;
int ret ;
ret = pthread_create(&tid,NULL,thread_run,NULL);
//wait
int res;
void* val;
res = pthread_join(tid,&val);
printf("pthread joined %s\n",(char*)val);
return 0;
}