Linux线程基础讲解学习.docx
《Linux线程基础讲解学习.docx》由会员分享,可在线阅读,更多相关《Linux线程基础讲解学习.docx(11页珍藏版)》请在冰豆网上搜索。
Linux线程基础讲解学习
Linux线程基础
Linux系统下的多线程编程
1.1引言
线程(thread)技术早在60年代就被提出,但真正应用多线程到操作系统中去,是在80年代中期,solaris是这方面的佼佼者。
传统的Unix也支持线程的概念,但是在一个进程(process)中只允许有一个线程,这样多线程就意味着多进程。
现在,多线程技术已经被许多操作系统所支持,包括Windows/NT,当然,也包括Linux。
为什么有了进程的概念后,还要再引入线程呢?
使用多线程到底有哪些好处?
什么的系统应该选用多线程?
我们首先必须回答这些问题。
1.2使用多线程的理由
一是和进程相比,它是一种非常"节俭"的多任务操作方式。
进程是系统中程序执行和资源分配的基本单位。
我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这就导致了进程在进行切换等操作起到了现场保护作用,这是一种"昂贵"的多任务工作方式。
但是为了进一步减少处理机的空转时间支持多处理器和减少上下文切换开销,进程演化中出现了另外一个概念,这就是线程,也被人称为轻量级的进程。
它是一个进程内的基本调度单位。
线程是在共享的内存空间中并发的多道执行路径,它们共享一个进程的资源,比如文件描述符和信号处理等。
因此,大大减少了上下文切换的开销。
而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。
二是线程间方便的通信机制。
对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。
线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。
当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1)提高应用程序响应。
这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(timeconsuming)置于一个新的线程,可以避免这种尴尬的情况。
2)使多CPU系统更加有效。
操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
3)改善程序结构。
一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
1.3进程和线程
可执行文件由指令和数据组成。
进程就是在计算机上运行的可执行文件针对特定的输入数据的一个实例,同一个可执行程序文件如果操作不同的输入数据就是两个不同的进程。
线程是进程的一条执行路径,它包含独立的堆栈和CPU寄存器状态,每个线程共享其所附属的进程的所有的资源,包括打开的文件、页表(因此也就共享整个用户态地址空间)、信号标识及动态分配的内存等等。
线程和进程的关系是:
线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一物理内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。
Linux在核外采用1:
1线程模型,即用一个核心进程(轻量进程)对应一个线程,将线程调度等同于进程调度,交给核心完成,而其它诸如线程取消、线程间的同步等工作,都是在核外线程库中完成的。
因此可以把进程看作一组线程,这组线程拥有相同的线程组号(TGID),这个TGID就是这组线程序所附属的进程的ID号,每个线程的ID号就是我们用ps命令所看到的LWP号。
为了方便,从现在起我们用任务来代替进程和线程,即每提到任务,我们就是指线程和进程,除非要强调线程和进程之间的不同之处。
任务的周期从被fork开始一直到给任务从进程表中消失。
一个进程包括:
正文段(text),数据段(data),栈段(STACK)和共享内存段(SHAREDMEMORY)。
线程跟进程一样,都拥有一张控制表,线程将相关的变量值放在线程控制表中。
一个进程可以拥有一个或者一个以上的线程,也就是有多个线程控制表和堆栈寄存器。
但是它们是共享一个内存空间的,这就导致了每个线程的操作都会影响到其他线程。
所以线程的同步是非常重要的。
线程可分为用户级线程和核心级线程。
(1)用户级线程主要是解决上下问切换的问题,它的调度算法和调度过程都是用户自己选择决定的,在运行时并不需要特定的内核支持,操作系统都会提供一个库函数,包括对线程的创建、调度、撤销的等功能。
而内核仍然度进程进行管理。
如果一个进程中的一个线程调用了阻塞的系统调用,那么该进程和该进程中的的其他线程都会被阻塞,这就无法发挥多处理器的优势。
(2)核心级线程是允许不同进程中的线程按照同一相对优先调度方法进行调度。
这样就可以充分发挥多处理器的作用。
现在大多数系统都是采用用户级和核心级相结合的方法,一个用户级线程可以对应一个或者一个以上的核心级线程,这样既可以满足多处理机系统的需要,也可以最大限度的减少了调度开销。
在Linux的发展历程中,在方开始,并不真正意义上的支持线程,到出现一对一的线程模型(一个用户级线程对应一个内核级线程),再到如今的NPTL,有了很大的改进,但是仍然采用的是一对一的线程模型。
3线程操作
用法:
使用头文件pthread.h,连接时需要使用库libpthread.a。
命令如下:
gcccreatethread.c–ocreatethread–lpthread
线程ID,tid,类型pthread_t,线程的标识符,用于唯一标识一个线程。
通过调用pthread_self()函数可以获得自身的线程号。
pthread_t在头文件/usr/include/bits/pthreadtypes.h中定义
typedefunsignedlongintpthread_t;
2.1函数pthread_create用来创建一个线程
创建线程实际上就是确定调用该线程函数的入口点,在线程创建之后,就开始运行相关的线程函数。
在该函数运行结束,线程也会随着退出。
它的原型为:
intpthread_create(pthread_t*restrictthread,constpthread_attr_t*restrictattr,
void*(*thread_func)(void*),void*restrictarg);
第一个参数为指向线程标识符的指针,
第二个参数用来设置线程属性,这个将在后面详细说明。
赋值为NULL即缺省
第三个参数是线程运行函数的起始地址,
线程入口函数接口必须如下:
void*thread_func(void*)。
该函数返回时,相应的线程就结束了,
第四个参数是运行函数的参数数组指针。
返回值:
当创建线程成功时,函数返回0,若不为0则说明创建线程失败,
常见的错误返回代码为和。
EAGAIN表示系统限制创建新的线程,例如线程数目过多了;
EINVAL表示第二个参数代表的线程属性值非法。
创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。
注:
创建子线程时,传给子线程的输入参数最好是malloc()返回的指针(这样的指针指向进程堆中的存储空间)或指向全局变量的指针,而不要是指向局部变量的指针。
因为当子线程访问输入参数时,创建子线程的函数可能已结束,局部变量也就不存在了。
关于restrict:
C99中新增加了restrict修饰的指针:
由restrict修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于第一个时,才能对对象进行存取。
对对象的存取都限定于基于由restrict修饰的指针表达式中。
由restrict修饰的指针主要用于函数形参,或指向由malloc()分配的内存空间。
restrict数据类型不改变程序的语义。
编译器能通过作出restrict修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。
pthread_tpthread_self(void);//获取本线程的线程ID
2.2线程的退出:
线程结束时的退出状态值是start_routine函数用phread_exit()函数返回的返回值。
2.2.1pthread_join
如果主线程在非分离子线程(joinable)之前退出,会带来未知问题,所以一般使用pthread_join函数让主线程阻塞,直到所有线程都已经退出。
函数pthread_join用来等待一个线程的结束。
函数原型为:
intpthread_join(pthread_tthread,void**status);
第一个参数为被等待的线程标识符,
第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。
pthread_join()会阻塞调用它的线程,直到参数thread指定的线程结束。
thread指定的线程必须在当前进程中,且必须是非分离的。
status接收指定线程终止时的返回状态码。
由于一个进程中的数据段是共享的,因此通常在线程退出之后,退出线程所占的资源并不会随着线程的结束而得到释放。
当pthread_join()返回时,终止线程占用的堆栈等资源已被回收。
返回值:
成功返回0
不能有多个线程等待同一个线程终止,否则返回错误码ESRCH
ESRCHtid指定的线程不是一个当前线程中合法且未分离的线程。
EDEADLKtid指定的是当前线程。
EINVALtid非法。
2.2.2pthread_detach分离线程
joinable/detached是线程的属性,细节可以看下文解释。
在线程设置为joinable后,可以调用pthread_detach(pthread_tthread)使之成为detached。
但是相反的操作则不可以。
分离线程不会被别的线程等待,所以执行完立即释放资源。
线程一般有分离和非分离两种状态。
默认的情形下是非分离状态,父线程维护子线程的某些信息并等待子线程的结束,在没有显示调用join的情形下,子线程结束时,父线程维护的信息可能没有得到及时释放,如果父线程中大量创建非分离状态的子线程(在LINUX系统中使用pthread_create函数),可能会产生表示出错的返回值12,即ENOMEM(在errno.h中定义),表示栈空间不足。
而对分离线程来说,不会有其他的线程等待它的结束,它运行结束后,线程终止,资源及时释放。
为了防止pthread_create线程是出现返回值12(即ENOMEM)的错误,建议以如下的方式创建分离状态的线程!
2.2.3pthread_cancel杀死一个线程
intpthread_cancel(pthread_tthread);函数调用成功返回0。
1)线程的取消定义:
一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的终止(取消)请求而强制终止,这个通过调用pthread_cancel来达到目的。
2)线程的取消语义:
线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。
线程接收到CANCEL信号的缺省处理(即pthread_create()创建线程的缺省状态)是继续运行至取消点,也就是说设置一个CANCELED状态,线程继续运行,只有运行至Cancelation-point的时候才会退出。
3.取消点:
根据POSIX标准,pthread_join()、pthread_testcancel()、pthread_cond_wait()、pthread_cond_timedwait()、sem_wait()、sigwait()等函数以及read()、write()等会引起阻塞的系统调用都是Cancelation-point,而其他pthread函数都不会引起Cancelation动作。
但是pthread_cancel的手册页声称,由于LinuxThread库与C库结合得不好,因而目前C库函数都不是Cancelation-point;但CANCEL信号会使线程从阻塞的系统调用中退出,并置EINTR错误码,因此可以在需要作为Cancelation-point的系统调用前后调用pthread_testcancel(),从而达到POSIX标准所要求的目标。
2.2.4pthread_exit()
线程可以通过自身执行结束来结束,也可以通过调用pthread_exit()来结束自身的执行。
函数pthread_exit用来结束一个线程自身。
它的函数原型为:
voidpthread_exit(void*thread_return);
唯一的参数是函数的返回代码,只要pthread_join中的第二个参数thread_return不是NULL,这个值将被传递给thread_return。
最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程则返回错误代码ESRCH。
2.3线程的属性
其中pthread_create函数的第二个参数,是关于线程属性的设置。
这些属性主要包括邦定属性、分离属性、堆栈地址、堆栈大小、优先级。
其中系统默认的是非邦定、非分离、缺省8M的堆栈、与父进程同样级别的优先级。
在pthread_create中,把第二个参数设置为NULL的话,将采用默认的属性配置。
2.3.1线程属性结构为pthread_attr_t
它同样在头文件/usr/include/pthread.h中定义。
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。
属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。
默认的属性为非绑定、非分离、缺省8M的堆栈、与父进程同样级别的优先级。
函数名pthread_attr_init
功能:
对线程属性变量的初始化。
头文件:
函数原型:
intpthread_attr_init(pthread_attr_t*attr);
函数传入值:
attr:
线程属性。
函数返回值:
成功:
0失败:
-1
2.3.2属性绑定
在LINUX中,采用的是“一对一”的线程机制。
也就是一个用户线程对应一个内核线程。
邦定属性就是指一个用户线程固定地分配给一个内核线程,因为CPU时间片的调度是面向内核线程(轻量级进程)的,因此具有邦定属性的线程可以保证在需要的时候总有一个内核线程与之对应,
而与之对应的非邦定属性就是指用户线程和内核线程的关系不是始终固定的,而是由系统来分配。
轻进程
关于线程的绑定,牵涉到另外一个概念:
轻进程(LWP:
LightWeightProcess)。
轻进程可以理解为内核线程,它位于用户层和系统层之间。
系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。
默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。
绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。
被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。
通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
属性绑定函数pthread_attr_setscope
功能:
设置线程绑定属性
头文件:
函数原型:
intpthread_attr_setscope(pthread_attr_t*attr,intscope);
函数传入值:
attr:
线程属性。
scope:
PTHREAD_SCOPE_SYSTEM(绑定)
PTHREAD_SCOPE_PROCESS(非绑定)
函数返回值:
成功:
0失败:
-1
#include
pthread_attr_tattr;
pthread_ttid;
/*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr,PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid,&attr,(void*)my_function,NULL);
2.3.3线程的分离状态
(参考2.2.2)
线程的分离状态决定一个线程以什么样的方式来终止自己。
非分离状态
线程的默认属性为非分离状态,这种情况下,原有的线程等待创建的线程结束。
只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。
分离线程
它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。
函数名pthread_attr_setdetachstate
功能:
设置线程分离属性。
头文件:
函数原型:
intpthread_attr_setdetachstate(pthread_attr_t*attr,intdetachstate);
函数传入值:
attr:
线程属性。
detachstate:
PTHREAD_CREATE_DETACHED(分离)
PTHREAD_CREATE_JOINABLE(非分离)
函数返回值:
成功:
0失败:
-1
这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在pthread_create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用pthread_create的线程就得到了错误的线程号。
要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread_cond_timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread_create返回。
设置一段等待时间,是在多线程编程里常用的方法。
但是注意不要使用诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
2.3.4线程优先级
线程的优先级,它存放在结构sched_param中,结构sched_param在文件bits/sched.h中
定义如下:
structsched_param{
intsched_priority;
};
结构sched_param的子成员sched_priority控制一个优先权值,大的优先权值对应高的优先权。
系统支持的最大和最小优先权值可以用
sched_get_priority_max函数和sched_get_priority_min函数分别得到。
注意:
如果不是编写实时程序,不建议修改线程的优先级。
因为,调度策略是一件非常复杂的事情,如果不正确使用会导致程序错误,从而导致死锁等问题。
一般说来,我们总是先取优先级,对取得的值修改后再存放回去。
函数名pthread_attr_getschedparam
功能:
得到线程优先级。
头文件:
函数原型:
intpthread_attr_getschedparam(pthread_attr_t*attr,structsched_param*param);
函数传入值:
attr:
线程属性;
param:
线程优先级;
函数返回值:
成功:
0失败:
-1
函数名pthread_attr_setschedparam
功能:
设置线程优先级。
头文件:
函数原型:
intpthread_attr_setschedparam(pthread_attr_t*attr,structsched_param*param);
函数传入值:
attr:
线程属性。
param:
线程优先级。
#include
#include
pthread_attr_tattr;
pthread_ttid;
sched_paramparam;
intnewprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr,¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr,¶m);
pthread_create(&tid,&attr,(void*)myfunction,myarg);
2.3.5线程堆栈大小
pthread_create创建线程时,若不指定分配堆栈大小,系统会分配默认值,查看默认值方法如下:
#ulimit-s
8192
#
上述表示为8M;单位为KB。
也可以通过#ulimit-a其中stacksize项也表示堆栈大小。
#ulimit-sstacksize用来重新设置stack大小。
正在执行的程序为执行该程序的初始(或主)线程维护一个主栈,并为每个从属线程维护不同的栈。
栈是临时内存地址空间,用于保留子程序或函数引用调用期间的参数和自动变量。
可能需要反复试验才能确定最佳栈大小。
如果栈大小太小,不足以满足线程的运行需要,可能会导致无提示数据损坏或段故障。
一般来说默认堆栈大小为8388608;堆栈最小为16384。
单位为字节。
堆栈最小值定义为PTHREAD_STACK_MIN,包含#include后可以通过打印其值查看。
对于默认值可以通过pthread_attr_getstacksize(&attr,&stack_size);打印stack_size来查看。
尤其在嵌入式中内存不是很大,若采用默认值的话,会导致出现问题,若内存不足,则pthread_create会返回12,定义如下:
#defineEAGAIN11
#defineENOMEM12/*Outofmemory*/
上面了解了堆栈大小,下面就来了解如何使用
函数名pthread_attr_setstacksize。
先看下:
功能重新设置堆栈大小
头文件#include
它的原型intpthread_attr_setstacksize(pthread_attr_t*attr,size_tstacksize);
参数:
attr是线程属性变量