用户名: 密码: 忘记密码? 注册

线程及线程的创建

作者:  时间: 2010-10-20
一、线程的理论基础
(1)在Linux中,进程(process)是操作系统分配资源的最小单位,线程(thread)是操作系统调度的最小单位.
(2)使用多线程的理由:
   <1>与进程相比,线程是一种非常节俭的多任务操作方式.
     在Linux中,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种昂贵的多任务工作方式.而一个线程与它的父线程共享这些资源,这样就大大节省了系统资源.
   <2>线程间方便的通信机制.对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过进程间通信的某种机制进行,这种方式不仅费时,而且很不方便.线程则不然,由于同一进程下的线程之间共享父进程的数据段、代码段和堆栈段等,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便.比如在一个线程中修改的变量值,可以在另一个线程中直接应用,不必再占用新的资源.比起进程间通信的那些机制(管道、FIFO、消息队列等)显得非常方便.
   <3>多线程程序作为一种多任务、并发的工作方式,使多CPU系统更加有效,也可以改善程序结构.比如操作系统会保证线程数不大于CPU数目时,不同的线程运行不同的CPU上.又如一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样程序会利于理解和修改.
(3)Linux系统下的多线程遵循POSIX线程接口,称作pthread.这些接口被封装在pthread库内(不在标准c库内).因此,编写Linux下的多线程程序,需要使用头文件pthread.h,链接时需要使用库libpthread.a
(4)所有进程至少有一个线程,多线程的意义在于一个进程中有多个执行部分同时执行.每个线程处理各自独立的任务.
(5)多任务程序设计:
   一个进程可能要处理不同的应用,要求处理多种任务.如果开发不同的进程来处理,系统开销很大,数据共享、程序结构都不方便,这时可使用多线程编程方法.
(6)并发程序设计:
   一个任务可能分不同的步骤,这些不同的步骤之间可能是松散耦合,可能通过线程的互斥、同步并发完成.这样可以为不同的任务步骤建立线程.
(7)就像每个进程都有一个ID一样,每个线程也有一个ID.进程ID在整个系统中是唯一的,但线程ID不同,线程ID只在它所属的进程环境中有效.
 
二、多线程程序设计
   线程的相关函数都被封装在libthread.a库中,头文件为pthread.h.由于pthread的库不是Linux系统的库,所以在进行编译时需要加上"-lpthread",即#gcc filename -lpthread.如果编译时不使用该库,则Linux提示:"undefined reference to 'pthread_create'",即找不到该函数.
(1)线程的创建
   #include <pthread.h>
   int pthread_create(pthread_t *tidp,const pthread_attr_t *attr,void *(*start_rtn)(void),void *arg)
   函数功能:创建进程
   参数tidp:线程ID
   参数attr:线程属性,通常设置为空(NULL)
   参数start_rtn:线程要执行的函数
   参数arg:函数start_rtn的参数
   返回值:
   Attention!!!
   1.在线程创建后,就开始运行相关的线程函数,该哈数运行完后,该线程也就结束了.
   2.编译程序时,对于标准C库不需要指定库名,而对于数据库、线程库都需要指定库名.
   3.创建线程时,线程处理函数的参数必须是void *类型,返回值也必须是*void型.
   4.这两个线程谁先执行并不一定,由操作系统的算法决定.一般来说,先创建的线程先被执行.
   5.进程的ID可由函数getpid()返回值得到,线程的ID可由函数pthread_self()返回值得到.若用户需要打印某线程的ID,需要在线程处理函数中作如下处理:printf("the thread ID is %u\n",(unsigned int)pthread_self());
   6.关于线程处理函数的返回值.
     若需要该返回值,假定返回值为ret,则写成: return ((void *)set);若不需要返回值,则直接写成: return NULL;
(2)线程的终止
   若进程中任何一个线程中调用exit或_exit,则整个进程(包括进程所属线程)都会终止.线程有3种退出方式:
   1、线程从启动例程中正常返回.比如线程函数执行到return NULL.
   2、线程可以被另一个进程终止.
   3、线程自己调用pthread_exit()函数.
   #include <pthread.h>
   void pthread_exit(void *rval_ptr)
   函数功能:终止调用线程
   参数rval_ptr:线程退出返回值.进程中的其它线程可以通过pthread_join()访问到该指针.
   例程:
    void *create(void *arg)
   {
       printf("new thread is created ... \n");
     //return (void *)8;
     //pthread_exit((void *)8);
     //exit(0);
   }
   int main(int argc,char *argv[])
   {
      pthread_t tid;
      int error;
      void *temp;
      error = pthread_create(&tid, NULL, create, NULL);
         printf("main thread!\n");
      if( error )
      {
         printf("thread is not created ... \n");
         return -1;
      }
      error = pthread_join(tid, &temp);
      if( error )
      {
         printf("thread is not exit ... \n");
         return -2;
      }
  
      printf("thread is exit code %d \n", (int )temp);
      return 0;
   }
    执行结果:
 第1种情况: thread is exit code 8         //正常退出线程
 第2种情况: thread is exit code 8         //正常退出线程
 第3种情况: 无法打印出"thread is exit code %d \n",这是由于exit(0)直接退出了进程.
(3)线程的等待
   #include <pthread.h>
   int pthread_join(pthread_t tid,void **rval_ptr)
   函数功能:阻塞线程调用,直到指定的线程终止.调用该函数的线程将被挂起,将一直等待到所等待线程结束为止.
   参数1:等待退出的线程的ID
   参数2:线程退出时的返回值的指针.
(4)线程的终止
   #include <pthread.h>
   void pthread_cancel(pthread_t tid)
   函数功能:线程可以通过该函数来请求取消同一进程中的其它线程.该函数会发送终止信号给线程号为tid的线程,然后发送成功并不意味着tid线程就一定能终止.它可以选择忽略或自己控制取消的方式.
   返回值:如果成功则返回0,不成功则返回非0.
(5)线程的清理
   线程的终止有两种情况:正常终止和非正常终止.
   当线程主动调用pthread_exit()或从线程函数中return都将使线程正常退出,这是可预见的退出方式.
   当线程在其它线程的干预下,或者由于自身运行出错(比如访问非法地址)而推出,这是不可预见的退出方式.
   无论是哪种方式推出,都会存在资源释放的问题.如何保证线程终止时能顺利的释放掉自己所占用的资源,是一个必须考虑的问题.比如在一个线程中malloc一段内存空间,如果在线程退出时不释放这段内存,就有可能导致内存泄露.由于用户不清楚该线程什么时候被非正常终止(非预知性),也就不知道在线程处理函数的哪里来释放掉这段内存,因此,必须有一种机制来处理该问题.
   在Linux中,使用成对出现的pthread_cleanup_push和pthread_cleanup_pop来处理该问题.从pthread_cleanup_push的调用点到pthread_cleanup_pop之间的程序段中的终止动作(包括调用pthread_exit()和异常终止,不包括return)都将执行pthread_cleanup_push所指定的清理函数.
   在Linux中,线程可以建立多个清理处理程序.处理程序在栈中,即它们的执行顺序与它们压入栈内时的顺序相反.
   #include <pthread.h>
   void pthread_cleanup_push(void (*rtn)(void *),void *arg)
   函数功能:将清除函数压入清除栈
   参数1:指向清除函数的指针
   参数2:清除函数的参数
  
   #include <pthread.h>
   void pthread_cleanup_pop(int execute)
   函数功能:将清除函数弹出清除栈
   参数:execute执行到pthread_cleanup_pop时,是否在弹出清除函数的同时执行该函数,非0:执行;0:不执行.
   Attention!!!
   1.清理函数是为了给子线程处理函数擦屁股.
   2.包括正常退出和非正常退出都可以使用清理函数,具体清理函数是否执行是由函数pthread_cleanup_pop()的参数决定的.比如在子线程中:
         pthread_cleanup_push(cleanup1,NULL);
   pthread_cleanup_push(cleanup2,NULL);
  此时,这样清理函数cleanup2在栈顶,cleanup1在栈底.若只使用清理函数cleanup1,可以这样来:
         pthread_cleanup_pop(0);  //cleanup2被弹出,却不执行
         pthread_cleanup_pop(1);  //cleanup1被弹出,执行
   例程:
    char *p=NULL;
    void cleanup(void *arg)
   {
      printf("clean up\n");
      free(p);
   }
    void *thread1_fun(void *arg)
   {
      pthread_cleanup_push(cleanup,NULL);
      printf("this is new thread\n");
      p=(char *)malloc(100);
      sleep(5);
      printf("before pop\n");
      pthread_cleanup_pop(1);
      return NULL;
   }
   int main(int argc,char *agrv[])
   {
      pthread_t tid;
      pthread_create(&tid,NULL,thread1_fun,NULL);
      sleep(1);
      pthread_cancel(tid);
      while(1);
      return 0;
   }
 执行结果:
 this is new thread
 clean up
 程序分析:
 在main()函数中,在主线程内创建了一子线程.但CPU会先执行主线程,该例程中创建了子线程后主线程sleep(1),因此CPU会首先执行子线程.当子线程执行到sleep(5)时,子线程让出CPU的占有权,CPU去执行了主线程.主线程终止了子线程,由于在子线程处理函数中pthread_cleanup_pop的参数为非0,因此非正常退出前要执行清理函数cleanup().
(6)综合例程
    thread.c
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/stat.h>
    #include <stdio.h>
    #include <pthread.h>
    struct menber
    {
       int a;
       char *s;
    };
    void *thread1_fun(void *arg)
    {
       int *j;
       j=(int *)arg;    //必须进行强制类型转换
       while(1)
      {
         printf("this is thread_1\n");
         sleep(1);
      }
      printf("arg=%d\n",*j);
      return NULL;
    }
    void *thread2_fun(void *arg)
    {
       char *temp;
       temp=(char *)arg;  //必须进行强制类型转换
       while(1)
      {
          printf("this is thread_2\n");
          sleep(1);
      }
       printf("temp=%s\n",temp);
       return NULL;
    }
    void *thread3_fun(void *arg)
    {
       struct menber *p;
       p=(struct menber *)arg;
       printf("menber->a = %d  \n",p->a);
       printf("menber->s = %s  \n",p->s);
       return (void *)0;
    }
    int main(int argc,char *argv[])
    {
       int i=100;
       int err;
       pthread_t thread_1,thread_2,thread_3;
       void *tret1,*tret2,*tret3;
       char *str="hello world i love china!\n";
       struct menber b;
       b.a = 4;
       b.s = "hello embeded";
       err = pthread_create(&thread_1,NULL,thread1_fun,&i);  //创建线程,并指定到函数thread1_fun()来处理
       if(err!=0)
          printf("cannot create thread 1:%s\n",strerror(err));
   
       err = pthread_create(&thread_2,NULL,thread2_fun,str);
       if(err!=0)
          printf("cannot create thread 2:%s\n",strerror(err));
   
       err = pthread_create(&thread_3,NULL,thread3_fun,&b);
       if(err!=0)
          printf("cannot create thread 3:%s\n",strerror(err));
       //sleep(5);  
//线程必须依赖于进程,因此sleep(5)目的是使进程等待线程的结束.推荐使用pthread_join()函数来实现
//进程在5s后结束,因此也必须保证线程在5s后也结束
       err=pthread_join(thread_1,&tret1);
//tret1保存着线程1结束后的返回值.若不需要线程1的返回值,则err=pthread_join(thread_1,NULL);
//当进程创建线程后,系统会首先运行进程.
//阻塞函数.该函数可以使进程等待线程1运行完后,再运行进程.
       if(err!=0)
          printf("cannot join with thread 1:%s\n",strerror(err));
       printf("thread 1 exit code %d\n",(int)tret1);
  
       err=pthread_join(thread_2,&tret2);  ]
//tret2保存着线程2结束后的返回值
       if(err!=0)
          printf("cannot join with thread 1:%s\n",strerror(err));
       printf("thread 1 exit code %d\n",(int)tret2);
  
       err=pthread_join(thread_3,&tret3);  //tret3保存着线程3结束后的返回值
       if(err!=0)
          printf("cannot join with thread 1:%s\n",strerror(err));
       printf("thread 1 exit code %d\n",(int)tret3);
  
       if(pthread_cancel(thread_1)!=0)  //在进程中终止线程1
          perror("phread cancel");
  
    return 0;
  }
 [root@localhost lishuai]# gcc thread.c -o thread -Wall -O2 -g -lpthread
 [root@localhost lishuai]# ./thread
 
三、线程同步
    线程的同步, 发生在多个线程共享相同内存的时候, 这时要保证每个线程在每个时刻看到的共享数据是一致的. 如果每个线程使用的变量都是其他线程不会使用的(read & write), 或者变量是只读的, 就不存在一致性问题. 但如果两个或两个以上的线程可以read/write一个变量时, 就需要对线程进行同步, 以确保它们在访问该变量时, 不会得到无效的值, 同时也可以唯一地修改该变量并使它生效.
    在linux下常用的线程同步方法有互斥锁和信号量.
(1)互斥锁
    互斥锁有两种状态,分别是lock和unlock,它确保同一时刻只有一个线程可以访问数据,它用一种使用加锁方法来控制对共享资源的存取.同一时刻只能有一个线程掌握某个互斥资源的锁,该线程能对共享资源进行操作.若其它线程想上锁一个已经上了锁的互斥资源,则该线程就会挂起,直到上锁的线程是释放那个互斥锁为止.
    互斥锁分静态分配的互斥锁和动态分配的互斥锁.
    当使用宏定义PTHREAD_MUTEX_INITIALIZER时,采用的是静态分配的互斥锁.
    当使用用户自定义的pthread_mutex_t类型的变量时,采用的是动态分配的互斥锁.
1、互斥锁的初始化
    #include <pthread.h>
    int pthread_mutex_init(pthread_mutex_init *mutex,const pthread_mutexattr)
    函数功能:初始化一个互斥锁
    参数1:互斥锁变量,且该变量是pthread_mutex_init数据类型的.
    参数2:互斥锁变量的属性.若以默认属性初始化互斥锁,则该参数为NULL.
2、清除互斥锁
    int pthread_mutex_destroy(pthread_mutex_t *mutex)
    函数功能:清除用户自定义的互斥锁
    参数:互斥锁变量
3、对互斥锁进行加锁
    int pthread_mutex_lock(pthread_mutex_t *mutex)
    函数功能:对一个已经存在的互斥锁进行加锁.互斥锁不加锁并不能阻止其它线程的访问.互斥锁被加锁后,其它线程将会阻塞至该函数处.
    参数:互斥锁变量
4、对互斥锁进行尝试加锁
    int pthread_mutex_trylock(pthread_mutex_t *mutex)
    函数功能:对互斥锁进行尝试加锁
    参数:互斥锁变量
    Attention!!!
    该函数与pthread_mutex_lock()的区别在于后者对互斥锁进行尝试加锁,若此时其它线程已经加锁,则该线程将无法加锁,但该线程并不会阻塞在该函数处.
5、对互斥锁进行解锁
    int pthread_mutex_unlock(pthread_mutex_t *mutex)
    函数功能:对互斥锁进行解锁
    参数:互斥锁变量
例程:模拟智能打印机
    若一台打印机同一时刻有N(N>1)个人同时需要打印,系统会创建N个线程分别处理,且CPU会给每个线程分配一定的时间,这样,CPU会分配一点时间给线程1,再分配一点时间给线程2...,最终结果是每个人打印一点,这样的结果并不是我们想要的.而采用互斥锁可以避免,因为若第1个线程率先被执行,该线程会锁住打印机,系统会阻止其它线程访问打印机,且阻塞在函数pthread_mutex_lock()处,直到第一个线程执行完毕,解锁后,第二个线程才可以被执行...
    mutex_printer.c
    pthread_mutex_t mutex1;
    void PRINT(char *str) //哪个线程先执行,则哪个线程先加锁
    {
       pthread_mutex_lock(&mutex1);  //线程2会阻塞在此,等待线程1的解锁
       char * temp=str;
       while(*temp!='\0')
       {
          printf("%c",*temp);
          fflush(stdout);
          sleep(1);
          temp++;
       }
       pthread_mutex_unlock(&mutex1);
    }
  //若线程1率先被执行,则该线程会锁住打印机,阻止线程2的访问,且线程2会阻塞在函数pthread_mutex_lock()处.
  //直到线程1执行完毕,打开互斥锁,这样线程2才可以访问打印机.
    void *thr_fun1(void * arg) 
   {
       PRINT("hello world\n"); 
       return NULL;
   }
   void *thr_fun2(void * arg)
  {
       PRINT("i love China!!!\n"); 
       return NULL;
  }
   int main(int argc, char *rgv[])
  {
       int err;
       pthread_t tid1,tid2;
       err=pthread_mutex_init(&mutex1,NULL);
       if(err!=0)
          perror("pthread_mutex_init");
       err=pthread_create(&tid1,NULL,thr_fun1,NULL);
       if(err!=0)
          perror("pthread_create tid1");
       err=pthread_create(&tid2,NULL,thr_fun2,NULL);
       if(err!=0)
          perror("pthread_create tid2");

       err=pthread_join(tid1,NULL);
       if(err!=0)
          perror("pthread_join tid1");
       err=pthread_join(tid2,NULL);
       if(err!=0)
          perror("pthread_join tid2 ");
       err=pthread_mutex_destroy(&mutex1);  //清除用户自定义的互斥锁
       if(err!=0)
         perror("pthread_mutex_destroy ");
       return 0;
   }
   [root@localhost lishuai]# gcc mutex_printer.c -o mutex_printer -Wall -O2 -g -lpthread
(2)信号量
   信号量详见博客"信号量".