以文本方式查看主题

-  中文XML论坛 - 专业的XML技术讨论区  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  使用pthreads[转帖]  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=39173)


--  作者: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.


--  作者:卷积内核
--  发布时间:10/23/2006 2:49:00 PM

--  
支持一下
--  作者:everlasting_188
--  发布时间:12/19/2008 10:56:00 AM

--  
非常感谢!!!!!
W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
92.773ms