Linux多线程编程

上一篇 / 下一篇  2012-08-13 10:53:31 / 个人分类:Linux

51Testing软件测试网9F5Hft1KY*_m8M1k8i

  线程

P-b[Tx)]u,@0

vq!~Qn\!ub? Cd0  线程是计算机中独立运行的最小单位,运行时占用很少的系统资源。可以把线程看成是操作系统分配CPU时间的基本单元。一个进程可以拥有一个至多个线程。它线程在进程内部共享地址空间、打开的文件描述符等资源。同时线程也有其私有的数据信息,包括:线程号、寄存器(程序计数器和堆栈指针)、堆栈、信号掩码、优先级、线程私有存储空间。

6H-v7U+j? ]dA051Testing软件测试网(X4~0OF2no/mNt$r3z

  为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?

A db.H!}Z)\.mm0

&L*@ N%qHcO0  使用多线程的理由之一是和进程相比,它是一种非常“节俭”的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种“昂贵”的多任务工作方 式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而 且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统 上,这个数据可能会有较大的区别。51Testing软件测试网2}2J |/`-c'lx*g

51Testing软件测试网/yJp"Eiw

  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据 的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它 线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可 能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。51Testing软件测试网 c6e8Bp,I_s['_

51Testing软件测试网'V,m O8ld z4m9xnwV

  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:51Testing软件测试网Qo"_Z+e'q:Y.z6\t-X

7`m5pKI ^ ^ |0  1)提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。51Testing软件测试网.e,}@Uta

51Testing软件测试网 w,^ qrg|P

  2)使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

W#@ c:g!wB,~0

~#KJ)|q0  3)改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

G I/hj*D051Testing软件测试网/j ~-vP2UI

  线程分类51Testing软件测试网Wwf+iVA

:~1j9U&Q.d0B|0G0  线程按照其调度者可以分为用户级线程和核心级线程两种。51Testing软件测试网{ ?gxs

2^3rNgGl7~4~D0  (1)用户级线程51Testing软件测试网/k!Yp+V a,r-I

k ?PZ vC6| ?Q0   用户级线程主要解决的是上下文切换的问题,它的调度算法和调度过程全部由用户自行选择决定,在运行时不需要特定的内核支持。在这里,操作系统往往会提供 一个用户空间的线程库,该线程库提供了线程的创建、调度、撤销等功能,而内核仍然仅对进程进行管理。如果一个进程中的某一个线程调用了一个阻塞的系统调 用,那么该进程包括该进程中的其他所有线程也同时被阻塞。这种用户级线程的主要缺点是在一个进程中的多个线程的调度中无法发挥多处理器的优势。

7@xk.dPj-X v5{(r8@/F0

Pw)g#n~\0  (2)核心级线程

cd/FZZ051Testing软件测试网&W-\L hB r

  这种线程允许不同进程中的线程按照同一相对优先调度方法进行调度,这样就可以发挥多处理器的并发优势。

2m(p]]U ^+Y ^;sc051Testing软件测试网Mk c/s6y,H @9{9s

  现在大多数系统都采用用户级线程与核心级线程并存的方法。一个用户级线程可以对应一个或几个核心级线程,也就是“一对一”或“多对一”模型。这样既可满足多处理机系统的需要,也可以最大限度地减少调度开销。51Testing软件测试网 @#xJ nb/[

0e`a0B:N$?9[mc0  线程创建的Linux实现

J3v\l*d3f&AYu/B0

9m{J}Q0   我们知道,Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用clone()和 fork(),最终都用不同的参数调用do_fork()核内API。当然,要想实现线程,没有核心对多进程(其实是轻量级进程)共享数据段的支持是不行 的,因此,do_fork()提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、 CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号 进程有效)。当使用fork系统调用时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境,而使用 pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从 而创建的“进程”拥有共享的运行环境,只有栈是独立的,由__clone()传入。

h)r eBe+B(p1v*_&y v"?0

n S{ ~jl5Y0  Linux线程在核内是以轻量级进程的形式存在的, 拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager(),每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号 (比如Cancel),而主线程(pthread_create())的调用者则通过管道将请求信息传给管理线程。51Testing软件测试网 G O Dv v ^

,a6d]V],c v(M$D%~0  多线程编程51Testing软件测试网Y&GHaUS S

51Testing软件测试网7}'sL;H tB a$R Y

  1、线程的创建和退出51Testing软件测试网'f8o Z8]y\Fnd

51Testing软件测试网5m6d1R*~%O

  pthread_create 线程创建函数

IS-a6l&g {^,I051Testing软件测试网Yh8W7@8o n7J.D/R(E

  int pthread_create (pthread_t * thread_id,__const pthread_attr_t * __attr,void *(*__start_routine) (void *),void *__restrict __arg);

y6pI P"G/iJD0

/QP6o~)v_'c0  线程创建函数第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的 参数。这里,我们的函数thread 不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。当创建线程成功时,函数返回0,若不为0 则说明创建线程失败,常见的错误返回代码为EAGAIN 和EINVAL。前者表示系统限制创建新的线程,例如线程数目过多了;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数 三和参数四确定的函数,原来的线程则继续运行下一行代码。51Testing软件测试网&g }y^+RZtE1M

7kXb7oRzR0  pthread_join 函数,来等待一个线程的结束。

}OBuRP0

-Z;h{ p-U)\0  函数原型为:int pthread_join (pthread_t __th, void **__thread_return)51Testing软件测试网.jq ^6]gm

51Testing软件测试网ur"i#yN2{I0H

  第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它 的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。线程只能被一个线程等待终止,并且应处于joinable状态(非 detached)。

:Be~C;w&p051Testing软件测试网-{,csG+~ P

  pthread_exit 函数51Testing软件测试网+H#o)_ `3{%X Ri!v-kT

Z;lV)l.pT w'n0  一个线程的结束有两种途径,一种是线程运行的函数结束了,调用它的线程也就结束了;51Testing软件测试网tE \FD Q;b})O"iX

51Testing软件测试网 CdEg9Kude

  另一种方式是通过函数pthread_exit 来实现。它的函数原型为:void pthread_exit (void *__retval)唯一的参数是函数的返回代码,只要pthread_join 中的第二个参数thread_return 不是NULL,这个值将被传递给thread_return。最后要说明的是,一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调 用pthread_join 的线程则返回错误代码ESRCH。51Testing软件测试网%w#B#]\t)Y

+VY@X7t:R#w0  2、线程属性

b}P!Go0

,Th G Aq;b g(E ~0  pthread_create函数的第二个参数线程的属性。将该值设为NULL,也就是采用默认属性,线程的多项属性都是可以更改的。这些属性 主要包括绑定属性、分离属性、堆栈地址、堆栈大小、优先级。其中系统默认的属性为非绑定、非分离、缺省1M 的堆栈、与父进程同样级别的优先级。下面首先对绑定属性和分离属性的基本概念进行讲解。51Testing软件测试网5D3o5V$Z+_

51Testing软件测试网&p)M2z}mA

  绑定属性:Linux中采用“一对一”的线程机制,也就是一个用户线程对应一个内核线程。绑定属性就是指一个用户线程固定地分配给一个内核线 程,因为CPU时间片的调度是面向内核线程 (也就是轻量级进程)的,因此具有绑定属性的线程可以保证在需要的时候总有一个内核线程与之对应。而与之相对的非绑定属性就是指用户线程和内核线程的关系 不是始终固定的,而是由系统来控制分配的。

7vL6}?(w3Tc051Testing软件测试网Y_1v1w'Y DT!Rmj

  分离属性:分离属性是用来决定一个线程以什么样的方式来终止自己。在非分离情况下,当一个线程结束时,它所占用的系统资源并没有被释放,也就是 没有真正的终止。只有当pthread_join()函数返回时,创建的线程才能释放自己占用的系统资源。而在分离属性情况下,一个线程结束时立即释放它 所占有的系统资源。

qV6P5VS([-o3o+E6h2C051Testing软件测试网|#by1`(CW5}R

  这里要注意的一点是,如果设置一个线程的分离属性,而这个线程运行又非常快,那么它很可能在pthread_create 函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这时调用pthread_create 的线程就得到了错误的线程号。

2n%x0sec*Vu0

]W-XG"j p };vy0  设置绑定属性:

+a0j-f.]Y:}S0

W8S)p2]FUcPT6t6[0  int pthread_attr_init(pthread_attr_t *attr)51Testing软件测试网!Rayk%_ a'QT.C m
  int pthread_attr_setscope(pthread_attr_t *attr, int scope)51Testing软件测试网5{$G$et Y_
  int pthread_attr_getscope(pthread_attr_t *tattr, int *scope)

{ p'C8^-z o\\0

l-P+FbR]0  scope:PTHREAD_SCOPE_SYSTEM:绑定,此线程与系统中所有的线程竞争

3OAAxPbl051Testing软件测试网 yD}q ` J"g,DG.J S

  PTHREAD_SCOPE_PROCESS:非绑定,此线程与进程中的其他线程竞争

}4Ln/oe/P`,m051Testing软件测试网2p5OH.i-t:I

  设置分离属性:51Testing软件测试网n:eZ.hJ^`"C

{7kw [5yS0  int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)
OK8[7Ga0  int pthread_attr_getdetachstate(const pthread_attr_t *tattr,int *detachstate)
51Testing软件测试网 s-C#_.OsS

51Testing软件测试网$~,gYVU i#LL-E

  detachstate PTHREAD_CREATE_DETACHED:分离51Testing软件测试网(T;e/MI%ZI0e^

0{0Fft8j7P/BZ0l0  PTHREAD _CREATE_JOINABLE:非分离51Testing软件测试网 c%v4~ ^s|C(T?"?

z+ya0S%\X$d$v0  设置调度策略:51Testing软件测试网G u/TH$j9r1L~+V

51Testing软件测试网(D M8G^.m)];Lx

  int pthread_attr_setschedpolicy(pthread_attr_t * tattr, int policy)
s Q|2P)oO/OH0  int pthread_attr_getschedpolicy(pthread_attr_t * tattr, int *policy)
51Testing软件测试网!y-K-AB9T)RN

gy U"Jz7I.C~G0  policy SCHED_FIFO:先入先出

hHW;H I9Lc051Testing软件测试网2DM \0~E |

  SCHED_RR:循环

"n.t]zt:Q(M'U%y051Testing软件测试网5l.H8Y#OB2Q2}k1V,p

  SCHED_OTHER:实现定义的方法51Testing软件测试网'Y$u `9Oz:?S-^(S)xW;d

51Testing软件测试网/OCUc$]!\(Z!\4^

  设置优先级:

6g$GQFD6i|?!`051Testing软件测试网-s*w I%x`z0P

  int pthread_attr_setschedparam (pthread_attr_t *attr, struct sched_param *param)
@ari/]0  int pthread_attr_getschedparam (pthread_attr_t *attr, struct sched_param *param)
51Testing软件测试网a,~7tL2O5S \4s0gY

5LK.q7HA0  3、线程访问控制

$Ki XX#R;| O0

D? [c;j H+b0  1)互斥锁(mutex)

Qi{u#C051Testing软件测试网$l\J.h.B$o-\

  通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。51Testing软件测试网M"]'HR6hT"a8~0jF

D{r,T:\051Testing软件测试网fD/hLNk(C

int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);51Testing软件测试网)Qc F:NC,@AY!ox
int pthread_mutex_lock(pthread_mutex_t *mutex);
W-Q;\VRe]XY1d9ATb0int pthread_mutex_unlock(pthread_mutex_t *mutex);
,e8t5}}'G(^ h2?&W0int pthread_mutex_destroy(pthread_mutex_t *mutex);
51Testing软件测试网0vRE|#WA8C`4e4Z~ }

  (1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER

kC E3s9UNH3f/J0

d/Q){ RQb t0  (2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY

d+N0bTu(_R D.[b051Testing软件测试网3T"q?#dD;v` [,D

  (3)解锁,unlock需满足是加锁状态,且由加锁线程解锁51Testing软件测试网!X*}.VvQTW@

yqh*FfU(Zy0  (4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY)

|L+J;q+Q4_051Testing软件测试网0C {]*H$T([1PL;w;?

  mutex 分为递归(recursive) 和非递归(non-recursive)两种,这是POSIX 的叫法,另外的名字是可重入(Reentrant) 与非可重入。这两种mutex 作为线程间(inter-thread) 的同步工具时没有区别,它们的惟一区别在于:同一个线程可以重复对recursive mutex 加锁,但是不能重复对non-recursive mutex 加锁。51Testing软件测试网q#?]b*ZN.k8S5F

51Testing软件测试网9V~.{ Y"w&C

  首选非递归mutex,绝对不是为了性能,而是为了体现设计意图。non-recursive 和recursive 的性能差别其实不大,因为少用一个计数器,前者略快一点点而已。在同一个线程里多次对non-recursive mutex 加锁会立刻导致死锁,我认为这是它的优点,能帮助我们思考代码对锁的期求,并且及早(在编码阶段)发现问题。毫无疑问recursive mutex 使用起来要方便一些,因为不用考虑一个线程会自己把自己给锁死了,我猜这也是Java 和Windows 默认提供recursive mutex 的原因。(Java 语言自带的intrinsic lock 是可重入的,它的concurrent 库里提供ReentrantLock,Windows的CRITICAL_SECTION 也是可重入的。似乎它们都不提供轻量级的non-recursive mutex。)51Testing软件测试网*Dd7jmhw3v


TAG:

 

评分:0

我来说两句

Open Toolbar