UNIX/LINUX 多线程编程 |
|
1. 线程
线程通常叫做轻型的进程。虽然这个叫法有些简单化,但这有利于了解线程的概念。 因为线程和进程比起来很小,所以相对来说,线程花费更少的CPU资源。进程往往需要它们自己的资源, 但线程之间可以共享资源,所以线程更加节省内存。Mach的线程使得程序员可以编写并发运行的程序, 而这些程序既可以运行在单处理器的机器上,也可以运行在多处理器的机器中。另外,在单处理器环境中, 当应用程序执行容易引起阻塞和延迟的操作时,线程可以提高效率。
使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,
启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段, 这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间, 共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且, 线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一 个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。
使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,
要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然, 由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。 当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static 的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。
除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作, 此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程, 可以避免这种尴尬的情况。 2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。 3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分, 这样的程序会利于理解和修改。
2. 线程编程介绍
用子函数pthread_create创建一个新的线程。它有四个参数:一个用来保存线程的线程变量、一个线程属性、 当线程执行时要调用的函数和一个此函数的参数。 例如: pthread_ta_thread ; pthread_attr_ta_thread_attribute ; void thread_function(void *argument); char * some_argument; pthread_create( &a_thread, a_thread_attribute, (void *)&thread_function, (void *) &some_argument); 线程属性只指明了需要使用的最小的堆栈大小。在以后的程序中,线程的属性可以指定其他的值, 但现在大部分的程序可以使用缺省值。不像UNIX系统中使用fork系统调用创建的进程,它们和它们的父进程使用同一个执行点, 线程使用在pthread_create中的参数指明要开始执行的函数。
现在我们可以编制第一个程序了。我们编制一个多线程的应用程序,在标准输出中打印“Hello Wo r l d”。
首先我们需要两个线程变量,一个新线程开始执行时可以调用的函数。我们还需要指明每一个线程应该打印的信息。 一个做法是把要打印的字符串分开,给每一个线程一个字符串作为开始的参数。 请看下面的代码: void print_message_function( void *ptr ); main( ) { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "Wo r l d " ; pthread_create( &thread1, pthread_attr_default, (void*)&print_message_function, (void*) message1); pthread_create(&thread2, pthread_attr_default, (void*)&print_message_function, (void*) message2); exit( 0 ) ; } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s ", message); }
程序通过调用pthread_create创建第一个线程,并将“Hello”作为它的启动参数。第二个线程的参数是“World”。
当第一个线程开始执行时,它使用参数“Hello”执行函数print_message_function。它在标准输出中打印“Hello”, 然后结束对函数的调用。线程当离开它的初始化函数时就将终止,所以第一个线程在打印完“Hello”后终止。 当第二个线程执行时,它打印“World”然后终止。但这个程序有两个主要的缺陷。 首先也是最重要的是线程是同时执行的。这样就无法保证第一个线程先执行打印语句。所以你很可能在屏幕上看到“World Hello”, 而不是“Hello World”。请注意对exit的调用是父线程在主程序中使用的。这样,如果父线程在两个子线程调用打印语句 之前调用exit,那么将不会有任何的打印输出。这是因为exit函数将会退出进程,同时释放任务,所以结束了所有的线程。 任何线程(不论是父线程或者子线程)调用exit 都会终止所有其他线程。如果希望线程分别终止,可以使用pthread_exit函数。 我们可以使用一个办法弥补此缺陷。我们可以在父线程中插入一个延迟程序,给子线程足够的时间完成打印的调用。同样, 在调用第二个之前也插入一个延迟程序保证第一个线程在第二个线程执行之前完成任务。
void print_message_function( void *ptr );
main ( ) { pthread_t thread1, thread2; char *message1 = "Hello”; char *message2 = "Wo r l d " ; pthread_create( &thread1, pthread_attr_default, (void *) &print_message_function, (void *) message1); sleep (10) ; pthread_create(&thread2, pthread_attr_default, (void *) &print_message_function, (void *) message2); sleep ( 10 ) ; exit (0) ; } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s", message); pthread_exit(0) ; }
这样是否达到了我们的要求了呢?不尽如此,因为依靠时间的延迟执行同步是不可靠的。这里遇到的情形和一个分布程序和
共享资源的情形一样。共享的资源是标准的输出设备,分布计算的程序是三个线程。 其实这里还有另外一个错误。函数sleep和函数e x i t一样和进程有关。当线程调用sleep时, 整个的进程都处于睡眠状态,也就是说,所有的三个线程都进入睡眠状态。这样我们实际上没有解决任何的问题。 希望使一个线程睡眠的函数是pthread_delay_np。例如让一个线程睡眠2秒钟,用如下程序:
struct timespec delay;
delay.tv_sec = 2; delay.tv_nsec = 0; pthread_delay_np( &delay ); }
3. 线程同步
POSIX提供两种线程同步的方法,mutex和条件变量。mutex是一种简单的加锁的方法来控制对共享资源的存取。 我们可以创建一个读/写程序,它们共用一个共享缓冲区,使用mutex来控制对缓冲区的存取。 void reader_function(void); void writer_function(void); char buf f e r ; int buffer_has_item = 0; pthread_mutex_t mutex; struct timespec delay; main( ) { pthread_t reader; delay.tv_sec = 2; delay.tv_nsec = 0; pthread_mutex_init(&mutex, pthread_mutexattr_default); pthread_create( &reader, pthread_attr_default, (void*)&reader_function, N U L L ) ; writer_function( ) void writer_function(void) { while( 1 ) { pthread_mutex_lock( &mutex ); if ( buffer_has_item == 0 ) { buffer = make_new_item(); buffer_has_item = 1; } pthread_mutex_unlock( &mutex ); pthread_delay_np( &delay ); } } void reader_function(void) { while( 1 ) { pthread_mutex_lock( &mutex ); if ( buffer_has_item == 1) { consume_item( buffer ); buffer_has_item = 0; } pthread_mutex_unlock( &mutex ); pthread_delay_np( &delay ); } } 在上面的程序中,我们假定缓冲区只能保存一条信息,这样缓冲区只有两个状态,有一条信息或者没有信息。 使用延迟是为了避免一个线程永远占有mutex。 但mutex的缺点在于它只有两个状态,锁定和非锁定。POSIX的条件变量通过允许线程阻塞和等待另一个线程的信号方法, 从而弥补了mutex的不足。当接受到一个信号时,阻塞线程将会被唤起,并试图获得相关的mutex的锁。
4. 信号量
信号量是一个可以用来控制多个进程存取共享资源的计数器。它经常作为一种锁定机制来防止当一个进程正在存取共享资源时, 另一个进程也存取同一资源。这里我会讲的很详细,我可是花了很多时间去找这资料呀!! 下面先介绍一下信号量中涉及到的数据结构。 1.内核中的数据结构semid_ds 和消息队列一样,系统内核为内核地址空间中的每一个信号量集都保存了一个内部的数据结构。数据结构的原型是semid_ds。 它是在linux/sem.h中做如下定义的: /*One semid data structure for each set of semaphores in the system.*/ structsemid_ds{ structipc_permsem_perm;/*permissions..seeipc.h*/ time_tsem_otime;/*last semop time*/ time_tsem_ctime;/*last change time*/ structsem*sem_base;/*ptr to first semaphore in array*/ structwait_queue*eventn; structwait_queue*eventz; structsem_undo*undo;/*undo requestson this array*/ ushortsem_nsems;/*no. of semaphores in array*/ }; sem_perm是在linux/ipc.h定义的数据结构ipc_perm的一个实例。它保存有信号量集的存取权限的信息, 以及信号量集创建者的有关信息。 sem_otime最后一次semop()操作的时间。 sem_ctime最后一次改动此数据结构的时间。 sem_base指向数组中第一个信号量的指针。 sem_undo数组中没有完成的请求的个数。 sem_nsems信号量集(数组)中的信号量的个数。 2.内核中的数据结构sem 在数据结构semid_ds中包含一个指向信号量数组的指针。此数组中的每一个元素都是一个 数据结构sem。它也是在linux/sem.h中定义的: /*One semaphore structure for each semaphore in the system.*/ structsem{ shortsempid;/*pid of las toperation*/ ushortsemval;/*current value*/ ushortsemncnt;/*num procs awaiting increase in semval*/ ushortsemzcnt;/*num procs awaiting semval=0*/ }; sem_pid最后一个操作的PID(进程ID)。 sem_semval信号量的当前值。 sem_semncnt等待资源的进程数目。 sem_semzcnt等待资源完全空闲的进程数目。
以上说的就是我们在程序经常用到的 (P/V)操作了
P 操作( 代表荷兰语 proberen 意思是尝试) : 等待(wait)一个信号灯,该操作测试这个信号灯的值, 如果小于0||等于0, 那就等待(阻塞), 一但值变大就将它减些 1. V 操作(代表荷兰语 verhogen 意思是增加): 挂出(post)一个信号灯, 该操作将信号灯的值加 1 . 关于P/V 操作先介绍到这了,下面在介绍信号量的一些方法: semget() 我们可以使用系统调用semget()创建一个新的信号量集,或者存取一个已经存在的信号量集: 系统调用:semget(); 原型:intsemget(key_t key,int nsems,int semflg); 返回值:如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1:errno=EACCESS(没有权限) EEXIST(信号量集已经存在,无法创建) EIDRM(信号量集已经删除) ENOENT(信号量集不存在,同时没有使用IPC_CREAT) ENOMEM(没有足够的内存创建新的信号量集) ENOSPC(超出限制)
系统调用semget()的第一个参数是关键字值(一般是由系统调用ftok()返回的)。
系统内核将此值和系统中存在的其他的信号量集的关键字值进行比较。打开和存取操作与参数semflg中的内容相关。 IPC_CREAT如果信号量集在系统内核中不存在,则创建信号量集。IPC_EXCL当和IPC_CREAT一同使用时, 如果信号量集已经存在,则调用失败。如果单独使用IPC_CREAT,则semget()要么返回新创建的信号量集的标识符, 要么返回系统中已经存在的同样的关键字值的信号量的标识符。如果IPC_EXCL和IPC_CREAT一同使用, 则要么返回新创建的信号量集的标识符,要么返回-1。IPC_EXCL单独使用没有意义。 参数nsems指出了一个新的信号量集中应该创建的信号量的个数。 信号量集中最多的信号量的个数是在linux/sem.h中定义的: #defineSEMMSL32/*<=512maxnumofsemaphoresperid*/ 下面是一个打开和创建信号量集的程序: intopen_semaphore_set(key_t keyval,int numsems) { intsid; if(!numsems) return(-1); if((sid=semget(mykey,numsems,IPC_CREAT|0660))==-1) { return(-1); } return(sid); } };
semop()
系统调用:semop(); 调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops); 返回值:0,如果成功。-1,如果失败:errno=E2BIG(nsops大于最大的ops数目) EACCESS(权限不够) EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行) EFAULT(sops指向的地址无效) EIDRM(信号量集已经删除) EINTR(当睡眠时接收到其他信号) EINVAL(信号量集不存在,或者semid无效) ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构) ERANGE(信号量值超出范围) 第一个参数是关键字值。第二个参数是指向将要操作的数组的指针。第三个参数是数组中的操作的个数。 参数sops指向由sembuf组成的数组。此数组是在linux/sem.h中定义的: /*semop systemcall takes an array of these*/ structsembuf{ ushortsem_num;/*semaphore index in array*/ shortsem_op;/*semaphore operation*/ shortsem_flg;/*operation flags*/ sem_num将要处理的信号量的个数。 sem_op要执行的操作。 sem_flg操作标志。
如果sem_op是负数,那么信号量将减去它的值。这和信号量控制的资源有关。如果没有使用IPC_NOWAIT,
那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。如果sem_op是正数,则信号量加上它的值。 这也就是进程释放信号量控制的资源。最后,如果sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。 这在一个进程等待完全空闲的资源时使用。
semctl()
系统调用:semctl(); 原型:int semctl(int semid,int semnum,int cmd,union semunarg); 返回值:如果成功,则为一个正数。 如果失败,则为-1:errno=EACCESS(权限不够) EFAULT(arg指向的地址无效) EIDRM(信号量集已经删除) EINVAL(信号量集不存在,或者semid无效) EPERM(EUID没有cmd的权利) ERANGE(信号量值超出范围) 系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。 但这两个系统调用的参数略有不同。因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。 所以在信号量集的操作中,不但要知道IPC关键字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd, 它用来指出要操作的具体命令。两个系统调用中的最后一个参数也不一样。在系统调用msgctl中, 最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息, 以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。 系统调用semctl()的第一个参数是关键字值。第二个参数是信号量数目。
参数cmd中可以使用的命令如下:
·IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。 ·IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。 ·IPC_RMID将信号量集从内存中删除。 ·GETALL用于读取信号量集中的所有信号量的值。 ·GETNCNT返回正在等待资源的进程数目。 ·GETPID返回最后一个执行semop操作的进程的PID。 ·GETVAL返回信号量集中的一个单个的信号量的值。 ·GETZCNT返回这在等待完全空闲的资源的进程数目。 ·SETALL设置信号量集中的所有的信号量的值。 ·SETVAL设置信号量集中的一个单独的信号量的值。
参数arg代表一个semun的实例。semun是在linux/sem.h中定义的:
/*arg for semctl systemcalls.*/ unionsemun{ intval;/*value for SETVAL*/ structsemid_ds*buf;/*buffer for IPC_STAT&IPC_SET*/ ushort*array;/*array for GETALL&SETALL*/ structseminfo*__buf;/*buffer for IPC_INFO*/ void*__pad; val当执行SETVAL命令时使用。buf在IPC_STAT/IPC_SET命令中使用。代表了内核中使用的信号量的数据结构。 array在使用GETALL/SETALL命令时使用的指针。 下面的程序返回信号量的值。当使用GETVAL命令时,调用中的最后一个参数被忽略: intget_sem_val(intsid,intsemnum) { return(semctl(sid,semnum,GETVAL,0)); } 下面是一个实际应用的例子: #defineMAX_PRINTERS5 printer_usage() { int x; for(x=0;x<MAX_PRINTERS;x++) printf("Printer%d:%d/n/r",x,get_sem_val(sid,x)); } 下面的程序可以用来初始化一个新的信号量值: void init_semaphore(int sid,int semnum,int initval) { union semunsemopts; semopts.val=initval; semctl(sid,semnum,SETVAL,semopts); }
注意系统调用semctl中的最后一个参数是一个联合类型的副本,而不是一个指向联合类型的指针。
|