-- 作者:zhu_ruixian
-- 发布时间:10/21/2006 7:02:00 PM
-- 使用pthreads[转帖]
(pThread是什么就不说了,看到这篇文章不错,共享一下吧!) 这几天想看一下pThread,但是本人没什么毅力,常常是想学什么,然后找本书,但是一般看完前言和 第一章就扔了...-_____-|||||。所以这次为了使自己看下去,干脆翻译一下这个Getting Started With POSIX Threads 好了。 1.介绍:什么是线程,用来干什么? 线程常常被叫做轻量级的(lightweight)进程,虽然这个称呼有点过于简单了,但这是一个不错的开始。线程虽然不是UNIX的进程, 但很相似。为了弄懂他们的区别,我们必须看看UNIX进程、Mach任务和线程的关系。在UNIX中,一个进程包括可执行的程序以及许多的资源, 象是文件描述符表和地址空间。在Mach中一个任务只包括资源;线程负责所有的执行活动。一个Mach任务可以有几个线程与之相关联,而且所 有的线程必须与某任务关联。与同一个任务相关联的线程共享该任务的资源。所以本质上一个线程就是一个程序计数器,一个堆栈,和一系列 寄存器--其余所有的数据结构都属于任务。一个在Mach中的UNIX进程被模拟为只有一个线程的任务。 因为相比于进程,线程很小,所以如果用cpu的消耗来衡量,线程的创建相对廉价。因为进程要求有它们自身的资源,而线程共享资源 ,所以线程节省内存。Mach线程赋予程序员开发可同时执行于单cpu和多cpu的机器上并发程序的能力,如果有的话可利用额外的cpu。另外,如 果程序在单cpu环境下易于阻塞或导致迟滞,比如文件操作活套接字操作,线程可以提升性能。 在接下来的部分中,我们会讨论一部分POSIX线程标准和它在DEC OSF/1 OS上的执行细节,V3.0.POSIX线程被称作pthreads并且与非 POSIX的cthreads很相似。 开始使用pthreads(2)Hello World 2.Hello World 既然形式做完了,那我们就开始吧。pthread_create函数创建一个新的线程。它有四个参数,一个线程变量或是线程的持有者,一个 线程的属性,当线程开始执行时调用的函数,一个该函数的参数。比如: pthread_t a_thread; pthread_attr_t a_thread_attribute; void thread_function(void *argument); char *some_argument; pthread_create( &a_thread, a_thread_attribute, (void *)&thread_function, (void *) &some_argument); 一个线程的属性当前只指定了被使用的最小的堆栈大小。以后的线程属性可能会更有趣,但现在大多数的程序只要简单的使用默认的 就行了,即把pthread_attr_default传入函数。与用UNIX的fork命令创建的进程会与父线程同时执行同一条指令不同,线程在指定的函数中开 始它们的执行。理由十分简单;如果线程不在别处开始执行,会得到的结果是许多线程用相同的资源执行同一条指令。回想一下每个进程都有 自己的资源,而线程共享它们。 既然我们知道了如何去创建线程,我们已为我们的第一个程序做好了准备。让我们设计一个在标准输出上 打印出"Hello World"的多线程的程序。首先我们需要两个线程变量和一个被新的线程调用并执行的函数。 我们也必须指定每个线程必须打印出不同的消息。一种方法是把两个单词分成两个不同的字符串并且给每个 线程一个不同的字符串当作它的"startup(启动)"参数。看一下下面的代码: void print_message_function( void *ptr ); main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; 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); } 注意一下print_message_function 的函数原型和在pthread_create 调用中message参数前的转换。这个程序通过调用pthread_create 函数并传递启动参数"Hello"创建第一个线程;第二个线程通过"World"参数来创建。当第一个线程开始执行时它从带着"Hello"参数的 print_message_function开始。它打印出"Hello"然后终止执行。当一个线程离开初始的函数它也终止了,所以第一个线程在打印出"Hello"后 就终止了。当第二个线程开始执行并打印出"World"后也相应的终止了。虽然这个程序看起来很合理,但它有两个缺点。 第一个也是最重要的,线程并发的执行。因此不能保证第一个线程比第二个线程线到达printf函数。所以我们可能看到"World Hello" 而不是"Hello World".还有更微妙的一点。注意被父线程的main函数调用的exit函数。如果父线程在两个子线程调用printf之前调用exit,就不 会有输出被产生。这是因为exit函数退出了这个进程(释放了任务),因此终止了所有线程。任何线程,附线程或子线程,只要有一个调用了 exit函数,所有的这个进程的线程都将终止。如果想明确的终止线程应调用pthread_exit函数。 因此我们的hello world小程序有两个竞争情形。exit调用的竞争和哪个子进程先调用printf的竞争。让我们 用一个不太安全的方法来调整这些竞争。因为我们希望每个子进程在父进程结束前完成执行,让我们 在父进程中插入一个延迟,这样会给子进程时间去执行printf。为了保证第一个子进程比第二个先执行printf ,我们在第二个pthread_create 调用前插入一个延迟。结果代码: void print_message_function( void *ptr ); main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; 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); } 这段代码是否符合我们的目标了呢?并不是。依靠时间延迟来执行同步永远不是安全的。因为线程之间的 紧密耦合而诱使人们用不那么严格的态度去处理同步,但那是应该被避免的。这里的竞争情形与一个分布式的应用程序和一个共享的资源情况 相同。这里资源就是标准输出而分布式的计算单元就是三个线程。线程一必须比线程而先用ptintf/stdout,而且他们必须在父线程退出之前做 。 在我们尝试用延时来同步的另一面,我们又犯了另一个大错。sleep函数与exit函数一样是与整个进程相连 系的。如果一个线程调用了sleep函数,整个进程将休眠,也就是当进程休眠时所有的线程也休眠了。因此这与我们不调用sleep函数的结果一 样,只不过程序多运行了20秒。当想延迟一个线程的合适函数是pthread_delay_np(np代表not portable)。比如,延迟一个线程2秒钟: struct timespec delay; delay.tv_sec = 2; delay.tv_nsec = 0; pthread_delay_np( &delay ); 本节提及的函数: pthread_create(), pthread_exit(), and pthread_delay_np(). 开始使用pthreads(3)线程同步 3.线程的同步 POSIX提供了两个同步的原语,mutex(互斥)和condition(条件)变量。互斥是可以被用来控制共享变量的访问简单的锁原语。注意, 对于线程来说,整个地址空间都是共享的,所以所有的东西都可以被当作共享资源。然而,在大多数情况下,线程使用私有的本地变量(在 pthread_create及连续的函数中制造出来的)单独的工作(理论上),并通过全局变量来把它们的成果合并起来。对于线程都要进行写操作的 变量的访问必须被控制。 让我们创建一个readers/writers程序,在这个程序中有一个reader和一个writer通过一个共享的缓存来通信并且通过互斥来控制访问: void reader_function(void); void writer_function(void); char buffer; 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, NULL); 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 ); } } 在这个简单的程序中我们假设缓存只能容纳一项,所以它一直是处于两种状态的其中一种,有内容或没有。writer首先锁住互斥变量, 如果已经被上锁了,那么该线程阻塞直到被解锁,然后查看缓存是否为空。如果缓存为空,它创建一个新的项并设置标记buffer_has_item,因 此reader会知道现在缓存里有内容。然后解锁互斥变量并延时2秒钟让reader有机会消耗这项内容。这个延时与我们的前一个延时不同,它是用 来改善程序性能的。如果没有这个延时,writer在释放锁之后可能马上又获得了锁并试图再制造另一项内容。reader很可能没有机会这么快的 消耗这项内容,所以延时是一个好办法。 reader的情况也差不多。他获得这个锁,查看是否存在内容,如果有就消耗它。它释放锁并延时一小段时间来给writer机会去制造新 的内容。在这个例子中reader和writer会一直执行下去,制造和消耗内容。如果一个互斥变量不再被需要,可以通过pthread_mutex_destroy (&mutex)来释放。观察在互斥变量的初始化函数中,我们使用被要求的pthread_ mutexattr_default作为互斥变量的属性。在OSF/1中,互斥变 量属性没什么作用,所以强烈推荐使用默认值。 适当的使用户斥变量保证消除了竞争情形。但是,互斥变量本身十分的弱,因为它只有两个状态:被锁和未被锁。POSIX的条件变量通 过允许一个线程阻塞而去等待另一个线程的信号来补充互斥变量。当这个信号被接受到了,被阻塞的信号被唤醒去尝试获得一个与之像关的互 斥变量。因此信号和互斥可以被合并起来消除readers/writers带来的自旋锁问题。我们已经设计了一个通过pthreads的mutex和condition实现 的简单的整数信号量并且今后会在那个环境中讨论同步的问题。信号量的代码可以在附录A中找到,关于条件变量的细节问题 可以在帮助(man)页中找到。 本节提到的函数: pthread_mutex_init(), pthread_mutex_lock(), pthread_mutex_unlock(), and pthread_mutex_destroy(). 开始使用pthreads(4)用信号量协调事件 4.用信号量协调事件 让我们用信号量来重新回顾一下readers/writes程序。我们会用最健壮的整数信号量来替代互斥原语来消除自旋锁的问题。信号量的操作是 semaphore_up, semaphore_down, semaphore_init, semaphore_destroy,和 semaphore_decrement。semaphore_up, semaphore_down遵循传统 的信号量语法-- 如果信号量的值小于或等于0,semaphore_down试线程阻塞,semaphore_up增加信号量的值。semaphore_init必须在使用信号 量前被调用并且所有的信号量都应被初始化为值1。semaphore_destroy函数释放不再使用的信号量。所有的函数使用一个指向信号量实例的参 数。semaphore_decrement是一个非阻塞的函数,它减少信号量的值。它允许线程在初始化过程中把信号量的值减为负的。我们将看一个使用 semaphore_decrement的readers/writers的程序: void reader_function(void); void writer_function(void); char buffer; Semaphore writers_turn; Semaphore readers_turn; main() { pthread_t reader; semaphore_init( &readers_turn ); semaphore_init( &writers_turn ); /* writer must go first */ semaphore_down( &readers_turn ); pthread_create( &reader, pthread_attr_default, (void *)&reader_function, NULL); writer_function(); } void writer_function(void) { while(1) { semaphore_down( &writers_turn ); buffer = make_new_item(); semaphore_up( &readers_turn ); } } void reader_function(void) { while(1) { semaphore_down( &readers_turn ); consume_item( buffer ); semaphore_up( &writers_turn ); } } 这个例子仍然没有完全发挥通用整数信号量的能力。让我们再回顾一下第二节的Hello World程序并把它的竞争情形用整数信号量来消除。 void print_message_function( void *ptr ); Semaphore child_counter; Semaphore worlds_turn; main() { pthread_t thread1, thread2; char *message1 = "Hello"; char *message2 = "World"; semaphore_init( &child_counter ); semaphore_init( &worlds_turn ); semaphore_down( &worlds_turn ); /* world goes second */ semaphore_decrement( &child_counter ); /* value now 0 */ semaphore_decrement( &child_counter ); /* value now -1 */ /* * child_counter now must be up-ed 2 times for a thread blocked on it * to be released * */ pthread_create( &thread1, pthread_attr_default, (void *) &print_message_function, (void *) message1); semaphore_down( &worlds_turn ); pthread_create(&thread2, pthread_attr_default, (void *) &print_message_function, (void *) message2); semaphore_down( &child_counter ); /* not really necessary to destroy since we are exiting anyway */ semaphore_destroy ( &child_counter ); semaphore_destroy ( &worlds_turn ); exit(0); } void print_message_function( void *ptr ) { char *message; message = (char *) ptr; printf("%s ", message); fflush(stdout); semaphore_up( &worlds_turn ); semaphore_up( &child_counter ); pthread_exit(0); } 在这个hello world的版本中没有竞争情形,单词以适当的顺序打印出来,读者们该满意了。child_counter信号量迫使父线程在两个子线程执 行完printf和semaphore_up( &child_counter )之前都被阻塞。 这节提到了函数: semaphore_init(), semaphore_up(), semaphore_down(), semaphore_destroy(), and semaphore_decrement(). 开始使用pthreads(5)编程方法 5.编程方法 为了编译pthreads,你必须包含pthreads头文件,#include并且链接到pthread库。例如,cc hello_world.c -o hello_world -lpthreads(在 alpha上,你还应包含 -lc_r)。为了使用信号量库,你也应包含它的头文件并且链接到目标文件或者库。DEC pthreads是建立在POSIX IV 线程 标准的基础上的,而不是在POSIX VIII线程基础上的。pthread_join函数允许一个线程去等待另一个线程的结束。这个函数可以被用在Hello World程序中来替代semaphore_up()/semaphore_down()的信号量操作,DEC对pthread_join函数的实现当指定的线程目标不再存在时将变得不可 靠。例如,在下面的代码中,如果some_thread不再存在,pthread_join不会返回而是会导致错误。 pthread_t some_thread; void *exit_status; pthread_join( some_thread, &exit_status ); 另一些奇怪的错误会在这个线程的函数外发生。当这些错误很说发生并且离的较远,一些库做了"单进程"的假设。例如,我们已经历过了在使 用有缓存的输入输出流函数fread和fwrite发生的间歇性的困难,这只能归因于竞争情形。在错误的问题上,虽然我们不检查线程相关函数的返 回值,返回值必须始终被检查。几乎所有的pthreads相关的函数如果返回-1这代表有错误。比如: pthread_t some_thread; if ( pthread_create( &some_thread, ... ) == -1 ) { perror("Thread creation error"); exit(1); } 信号量库会打印出信息并且在有错误的情况下退出。一些有用的函数没有在例子中提到: pthread_yield(); Informs the scheduler that the thread is willing to yield its quantum, requires no arguments. pthread_t me; me = pthread_self(); Allows a pthread to obtain its own identifier pthread_t thread; pthread_detach(thread); Informs the library that the threads exit status will not be needed by subsequent pthread_join calls resulting in better threads performance.
|