1. 首页
  2. 编程语言
  3. C
  4. Linux下的C语言多线程编程

Linux下的C语言多线程编程

上传者: 2018-12-25 20:29:15上传 PDF文件 817.38KB 热度 48次
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空 间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址 空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。This is a pthreadThis is the main processThis is a pthread.This is the main processThis is a pthreadThis is the main process前后两次结果不一样,这是两个线程争夺CPU资源的结果。上面的示例中,我们使用到了两个函数, pthread create和 pthread join,并声明了个 pthread t型的变量。pthread U在头文件/usr/ include/biLs/ pChreadtypes.h中定义:typedef unsigned long int pthread t:它是一个线程的标识符。函数 pthread create用来创建个线程,它的原型为:extern int pthread create P((pthread t *k thread, constpthread attr t *k attr,Void*(半 start routine)(void米),void*arg);第一个参数为指向线程标识符的指针,第二个参数用来设置线程属性,第三个参数是线程运行函数的起始地址,最后一个参数是运行函数的参数。这里,我们的函数 thread不需要参数,所以最后一个参数设为空指针。第二个参数我们也设为空指针,这样将生成默认属性的线程。对线程属性的设定和修改我们将在下一节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为 EAGAIN和 EINVAL。前者表示系统限制创建新的线程,例如线程数日过多」;后者表示第二个参数代表的线程属性值非法。创建线程成功后,新创建的线程则运行参数三和参数四确定的函数,原来的线程则继续运行下一行代码。函数 pthread join用来等待一个线程的结束。函数原型为extern int pthread join p((pthread t th, void**k thread return))第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。一个线程的结束有两种途径,一种是象我们上面的例子一样,函数结東了,调用它的线程也就结束了;另一种方式是通过函数 pthread exit来实现。它的响数原型为extern void pthread exit P((void s retval)) attribute( noreturn )唯一的参数是函数的返回代码,只要 pthread join中的第二个参数thread return不是NULL,这个值将被传递给 thread return。最后要说明的是个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用 pthread join的线程则返回错误代码 ESRCH。程序员最可信赖的求职帮手在这一节里,我们编写了一个最简单的线程,并掌握∫最常用的三个函数 pthread create, pthread join和 pthread exit。下面,我们来了解线程的回成忌回些常用属性以及如何设置这些属性。修改线程的属性程序员最可信赖的求职帮手在上一节的例子里,我们用 pthread create函数创建了一个线程,在这个线程中,我们使用了默认参数,即将该函数的第二个参数设为NULL。的确,对大多数程序来说,使用默认属性就够了∫,但我们还是有必要来了解一下线程的有关属性。属性结构为 pthread attr t,它同样在头文件/usr/ include/ pthread.h中定义,喜欢追根问底的人可以自己去查看。属性值不能直接设置,须使川相关函数进行操作,初始化的函数为 pthread attr init,这个函数必须在pthread create函数之前调用。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈大小、优先级。默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。关于线程的绑定,牵涉到另外一个概念:轻进程(LWP: Light WeighTProcess)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制程序員最可信赖的求职帮手个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求设置线程绑定状态的函数为 pthread attr setscope,它有两个参数,第一个是指向属性结构的指针,第二个是绑定类型,它有两个取值PTHREAD SCOPE SYSTEM(绑定的)和 PTHREAD SCOPE PROCESS(非绑定的)。下面的代码即创建了一个绑定的线程。#include pthread. h>pthread attr t attrpthread t tid/*初始化属性值,均设为默认值*/pthread attr init(&attr)pUhread attr selscope(&attr, PTHREAD SCOPE SYSTEM)pthread create(tid, &attr, (void *)my function, NULL)线程的分离状态决定一个线程以什么样的方式来终止自己。在上面的例子中,我们采用了线程的默认属性,即为非分离状态,这种情况下,原有的线程可回等待创建的线程结束。只有当 pthread join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,白己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。设置线程分离状态的函数为pthread attr setdetachstate (pthread attr t *attr, int detachstate)第二个参数可选为 PTHREAD CREATE DETACHED(分离线程)和 PTHREADCREATE JOINABLE(非分离线程)。这里要注意的一点是,如果设置一个线程为分离线程,而这个线程运行又非常快,它很可能在 pthread create函数返回之前就终止了,它终止以后就可能将线程号和系统资源移交给其他的线程使用,这样调用 pthread create的线程就得到了错误的线程号。要避免这种情况可以采取一定的同步措施,最简单的方法之一是可以在被创建的线程里调用pthread cond timewait函数,让这个线程等待一会儿,留出足够的时间让函数pthread create返回。设置段等待吋间,是在多线程编程里常用的方法。但是注意不要使川诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。另外一个可能常用的属性是线程的优先级,它存放在结构 sched param中用函数 pthread attr getschedparam和函数 pthread attr setschedparam进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。下面即是一段简单的例子。#include pthread. h>#include pthread attr t attrpthread t tidsched param paramint newprio=20pthread attr inil(&atirpthread attr getschedparam(&attr, ¶m)param sched priority=newpriopthread attr setschedparam(&attr, ¶m)pthread create(&tid, &attr, (void *)myfunction, myarg线程的数据处理和进程相比,线程的最大优点之一是数据的共享性,各个进程共享父进程处沿袭的数据段,可以方便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当心有多个不同的进程访问相同的变量。许多函数是不可重入的,即同时不能运行一个函数的多个拷贝(除非使用不同的数据段)。在函数中声明的静态变量常常带来问题,κ数的返回值也会有问题。因为如果返回的是函数内部静态声明的空问的地址,则在一个线程调用该函数得到地址后使用该地址指向的数据时,别的线程可能调用此函数并修改了这一段数据。在进程中共享的变量必须用关键字 volatile来定义,这是为了防止编译器在优化时(如gcc中使用一0X参数)改变它们的使用方式。为了保护变量,我们必须使用信号量、互斥等方法来保证我们对变量的正确使用。下面,我们就逐步介绍处理线程数据时的有关知识。4.1线程数据在单线程的程序里,有两种基本的数据:全局变量和局部变量。但在多线程程序里,还有第三种数据类型:线程数据(TSD: Thread- Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使用全局变量一样调用它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显而易见的。例如我们常见的变量erro,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,亡和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。和线程数据相关的函数主要有4个:创建一个键;为一个键指定线程数回器品口据;从一个键读取线程数据;删除键。创建键的函数原型为:extern int pthread key create P ( pthread key t x keyvoid( destr function)(void *)))程序员最可信赖的求职帮手第一个参数为指向一个键值的指针,第二个参数指明了一个 destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。这个函数常和函数 pthread once(φ pthread once t*once control,void(* in1 routine)(void)一起使用,为了让这个键只被创建一次。函数 pthread once声明一个初始化函数,第一次调用 pthread once时它执行这个函数,以后的调用将被它忽略在下由的例子中,我们创建一个键,并将它和某个数据相关联。我们要定义一个函数 createWindow,这个函数定义一个图形窗口(数据类型为FI Window*,这是图形界面开发工具FLTK中的数据类型)。由于各个线程都会调用这个函数,所以我们使用线程数据。/声明一个键pthread key t myWinKey水函数 createWindow*/void createWindow void)(F1 Window米winstatic pthread once t once= PTHREAD ONCE INIT*调用函数 createMy Key,创建键*pthread once(& once, createMyKey)/*win指向一个新建立的窗口米win=new Fl Window(0, 0, 100, 100,"MyWindow")/*对此窗凵作一些可能的设置工作,如大小、位置、名称等*/setWindow(win)/*将窗口指针值绑定在键 my WinKey上*/pthread setpecific( my WinKey, win)/*函数 createMyKey,创建一个键,并指定了 destructor*/void createMyKey voidpthread keycreate(&my WinKey, freeWinKey*函数 freeWinKey,释放空间*/void freeWinKey( Fl Window *k win)[delete win这样,在不同的线程中调用函数 createMy Win,都可以得到在线程内部均可见的窗凵变量,这个变量通过函数 pthread getspecific得到。在上面的例子中,我们已经使用了函数 pthread setspecific来将线程数据和一个键绑定在一起。这两个函数的原型如下extern int pthread setspeciric P((pthread key t key, constvoid pointer)extern void *pthread getspecific p ((pthread key t key))这两个函数的参数意义和使用方法是显而易见的。要注意的是,用pthread set specific为一个键指定新的线程数据时,必须自己释放原有的线程数据以回收空间。这个过程函数 pthread key delete用来删除个键,这个键占用的内存将被释放,但同样要注意的是,它只释放键占川的内存,并不释敚该键关联的线程数据所占用的内存资源,而且它也不会触发函数pthread key create中定义的 destructor凶数。线程数据的释放必须在释放键之前完成。4.2互斥锁互斥锁用来保证一段时间内只有一个线程在执行段代码。必要性显而易见:假设各个线程向同一个文件顺序写入教据,最后得到的结果一定是灾难性的我们先看下面段代码。这是个读/写程序,它们公用一个缓冲区,并且我们假定一个缓冲区只能保存一条信息。即缓冲区只有两个状态:有信息或没有信息。void reader function( voidvoid writer function( voidchar bufferint buffer has item=0pthread mutex t mutexstruct timespec delayvoid main( void )tpthread t reader/*定义延迟时间*delay. tv sec-2delay. tv nec =0/*用默认属性初始化一个互斥锁对象*pthread mutex init (&mutex, NULL)pthread create(&reader, pthread attr default, (void*&reader function), NULLwriter function()void writer function (void)iwhile(1)/*锁定互斥锁*pthread mutex lock( &mutexif(buffer has item==0)(buffer-make new item()buffer has item=1打开互斥锁*pthread mutex unlock(&mutex)pthread delay np ( &delay口回void reader function (void)iwhile(1)ipthread mutex lock(&mutexif(buffer has item==1)[百法校Lconsume i temm (buffer)程序员最可信赖的求职帮手buffer has item=0pthread mutex unlock(&mutexpthread delay np( &delay)这里声明了互斥锁变量 mutex,结构 pthread mutex t为不公开的数据类型,其中包含一个系统分配的属性对象。函数 pthread mutex init用来生成一个互斥锁。NULL参数表明使用默认属性。如果需要声明特定属性的互斥锁,须调用涿数 pthread mutexattr init。函数 pthread mutexattr setpshared和函数 pthread mutexattr settype用来设置互斥锁属性。前一个函数设置属性 shared,它有两个取值, PTHREAD PROCESS PRIVATE和PTHREAD PROCESS SHARED。前者用来不同进程中的线程同步,后者用于同步本进程的不同线程。在上面的例子中,我们使用的是默认属性 PTHREAD PROCESSPRIVATE。后者用来设置互斥锁类型,可选的类型有 PTHREAD MUTEX ORMAL、PTHREAD MUTEX ERRORCHECK PTHREAD MUTEX RECURSIVE FL PTHREADMUTEⅩ DEFAULT。它们分别定义了不同的上所、解锁机制,般情况下,选用最后一个默认属性。pthread mutex lock声明开始用互斥锁上锁,此后的代码直至调用pthread mutex unlock为止,均被上锁,即同吋间只能被一个线程调用执行。当一个线程执行到 pthread mutex lock处时,如果该锁此时被另一个线程使用,那此线程被阻塞,即程序将等待到另一个线程释放此互斥锁。在上面的例子中,我们使用了 pthread delay np函数,让线程睡眠一段时间,就是为了防止一个线程始终据此函数。上面的例子非常简单,就不再介绍了,需要提出的是在使用互斥锁的过程中很有可能会出现死锁:两个线程试图同时占用两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这吋就出现了死锁。此吋我们可以使用函数pthread mutex try lock,它是函数 pthread mutex10ck的非阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不一样,但最主要的还是要程序员自口在程序设计注意这一点。4.3条件变量前一节中我们讲述了如何使用互斥锁来实现线程间数据的共亨和通信,互斥锁一个眀显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承问的同步。条件变量的结构为 pthread cond t,函数 pthread cond init()被用来初始化一个条件变量。它的原型为extern int pthread cond init P((pthread cond tcond, const pthread condaltr t *k cond atir))其中cond是一个指向结构 pthread cond t的指针, cond attr是一个指向结构 pthread condattr t的指针。结构 pthread condattr t是条件变量口的属性结构,和互斥锁一样我们可以用它来设置条件变量是进程内可用还是进程可俗的求间可用,默认值是 PTHREAD PROCESS PRIVATE,即此条件变量被同一进程内的各个线程使用。注意初始化条件变量只有未被使用时才能重新初始化或被释放。释放一个条件变量的函数为 pthread cond destroy( pthread cond t cond)。函数 pthread cond wait()使线程阻塞在一个条件变量上。它的函数原型为extern int pthread cond wait P((pChread cond L condthread mutex t米 mutex)线程解开 mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread cond signal和函数 pthread cond broadcast唤醒,但是要注意的是条件变量只是起阻塞和唤醒线程的作用,具体的判断条件还需用户给出,例如个变量是否为0等等,这一点我们从后面的例子中可以看到。线程被唤醒后,它将重新检查判断糸件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。这个过程一般用 while语句实现另一个用来阻塞线程的函数是 pthread cond timedwait(),它的原型为extern int pthread cond timedwait p((pthread cond t* condpthread mutex t* mutex, const struct timespec abstime))它比函数 pthread cond wait()多了一个时间参数,绎历 abstime段时间后,即使条件变量不满足,阻塞也被解除。函数 pthread cond signal()的原型为extern int pthread cond signal P ((pthread cond t cond))它用来释放被阻塞在条件变量cond上的一个线程。多个线程阻塞在此条件变量上时,哪一个线程被唤醒是由线程的调度策略所决定的ε要注意的是,∮须用保护条件变量的互斥锁来保护这个函数,否则条件满足信号又可能在测试条件和调用 pthread cond wait函数之间被发出,从而造成无限制的等待。下面是使用函数 pthread cond wait()和函数 pthread cond signal()的一个简单的例子。pthread mutex t count lockpthread cond t count nonzero;unsigned countdecrement count opthread mutex lock (&count lock)while(count==0)口常pthread cond wait( &count nonzero, &count lock)count=count -1pthread mutex unlock (&count lock)之家increment countypthread mutex lock(&count lock)程序员最可信赖的求职帮手
用户评论