有时人们认为,用fork调用来创建新进程的代价太高。在这种情况下,如果能让一个进程同时做两件事情或至少看起来是这样将会非常有用。而且,你可能希望能有两件或更多的事情以一种非常紧密的方式同时发生。这就是需要线程发挥作用的时候了。

下面将介绍以下内容:

在进程中创建新线程

在一个进程中同步线程之间的数据访问

修改线程的属性

在同一个进程中,从一个线程中控制另一个线程


什么是线程?

      在一个程序中的多个执行路线就叫做线程(thread)。更准确的定义是:线程是一个进程内部的一个控制序列。虽然linux和许多其他的操作系统一样,都擅长同时运行多个进程,但迄今为止我们看到的所有程序在执行的时候都是作为一个单独的进程。事实上,所有的进程都至少有一个执行线程。

      弄清楚fork系统调用和创建新线程之间的区别非常重要。当进程执行fork调用时,将创建出该进程的一份新副本。这个新进程拥有自己的变量和自己的PID,它的时间调度也是独立的,它的执行几乎完全独立于父进程。当在进程中创建一个新线程时,新的执行线程将拥有自己的栈,但与它的创建者共享全局变量、文件描述符、信号处理函数和当前目录状态。


线程的优点和缺点

在某些环境下,创建新线程要比创建新进程有更明显的优势。新线程的创建代价要比新进程小得多

使用线程的优点:

     有时,让程序看起来好像是在同时做两件事情是很有用的。一个经典的例子是,在编辑文件的同时对文件的单词个数进行实时统计。一个线程负责处理用户的输入并执行文本编辑工作,另一个则不断刷新单词计数变量。第一个线程(甚至是第三个线程)通过这个共享的计数变量让用户随时了解自己的工作进展情况。另一个例子是一个多线程的数据库服务器,这是一种明显的单进程服务多用户的情况。它会在响应一些请求的同时阻塞另外一些请求,使之等待磁盘操作,从而改善整体上的数据吞吐量。对于数据库服务器来说,这个明显的多任务工作如果用多进程的方式来完成将很难做到高效,因为各个不同的进程必须紧密合作才能满足加锁和数据一致性方面的要求,而用多线程来完成就比多进程要容易得多。

      一个混杂着输入、计算和输出的应用程序,可以将这几个部分分离为3个线程来执行,从而改善程序执行的性能。当输入或输出线程等待连接时,另外一个线程可以继续执行。因此,如果一个进程在任一时刻最多只能做一件事情的话,线程可以让它在等待连接之类的事情的同时做一些其他有用的事情。一个需要同时处理多个网络连接的服务器应用程序也是一个天生适用于应用多线程的例子。

      一般而言,线程之间的切换需要操作系统做的工作要比进程之间的切换少得多,因此多个线程对资源的需求要远小于多个进程。如果一个程序在逻辑上需要有多个执行线程,那么在单处理器系统上把它运行为一个多线程程序才更符合实际情况。虽然如此,编写一个多线程程序的设计困难较大。


线程也有缺点:

      编写多线程程序需要非常仔细的设计。在多线程程序中,因时序上的细微偏差或无意造成的变量共享而引发错误的可能性是很大的。

      对多线程程序的调试要比单线程程序的调试困难得多,因为线程之间的交互非常难于控制。

      将大量计算分成两个部分,并把这两个部分作为两个不同的线程来运行的程序在一台单处理器机器上并不一定运行得更快,除非计算确实允许它的不同部分可以被同时计算,而且运行它的机器拥有多个处理器核来支持真正的多处理。


第一个线程程序

      线程有一套完整的与其有关的函数库调用,它们中的绝大多数函数名都以pthread_开头。为了使用这些函数库调用,我们必须定义_REENTNANT,在程序中包含头文件pthread.h,并且在编译程序时需要用选项-l pthread来链接线程库。

      在设计最初的unix和posix库例程时,人们假设每个进程中只有一个执行进程。一个明显的例子就是errno,该变量用于获取某个函数调用失败后的错误信息。在一个多线程程序里,默认情况下,只有一个errno变量供所有的线程共享。在一个线程准备获取刚才的错误代码时,该变量很容易被另一线程中的函数调用所改变。类似的问题还存在于fputs之类的函数中,这些函数通常用一个全局性区域来缓存输出数据。

      为解决这个问题,我们需要使用被称为可重入的例程。可重入代码可以被多次调用而仍然正常工作,这些调用可以来自不同的线程,也可以是某种形式的嵌套调用。因此,代码中的可重入部分通常只使用局部变量,这使得每次对该代码的调用都将获得它自己的唯一的一份数据副本。

      编写多线程程序时,我们通过定义宏_RENTRANT来告诉编译器我们需要可重入功能,这个宏的定义必须位于程序中的任何#include语句之前。它将为我们做3件事情,并且做得非常优雅,以至于我们一般不需要知道它到底做了哪些事。

      它会对部分函数重新定义它们的可安全重入的版本,这些函数的名字一般不会发生改变,只是会在函数名后面添加_r字符串。例如:函数名gethostbyname将变为gethostbyname_r。

      stdio.h中原来以宏的形式实现的一些函数将变成可安全重入的函数。

      在errno.h中定义的变量errno现在将成为一个函数调用,它能够以一种多线程安全的方式来获取真正的errno值。

      在程序中包含头文件pthread.h还将向我们提供一些其他的将在代码中使用到的定义和函数原型,就如同头文件stdio.h为标准输入和标准输出例程所提供的定义一样。最后,需要确保在程序中包含了正确的线程头文件,并且在编译程序时链接了实现pthread函数的正确的线程库。有关编译线程程序的更详细的情况将在下面介绍。现在,我们首先来看一个用于管理线程的新函数pthread_create,它的作用是创建一个新线程,类似于创建新进程的fork函数。它的定义如下所示:

      #include <pthread.h>

      int pthread_create(pthread_t * thread,pthread_attr_t * attr,void *(*start_routine)(void *),void * arg);

这个函数定义看起来很复杂,其实用起来很简单。第一个参数是指向pthread_t类型数据的指针。线程被创建时,这个指针指向的变量中将被写入一个标识符,我们用该标识符来引用新线程。下一个参数用于设置线程的属性。我们一般不需要特殊的属性,所以只需设置该参数为NULL。最好两个参数分别告诉线程将要启动执行的函数和传递给该函数的参数。

      void *(* start_routine) (void *)

上面一行告诉我们必须要传递一个函数地址,该函数以一个指向void的指针为参数,返回的也是一个指向void的指针。因此,可以传递一个任一类型的参数并返回一个任一类型的指针。用fork调用后,父子进程将在同一位置继续执行下去,只是fork调用的返回值是不同的;但对新线程来说,我们必须明确地提供给它一个函数指针,新线程将在这个新位置开始执行。

      该函数调用成功时返回值是0,如果失败则返回错误代码。pthread_create和大多数pthread_系列函数一样,在失败时并未遵循unix函数的惯例返回-1,这种情况在unix函数中属于一少部分。

      线程通过调用pthread_exit函数终止执行,就如同进程在结束时调用exit函数一样。这个函数的作用是,终止调用它的线程并返回一个指向某个对象的指针。注意,绝不能用它来返回一个指向局部变量的指针,因为线程调用该函数后,这个局部变量就不再存在了,这将引起严重的程序漏洞。pthread_exit函数的定义如下所示:

#include <pthread.h>

void pthread_exit(void * retval);


pthread_join函数在线程中的作用等价于进程中用来收集子进程信息的wait函数。这个函数的定义如下所示:

#include <pthread.h>

int pthread_join(pthread_t th,void ** thread_return);

第一个参数指定了将要等待的线程,线程通过pthread_create返回的标识符来指定。第二个参数是一个指针,它指向另一个指针,而后者指向线程的返回值。与pthread_create类似,这个函数在成功时返回0,失败时返回错误代码。


实验:一个简单的线程程序

这个程序创建一个新线程,新线程与原先的线程共享变量,并在结束时向原先的线程返回一个结果。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
void *thread_function(void * arg);
char message[]="Hello World";
void * thread_function(void * arg)
{
  printf("thread_function is running.Argument was %s\n",(char *)arg);
  sleep(3);
  strcpy(message,"Bye");
  pthread_exit("Thank you for the CPU time");
}
int main(int argc, char **argv) {
     int res;
     pthread_t a_thread;
     void * thread_result;
     res=pthread_create(&a_thread,NULL,thread_function,(void *)message);
     if(res!=0)
     {
         perror("Thread creation failed");
         exit(EXIT_FAILURE);
     }
     printf("Waiting for thread to finish \n");
     res=pthread_join(a_thread,&thread_result);
     if(res!=0)
     {
         perror("Thread join failed");
         exit(EXIT_FAILURE);
     }
     printf("Thread joined,it returned %s\n",(char *)thread_result);
     printf("Message is now %s\n",message);
     exit(EXIT_SUCCESS);
}


      编译这个程序时,需要定义宏_REENTRANT,在少数系统上,可能还需要定义宏POSIX_C_SOURCE,但一般不需要定义它。

      必须链接正确的线程库。如果使用的是一个老的linux发行版,默认的线程库不是NPTL,可能需要升级linux发行版,查看头文件/usr/include/pthread.h。如果这个文件中显示的版权日期是2003或更晚,那几乎可以肯定你的linux发行版使用的是NPTL实现。如果日期比这个早,你可能就需要安装一个较新版本的linux了。


编译程序:gcc -D_REENTRANT -I/usr/include/nptl -o thread1 thread1.c -L/usr/lib/nptl -lpthread


如果系统默认使用的就是NPTL线程库,那么编译程序时就无需加上-I和-L选项,使用的命令如下所示

gcc -D_REENTRANT -o thread1 thread1.c  -lpthread


程序运行结果:

[root@localhost C_test]# ./thread1
Waiting for thread to finish
thread_function is running.Argument was Hello World
Thread joined,it returned Thank you for the CPU time
Message is now Bye


原线程和新线程共享全局变量message。



同时执行    

编写一个程序来验证两个线程的执行是同时进行的(如果是在一个单处理器系统上,线程的同时执行就需要靠CPU在线程之间的快速切换来实现)。这里还未介绍到任何线程同步函数,在这个程序中我们是在两个线程之间使用轮询技术,所以它的效率很低。同时,我们的程序仍然要利用这一事实,即除局部变量外,所有其他变量都将在一个进程的所有线程之间共享。


实验:两个线程同时执行

创建的程序thread2.c是在对thread1.c稍加修改的基础上编写出来的。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
int run_now=1;
void * thread_function(void * arg)
{
    int print_count2=0;
    while(print_count2++ < 20)
    {
      if(run_now==2)
      {
          printf("2");
          run_now=1;
      }
      else {
        sleep(1);
    }
    }
    return NULL;
}
int main(int argc, char **argv) {
    pthread_t pthread;
    void * pthread_return;
    int res,print_count1=0;
    res=pthread_create(&pthread,NULL,thread_function,NULL);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
    printf("Waiting for thread to finish \n");
      while(print_count1++ < 20)
        {
          if(run_now==1)
          {
              printf("1");
              run_now=2;
          }
          else {
            sleep(1);
        }
        }
    return 0;
}

我们增加了一个变量来测试哪个线程正在运行。

int run_now=1;

我们将在执行main函数时把run_now设置为1,在执行新线程时将其设置为2。

我们不断地检查来等待它的值为1,这种方式被称为忙等待,虽然已经在两次检查之间休息1秒钟来减慢检查的频率了。

在新线程执行的thread_function函数中,我们所做的事情和上面大部分相同,只是把run_now的值颠倒了一下。


我们还删除了参数的传递和返回值的传递。

编译运行程序:

[root@localhost C_test]# gcc -D_REENTRANT -o thread2 thread2.c -lpthread
[root@localhost C_test]# ./thread2
Waiting for thread to finish
12121212121212121212


实验解析:

每个线程通过设置run_now变量的方法来通知另一个线程开始运行,然后它会等待另一个线程改变了这个变量的值后再次运行。这个例子显示了两个线程之间自动交替执行,同时也再次阐明了一个观点,即这两个线程共享run_now变量。




同步

     上面我们看到两个线程同时执行的情况,但我们采用的在它们之间进行切换的方法是非常笨拙且没有效率的。不过,专门有一组设计好的函数为我们提供了更好的控制线程执行很访问代码临界区域的方法。

      这里学习两种基本的方法。一种是信号量,它的作用如同看守一段代码的看门人;另一种是互斥量,它的作用如同保护代码段的一个互斥设备。这两种方法很相似,事实上,它们可以互相通过对方来实现。但在实际应用中,对于一些情况,可能使用信号量或互斥量中的一个更符合问题的语义,并且效果更好。例如:如果想控制任一时刻只能有一个线程可以访问一些共享内存,使用互斥量就要自然得多。但在控制对一组相同对象的访问时---比如从5条可用的电话线中分配1条给某个线程的情况,就更适合使用计数信号量。具体选择哪种方法取决于个人偏好和相应的程序机制。


用信号量进行同步

      有两组接口函数用于信号量。一组取自POSIX的实时扩展,用于线程。另一组被称为系统V信号量,常用于进程的同步。这两组接口函数虽然很相近,但并不保证它们之间可以互换,而且它们呢使用的函数调用各不相同。

       信号量是一个特殊类型的变量,它可以被增加或减少,但对其的关键访问被保证是原子操作,即使在一个多线程程序中也是如此。这意味着如果一个程序中有两个(或更多)的线程试图改变一个信号量的值,系统将保证所有的操作都将依次进行。但如果是普通变量,来自同一程序中的不同线程的冲突操作所导致的结果将是不确定的。

      这里,我们介绍一种最简单的信号量--二进制信号量,它只有0和1两种取值。还有一种更通用的信号量--计数信号量,它可以有更大的取值范围。信号量一般常用来保护一段代码,使其每次只能被一个执行线程进行,要完成这个工作,就要使用二进制信号量。有时,我们希望可以允许有限数目的线程执行一段指定的代码,这就需要用到计数信号量。由于计数信号量并不常用,所以我们在这里不对它进行深入的介绍,实际上它仅仅是二进制信号量的一种逻辑扩展,两者实际调用的函数都一样。

     信号量函数的名字都以sem_开头,而不像大多数线程函数那样以pthread_开头。线程中使用的基本信号量函数有4个,它们都非常简单。

信号量通过sem_init函数创建,它的定义如下:

#include <semaphore.h>

int sem_init(sem_t * sem,int pshared,unsigned int value);

这个函数初始化由sem指向的信号量对象,设置它的共享选项,并给它一个初始的整数值。pshared参数控制信号量的类型,如果其值为0,就表示这个信号量是当前进程的局部信号量,否则,这个信号量就可以在多个进程之间共享。我们在这里只对不能在进程间共享的信号量感兴趣。


#include <semaphore.h>

int sem_wait(sem_t * sem);

int sem_post(sem_t * sem);

这两个函数都以一个指针为参数,该指针指向的对象是由sem_init调用初始化的信号量。

        sem_post函数的作用是以原子操作的方式给信号量的值加1。所谓原子操作是指,如果两 个线程企图同时给一个信号量加1,它们之间不会互相干扰,而不像如果两个程序同时对同一个文件进行读取、增加、写入操作时可能会引起冲突。信号量的值总是会被正确地加2,因为有两个线程试图改变它。

        sem_wait韩式以原子操作方式将信号量减1,但它会等待直到信号量有个非零值才会开始减法操作。如果对值为0的信号量调用sem_wait,这个函数就会等待,直到有其他线程增加了该信号量使其不再是0为止。如果两个线程同时在sem_wait调用上等待同一个信号量变为非零值,那么当该信号量被第三个线程增加1时,只有其中一个线程将开始对信号量减1,然后继续执行,另外一个线程还将继续等待。信号量的这种“在单个函数中就能原子化地进行测试和设置”的能力使其变得非常有价值。

         最好一个信号量函数是sem_destroy。这个函数的作用是,用完信号量后对它进行清理。它的定义如下:

#include <semaphore.h>

int sem_destroy(sem_t * sem);

与前几个函数一样,这个函数也以一个信号量指针为参数,并清理该信号量拥有的所有资源。如果企图清理的信号量正被一些线程等待,就会收到一个错误。与大多数linux函数一样,这些函数在成功时都返回0。


实验:一个线程信号量

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
sem_t bin_sem;
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
void * thread_function(void * arg)
{
   sem_wait(&bin_sem);
   while(strncmp("end",work_area,3)!=0)
   {
       printf("You input %d characters\n",strlen(work_area)-1);
       sem_wait(&bin_sem);
   }
   pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread;
    void * thread_result;
    res=sem_init(&bin_sem,0,0);
    if(res!=0)
    {
        perror("Semaphore init failed");
        exit(1);
    }
    res=pthread_create(&a_thread,NULL,thread_function,NULL);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
    printf("Input some text.Enter 'end' to finish\n");
    while(strncmp("end",work_area,3)!=0)
    {
        fgets(work_area,WORK_SIZE,stdin);
        sem_post(&bin_sem);
    }
    printf("\nWaiting for thread to finish ...\n");
    res=pthread_join(a_thread,&thread_result);
    if(res!=0)
    {
        perror("Thread join failed");
        exit(1);
    }
    printf("Thread joined\n");
    sem_destroy(&bin_sem);
    exit(0);
}

sem_t bin_sem;我们将这个信号量的初始值设置为0。

      在main函数中,启动新线程后,我们从键盘读取一些文本并把它们放到工作区work_area数组中,然后调用sem_post增加信号量的值。

      在新线程中,我们等待信号量,然后统计来自输入的字符个数。

      设置信号量的同时,我们等待着键盘的输入。当输入到达时,我们释放信号量,允许第二个线程在第一个线程再次读取键盘输入之前统计出输入字符的个数。

      这两个程序共享同一个work_area数组。为了让代码更加简介并容易理解,我们还省略了一些错误检查。如没有检查sem_wait函数的返回值。


编译运行:

[root@localhost C_test]# gcc -D_REENTRANT -o thread3 thread3.c -lpthread
[root@localhost C_test]# ./thread3
Input some text.Enter 'end' to finish
123
You input 3 characters
123456
You input 6 characters
end

在线程程序中,时序错误查找起来总是特别困难,但这个程序似乎对快速的文本输入和悠闲的暂停都很适应。


实验解析:

初始化信号量时,我们把它的值设置为0.这样,在线程函数启动时,sem_wait函数调用就会阻塞并等待信号量变为非零值。

      在主线程中,我们等待到有文本输入,然后调用sem_post增加信号量的值,这将立刻令另一个线程从sem_wait的等待中返回并开始执行。在统计完字符个数之后,它再次调用sem_wait并再次被阻塞,直到主线程再次调用sem_post增加信号量的值为止。

      我们很容易忽略程序设计上的细微错误,而该错误会导致程序运行结果中的一些细微错误。我们将上面的程序稍加修改。它偶尔会将来自键盘的输入用事先准备好的文本自动替换掉。

我们把main函数中的读数据循环修改为:


  printf("Input some text.Enter 'end' to finish\n");
       while(strncmp("end",work_area,3)!=0)
       {
                 if(strncmp(work_area,"FAST",4)==0)
                  {
                     sem_post(&bin_sem);
                     strcpy(work_area,"yao");
                  }

             else{
                  fgets(work_area,WORK_SIZE,stdin);
                 }

               sem_post(&bin_sem);
       }


程序输出结果:


[root@localhost C_test]# ./thread3a
Input some text.Enter 'end' to finish
111
You input 3 characters
FAST
You input 2 characters
You input 2 characters
You input 2 characters
end

问题在于,我们的程序依赖其接收文本输入的时间要足够长,这样另一个线程才有时间在主线程还未准备好给它更多的单词去统计之前统计出工作区中字符的个数。当我们试图连续快速给它两组不同的单词去统计时(键盘输入的FAST和程序自动提供的yao),第二个线程就没有时间去执行。但信号量已被增加了不止一次,所以字符统计线程就会反复统计字符数目并减少信号量的值,直到它再次变为0为止。

       这个例子显示:在多线程程序中,我们需要对时序考虑得非常详细。为了解决上面的问题,我们可以再增加一个信号量,让主线程等到统计线程完成字符个数的统计后再继续执行,但更简单的一种方式是使用互斥量。



用互斥量进行同步

     另一种用在多线程程序中的同步访问方法是使用互斥量。它允许程序员锁住某个对象,使用每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后解锁它。

     用于互斥量的基本函数和用于信号量的函数非常相似,它们的定义如下所示:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex,const pthread_mutexattr_t * mutexattr);

int pthread_mutex_lock(pthread_mutex_t * mutex);

int pthread_mutex_unlock(pthread_mutex_t * mutex);

int pthread_mutex_destroy(pthread_mutex_t * mutex);

     与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,你必须对函数的返回代码进行检查。

      与信号量类似,这些函数的参数都是一个先前声明过的对象的指针。对互斥量来说,这个对象的类型为pthread_mutex_t。pthread_mutex_init函数中的属性参数允许我们设置互斥量的属性,而属性控制着互斥量的行为。属性类型默认为fast,但它有一个小缺点:如果程序试图对一个已经加了锁的互斥量调用pthread_mutex_lock,程序就会被阻塞,而又因为拥有互斥量的这个线程正是现在被阻塞的线程,所以互斥量就永远也不会被解锁了,程序也就进入死锁状态。这个问题可以通过改变互斥量的属性来解决,我们可以让它检查这种情况并返回一个错误,或者让它递归的操作,给同一个线程加上多个锁,但必须注意在后面执行同等数量的解锁操作。

       这里不设置互斥量的属性,我们将传递NULL给属性指针。


实验:线程互斥量

我们用一个互斥量来保证任一时刻只能有一个线程访问它们。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
#define WORK_SIZE 1024
pthread_mutex_t work_mutex;
char work_area[WORK_SIZE];
int time_to_exit=0;
void * thread_function(void * arg)
{
    sleep(1);
    pthread_mutex_lock(&work_mutex);
    while(strncmp("end",work_area,3)!=0)
    {
        printf("You input %d characters\n",strlen(work_area)-1);
        work_area[0]='\0';
        pthread_mutex_unlock(&work_mutex);
        sleep(1);
        pthread_mutex_lock(&work_mutex);
        while(work_area[0]=='\0')
        {
            pthread_mutex_unlock(&work_mutex);
            sleep(1);
            pthread_mutex_lock(&work_mutex);
        }
    }
    time_to_exit=1;
    work_area[0]='\0';
    pthread_mutex_unlock(&work_mutex);
    pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread;
    void * thread_result;
    res=pthread_mutex_init(&work_mutex,NULL);
    if(res!=0)
    {
        perror("Mutex init failed");
        exit(1);
    }
    res=pthread_create(&a_thread,NULL,thread_function,NULL);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
     pthread_mutex_lock(&work_mutex);
     printf("Input some text,Enter 'end' to finish\n");
     while(!time_to_exit)
     {
         fgets(work_area,WORK_SIZE,stdin);
         pthread_mutex_unlock(&work_mutex);
         while(1)
         {
             pthread_mutex_lock(&work_mutex);
             if(work_area[0]!='\0')
             {
                 pthread_mutex_unlock(&work_mutex);
                 sleep(1);
             }
             else {
                break;
            }
         }
     }
     pthread_mutex_unlock(&work_mutex);
     printf("\nWaiting for thread to finish..\n");
     res=pthread_join(a_thread,&thread_result);
     if(res!=0)
     {
         perror("Thread join failed");
         exit(1);
     }
     printf("Thread joined\n");
     pthread_mutex_destroy(&work_mutex);
     exit(0);
}


编译运行

[root@localhost C_test]# gcc -D_REENTRANT -o thread4 thread4.c -lpthread
[root@localhost C_test]# ./thread4
Input some text,Enter 'end' to finish
adss
You input 4 characters
The sdf ? df
You input 12 characters
FAST
You input 4 characters
end
Waiting for thread to finish..
Thread joined


实验解析

在程序的开始,我们声明了一个互斥量、工作区和一个变量time_to_exit。如下所示:

pthread_mutex_t work_mutex;

#define WORK_SIZE 1024

char work_area[WORK_SIZE];

int time_to_exit=0;


然后初始化互斥量,如下所示:

res=pthread_mutex_init(&work_mutex,NULL);


接下来启动新线程。

pthread_mutex_lock(&work_mutex);
   while(strncmp("end",work_area,3)!=0)
   {
       printf("You input %d characters\n",strlen(work_area)-1);
       work_area[0]='\0';
       pthread_mutex_unlock(&work_mutex);
       sleep(1);
       pthread_mutex_lock(&work_mutex);
       while(work_area[0]=='\0')
       {
           pthread_mutex_unlock(&work_mutex);
           sleep(1);
           pthread_mutex_lock(&work_mutex);
       }
   }
   time_to_exit=1;
   work_area[0]='\0';
   pthread_mutex_unlock(&work_mutex);

      新线程首先试图对互斥量加锁。如果它已经被锁住,这个调用将被阻塞直到它被释放为止。一旦获得访问权,我们就检查是否有申请退出程序的请求。如果有,就设置time_to_exit变量,再把工作区的第一个字符设置为\0,然后退出。

      如果不想退出,就统计字符个数,然后把work_area数组中的第一个字符设置为NULL。我们用将第一个字符设置为NULL的方法通知读取输入的线程,我们已完成了字符统计。然后解锁互斥变量并等待主线程继续进行。我们将周期性地尝试给互斥量加锁,如果加锁成功,就检查是否主线程又有字符送来要处理。如果还没有,就解锁互斥量继续等待;如果有,就统计字符个数并再次进入循环。


下面是主线程的代码:

pthread_mutex_lock(&work_mutex);
    printf("Input some text,Enter 'end' to finish\n");
    while(!time_to_exit)
    {
        fgets(work_area,WORK_SIZE,stdin);
        pthread_mutex_unlock(&work_mutex);
        while(1)
        {
            pthread_mutex_lock(&work_mutex);
            if(work_area[0]!='\0')
            {
                pthread_mutex_unlock(&work_mutex);
                sleep(1);
            }
            else {
               break;
           }
        }
    }

    pthread_mutex_unlock(&work_mutex);

这段代码和上面新线程中的很类似。我们首先给工作区加锁,读入文本到它里面,然后解锁以允许其他线程访问它并统计字符数目。我们周期性地对互斥量再加锁,检查字符数目是否已统计完(work_area[0]被设置为NULL)。如果还需要等待,就释放互斥量。如前所示,这种通过轮询来获得结果的方法通常不是好的编程方式。在实际的编程中,我们应该尽可能用信号量来避免出现这种情况。



线程的属性

     我们可以控制的线程属性非常多,但在这里只介绍最可能用到的,其他属性的可以在手册中找到。在前面的所有程序示例中,我们都在程序退出之前用pthread_join对线程再次进行同步,如果

我们想让线程向创建它的线程返回数据就需要这样做。但有时也会有这种情况,我们既不需要第二个线程向主线程返回信息,也不想让主线程等待它的结束。

     假设我们在主线程继续为用户提供服务的同时创建了第二个线程,新线程的作用是将用户正在编辑的数据文件进行备份存储。备份工作结束后,第二个线程就可以直接终止了,它没有必要再回到主线程中了。

     我们可以创建这一类型的线程,它们被称为脱离线程(detached thread)。可以通过修改线程属性或调用pthread_detach的方法来创建它们。这里只介绍前一种方法:

需要用到函数是pthread_attr_init,它的作用是初始化一个线程属性对象。

#include <pthread.h>

int pthread_attr_init(pthread_attr_t * attr);

与前面函数一样,成功时返回0,失败时返回错误代码。

    还有一个回收函数pthread_attr_destroy,它的目的是对属性对象进行清理和回收。一旦对象被回收了,除非它被重新初始化,否则就不能被再次使用。

    初始化一个线程属性对象后,我们可以调用许多其他的函数来设置不同的属性行为。我们把其中主要的一些函数列在下面。

#include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t * attr,int detachstate);

int pthread_attr_getdetachstate(const pthread_attr_t * attr,int * detachstate);

int pthread_attr_setschedpolicy(pthread_attr_t * attr,int policy);

int pthread_attr_getschedpolicy(const pthread_attr_t * attr,int policy);

int pthread_attr_setschedparam(pthread_attr_t * attr,const struct sched_param * param);

int pthread_attr_setinheritsched(pthread_attr_t * attr,int inherit);

int pthread_attr_getinheritsched(const pthread_attr_t * attr,int * inherit);

int pthread_attr_setscope(pthread_attr_t * attr,int scope);

int pthread_attr_getscope(const pthread_attr_t * attr,int * scope);

int pthread_attr_setstacksize(pthread_attr_t * attr,int scope);

int pthread_attr_getstacksize(pthread_attr_t * attr,int * scope);



detachedstate:这个属性允许我们无需对线程进行重新合并。与大多数_set类函数一样,它以一个属性指针和一个标志为参数来确定需要的状态。pthread_attr_setdetachstate函数可能用到的两个标志分别是PTHREAD_CREATE_JOINABLE(默认标志值)和PTHREAD_CREATE_DETACHED。

PTHREAD_CREATE_JOINABLE:可以允许两个线程重新合并

PTHREAD_CREATE_DETACHED:不能调用pthread_join来获得另一个线程的退出状态。


schedpolicy:这个属性控制线程的调度方式。它的取值可以是SCHED_OTHER、SCHED_RR和SCHED_FIFO。这个属性的默认值为SCHED_OTHER。另外两种调用方式只能用于以超级用户权限运行的进程,因为它们都具备实时调度的功能,但在行为上略有区别。SCHED_RR使用循环(round-robin)调度机制,而SCHED_FIFO使用“先进先出”策略。


schedparam:这个属性是和schedpolicy属性结合使用的,它可以对以SCHED_OTHER策略运行的线程的调度进行控制。


inheritsched:这个属性可取两个值:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED。它的默认值是PTHREAD_EXPLICIT_SCHED,表示调度由属性明确地设置。如果把它设置为PTHREAD_INHERIT_SCHED,新线程将沿用其创建者所使用的参数。


scope:这个属性控制一个线程调度的计算方式。由于目前linux只支持它的一种取值PTHREAD_SCOPE_SYSTEM。


stacksize:这个属性控制线程创建的栈的大小,单位为字节。它属于POSIX规范中的“可选”部分,只有在定义了宏_POSIX_THREAD_ATTR_STACKSIZE的实现版中才支持。Linux在实现线程时,默认使用的栈很大,所以这个功能对Linux来说显得有些多余。


实验:设置脱离状态属性

     在脱离线程实例thread5.c中,我们创建一个线程属性,将其设置为脱离状态,然后用这个属性创建一个线程。子线程结束时,它照常调用pthread_exit,但这次,原先的线程不再等待与它创建的子线程重新合并。主线程通过一个简单的thread_finished标志来检测子线程是否已经结束,并显示线程之间仍然共享着变量。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
char message[]="Hello World";
int thread_finished=0;
void * thread_function(void * arg)
{
   printf("thread_function is running.Argument was %s\n",(char *)arg);
   sleep(3);
   printf("Second thread setting finished flag,and exiting now\n");
   thread_finished=1;
   pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread;
    pthread_attr_t thread_attr;
    res=pthread_attr_init(&thread_attr);
    if(res!=0)
    {
        perror("Attribute creation failed");
        exit(1);
    }
    res=pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
    if(res!=0)
        {
            perror("Setting detached attribute failed");
            exit(1);
        }
    res=pthread_create(&a_thread,&thread_attr,thread_function,(void *)message);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
    pthread_attr_destroy(&thread_attr);
    while(!thread_finished)
    {
        printf("Waiting for thread to finish... \n");
        sleep(1);
    }
    printf("Other thread finished \n");
    exit(0);
}

设置脱离状态属性可以允许第二个线程独立地完成工作,无需原先的线程等待它。


编译运行:

[root@localhost C_test]# gcc -D_REENTRANT -o thread5 thread5.c -lpthread
[root@localhost C_test]# ./thread5
Waiting for thread to finish...
thread_function is running.Argument was Hello World
Waiting for thread to finish...
Waiting for thread to finish...
Second thread setting finished flag,and exiting now
Other thread finished


线程属性——调度

来看另外一个 可能希望修改的线程属性:调度。改变调度属性和设置脱离状态非常相似,可以用sched_get_priority_max和sched_get_priority_min这两个函数来查找可用的优先级级别。


实验:调度

这里的程序thread6.c与前面的例子很相似。

(1)首先,定义一些额外的变量:

int max_priority;

int min_priority;

struct sched_param scheduling_value;


(2)设置好脱离属性后,设置调度策略:

res=pthread_attr_setschedpolicy(&thread_attr,SCHED_OTHER);

if(res!=0)

{

   perror("Setting scheduling policy failed");

   exit(1);

}


(3)接下来查找允许的优先级范围:

max_priority=sched_get_priority_max(SCHED_OTHER);

min_priority=sched_get_priority_min(SCHED_OTHER);


(4)然后设置优先级:

scheduling_value._sched_priority=min_priority;

res=pthread_attr_setschedparam(&thread_attr,&scheduling_value);

if(res!=0)

{

  perror("Setting scheduling priority failed");

  exit(1);

}


thread6.c

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
char message[]="Hello World";
int thread_finished=0;
void * thread_function(void * arg)
{
   printf("thread_function is running.Argument was %s\n",(char *)arg);
   sleep(3);
   printf("Second thread setting finished flag,and exiting now\n");
   thread_finished=1;
   pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res,min_priority,max_priority;
    pthread_t a_thread;
    pthread_attr_t thread_attr;
    struct sched_param scheduling_value;
    res=pthread_attr_init(&thread_attr);
    if(res!=0)
    {
        perror("Attribute creation failed");
        exit(1);
    }
    res=pthread_attr_setdetachstate(&thread_attr,PTHREAD_CREATE_DETACHED);
    res=pthread_attr_setschedpolicy(&thread_attr,SCHED_OTHER);
    if(res!=0)
        {
            perror("Setting scheduling policy failed");
            exit(1);
        }
    max_priority=sched_get_priority_max(SCHED_OTHER);
    min_priority=sched_get_priority_min(SCHED_OTHER);
    scheduling_value.__sched_priority=min_priority;
    res=pthread_attr_setschedparam(&thread_attr,&scheduling_value);
    if(res!=0)
            {
                perror("Setting scheduling priority failed");
                exit(1);
            }
    res=pthread_create(&a_thread,&thread_attr,thread_function,(void *)message);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
    pthread_attr_destroy(&thread_attr);
    while(!thread_finished)
    {
        printf("Waiting for thread to finish... \n");
        sleep(1);
    }
    printf("Other thread finished \n");
    exit(0);
}


编译运行:

[root@localhost C_test]# gcc -D_REENTRANT -o thread6 thread6.c -lpthread
[root@localhost C_test]# ./thread6
Waiting for thread to finish...
thread_function is running.Argument was Hello World
Waiting for thread to finish...
Waiting for thread to finish...
Second thread setting finished flag,and exiting now
Other thread finished

这与设置脱离状态属性很相似,区别只是我们设置的是调度策略。



取消一个线程

      有时我们想让一个线程可以要求另一个线程终止,就像给它发送一个信号一样。线程有方法可以做到这点,与信号处理一样,线程可以在被要求终止时改变其行为。

      请求一个线程终止的函数:

#include <pthread.h>

int pthread_cancel(pthread_t thread);

这个函数的定义很简单,提供一个线程标识符,我们就可以发送请求来取消它。但在接收到取消请求的一端,事情就会稍微复杂点,不过也不是非常复杂。线程可以用pthread_setcancelstate设置自己的取消状态。

#include <pthread.h>

int pthread_setcancelstate(int state,int * oldstate);

第一个参数的取值可以是PTHREAD_CANCEL_ENABLE,这个值允许线程接收取消请求:或者是PTHREAD_CANCEL_DISABLE,它的作用是忽略取消请求。oldstate指针用于获取先前的取消状态。如果你对它没兴趣,只需传递NULL给它。如果取消请求被它接受了,线程就可以进入第二个控制层次,用pthread_setcanceltype设置取消类型。

#include <pthread.h>

int pthread_setcanceltype(int type,int * oldtype);

type参数可以有两种取值:一个是PTHREAD_CANCEL_ASYNCHRONOUS,它将使得在接收到取消请求后立即采取行动:另一个是PTHREAD_CANCEL_DEFERRED,它将使得在接收到取消请求后,一直等待直到线程执行了下述函数之一才采取行动。具体是函数pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancel、sem_wait或sigwait.

根据POSIX标准,其他可能阻塞的系统调用,如read、wait等也可以成为取消点。如sleep确实允许取消动作的发生。为了安全起见,你可能会想在估计会被取消的代码中添加一些pthread_testcancel调用。

oldtype参数可以保存先前的状态,如果不想知道先前的状态,可以传递给NULL给它。默认情况下,线程在启动时的取消状态为PTHREAD_CANCEL_ENABLE,取消类型是PTHREAD_CANCEL_DEFERRED。


实验:取消一个线程

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void * thread_function(void * arg)
{
     int i,res;
     res=pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
     if(res!=0)
        {
            perror("Thread pthread_setcancelstate failed");
            exit(1);
        }
     res=pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
     if(res!=0)
            {
                perror("Thread pthread_setcanceltype failed");
                exit(1);
            }
     printf("thread_function is running \n");
     for(i=0;i<10;i++)
     {
         printf("Thread is still running (%d)..\n",i);
         sleep(1);
     }
     pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread;
    void * thread_result;
    res=pthread_create(&a_thread,NULL,thread_function,NULL);
    if(res!=0)
    {
        perror("Thread creation failed");
        exit(1);
    }
    sleep(3);
    printf("Canceling thread...\n");
    res=pthread_cancel(a_thread);
    if(res!=0)
    {
        perror("Thread cancelation failed");
        exit(1);
    }
    printf("Waiting for thread to finish ...\n");
    res=pthread_join(a_thread,&thread_result);
    if(res!=0)
        {
            perror("Thread join failed");
            exit(1);
        }
    exit(0);
}


编译运行:

[root@localhost C_test]# gcc -D_REENTRANT -o thread7 thread7.c -lpthread
[root@localhost C_test]# ./thread7
thread_function is running
Thread is still running (0)..
Thread is still running (1)..
Thread is still running (2)..
Canceling thread...
Waiting for thread to finish ...


实验解析:

通常的方法创建了新线程后,主线程休眠3秒(好让新线程有时间开始执行),然后发送一个取消请求。

   sleep(3);
   printf("Canceling thread...\n");
   res=pthread_cancel(a_thread);
   if(res!=0)
   {
       perror("Thread cancelation failed");
       exit(1);
   }


在新创建的线程中,我们首先将取消状态设置为允许取消。

    res=pthread_setcancelstate(PTHREAD_CANCEL_ENABLE,NULL);
    if(res!=0)
        {
            perror("Thread pthread_setcancelstate failed");
            exit(1);
        }


然后取消类型设置为延迟取消

    res=pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED,NULL);
    if(res!=0)
            {
                perror("Thread pthread_setcanceltype failed");
                exit(1);
            }


最好线程在循环中等待被取消,如下所示

for(i=0;i<10;i++)
    {
        printf("Thread is still running (%d)..\n",i);
        sleep(1);
    }



多线程

我们总是让程序的主执行线程仅仅创建一个线程,但现在想创建不止一个线程。


实验:多线程

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define NUM_THREADS 6
void * thread_function(void * arg)
{
    int number=*(int *)arg;
    int rand_num;
    printf("thread_function is running.Argument was %d\n",number);
    rand_num=(int)(rand()*9.0/(RAND_MAX+1.0))+1;
    sleep(rand_num);
    printf("Bye form %d\n",number);
    pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread[NUM_THREADS];
    void * thread_result;
    int lots_of_threads;
    for(lots_of_threads=0;lots_of_threads<NUM_THREADS;lots_of_threads++)
    {
        res=pthread_create(&a_thread[lots_of_threads],NULL,thread_function,(void *)&lots_of_threads);
        if(res!=0)
        {
            perror("Thread creation failed");
            exit(1);
        }
        sleep(1);
    }
    printf("Waiting for threads to finish...\n");
    for(lots_of_threads=NUM_THREADS-1;lots_of_threads>=0;lots_of_threads--)
    {
        res=pthread_join(a_thread[lots_of_threads],&thread_result);
        if(res==0)
        {
            printf("Picked up a thread\n");
        }
        else {
            perror("pthread_join failed");
        }
    }
    exit(0);
}


编译运行:

[root@localhost C_test]# gcc -D_REENTRANT -o thread8 thread8.c -lpthread
[root@localhost C_test]# ./thread8
thread_function is running.Argument was 0
thread_function is running.Argument was 1
thread_function is running.Argument was 2
thread_function is running.Argument was 3
thread_function is running.Argument was 4
Bye form 1
thread_function is running.Argument was 5
Waiting for threads to finish...
Bye form 5
Picked up a thread
Bye form 0
Bye form 2
Bye form 3
Bye form 4
Picked up a thread
Picked up a thread
Picked up a thread
Picked up a thread
Picked up a thread

我们创建了许多线程并让它们呢以随意的顺序结束执行。这个程序有一个小漏洞,如果将sleep调用从启动线程的循环中删除,它就会变得很明显。



删除主线程中sleep调用后,编译运行这个程序

[root@localhost C_test]# ./thread8a
Waiting for threads to finish...
thread_function is running.Argument was 5
thread_function is running.Argument was 5
thread_function is running.Argument was 5
thread_function is running.Argument was 5
thread_function is running.Argument was 5
thread_function is running.Argument was 5
Bye form 5
Bye form 5
Bye form 5
Bye form 5
Bye form 5
Picked up a thread
Picked up a thread
Picked up a thread
Picked up a thread
Bye form 5
Picked up a thread
Picked up a thread

启动线程时,线程函数的参数是一个局部变量,这个变量在循环中被更新,引起问题的参数是lots_of_threads,传递是一个地址,如果主线程运行够快的话,lots_of_threads的值就一下子累加到5了。要修正这个问题,我们可以直接传递这个参数的值。

res=pthread_create(&a_thread[lots_of_threads],NULL,thread_function,(void *)lots_of_threads);

当然thread_function函数也要修改

void * thread_function(void * arg)
{
   int number=(int)arg;


thread8b.c

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#define NUM_THREADS 6
void * thread_function(void * arg)
{
    int number=(int)arg;
    int rand_num;
    printf("thread_function is running.Argument was %d\n",number);
    rand_num=(int)(rand()*9.0/(RAND_MAX+1.0))+1;
    sleep(rand_num);
    printf("Bye form %d\n",number);
    pthread_exit(NULL);
}
int main(int argc, char **argv) {
    int res;
    pthread_t a_thread[NUM_THREADS];
    void * thread_result;
    int lots_of_threads;
    for(lots_of_threads=0;lots_of_threads<NUM_THREADS;lots_of_threads++)
    {
        res=pthread_create(&a_thread[lots_of_threads],NULL,thread_function,(void *)lots_of_threads);
        if(res!=0)
        {
            perror("Thread creation failed");
            exit(1);
        }
    }
    printf("Waiting for threads to finish...\n");
    for(lots_of_threads=NUM_THREADS-1;lots_of_threads>=0;lots_of_threads--)
    {
        res=pthread_join(a_thread[lots_of_threads],&thread_result);
        if(res==0)
        {
            printf("Picked up a thread\n");
        }
        else {
            perror("pthread_join failed");
        }
    }
    exit(0);
}



编译运行:

[root@localhost C_test]# vim thread8b.c
[root@localhost C_test]# gcc -D_REENTRANT -o thread8b thread8b.c -lpthread
[root@localhost C_test]# ./thread8b
Waiting for threads to finish...
thread_function is running.Argument was 3
thread_function is running.Argument was 4
thread_function is running.Argument was 5
thread_function is running.Argument was 2
thread_function is running.Argument was 1
thread_function is running.Argument was 0
Bye form 0
Bye form 4
Bye form 3
Bye form 5
Picked up a thread
Picked up a thread
Picked up a thread
Bye form 2
Picked up a thread
Bye form 1
Picked up a thread
Picked up a thread

问题解决了!