Linux下c语⾔多线程编程
引⾔
  线程(thread)技术早在60年代就被提出,但真正应⽤多线程到中去,是在80年代中期,solaris是这⽅⾯的佼佼者。传统的Unix也⽀持线程的概念,但是在⼀个进程(process)中只允许有⼀个线程,这样多线程就意味着多进程。现在,多
  为什么有了进程的概念后,还要再引⼊线程呢?使⽤多线程到底有哪些好处?什么的系统应该选⽤多线程?我们⾸先必须回答这些问题。
  使⽤多线程的理由之⼀是和进程相⽐,它是⼀种⾮常"节俭"的多任务操作⽅式。我们知道,在Linux系统下,启动⼀个新的进程必须分配给它独⽴的地址空间,建⽴众多的数据表来维护它的代码段、堆栈段和数据段,这是⼀种"昂贵"的多任务⼯作⽅式。⽽运⾏于⼀个进程中的多个线程,它们彼此之间使⽤相同的地址空间,共享⼤部分数据,启动⼀个线程所花费的空间远远⼩于启动⼀个进程所花费的空间,⽽且,线程间彼此切换所需的时间也远远⼩于进程间切换所需要的时间。
  使⽤多线程的理由之⼆是线程间⽅便的机制。对不同进程来说,它们具有独⽴的数据空间,要进⾏数据的传递只能通过通信的⽅式进⾏,这种⽅式不仅费时,⽽且很不⽅便。线程则不然,由于同⼀进程下的线
程之间共享数据空间,所以⼀个线程的数据可以直接为其它线程所⽤,这不仅快捷,⽽且⽅便。当然,数据的共享也带来其他⼀些问题,有的变量不能同时被两个线程所修改,有的⼦程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地⽅。
  除了以上所说的优点外,不和进程⽐较,多线程程序作为⼀种多任务、并发的⼯作⽅式,当然有以下的优点:
  1) 提⾼应⽤程序响应。这对图形界⾯的程序尤其有意义,当⼀个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应、、菜单的操作,⽽使⽤多线程技术,将耗时长的操作(time consuming)置于⼀个新的线程,可以避免这种尴尬的情况。
  2) 使多系统更加有效。操作系统会保证当线程数不⼤于CPU数⽬时,不同的线程运⾏于不同的CPU上。
  3) 改善程序结构。⼀个既长⼜复杂的进程可以考虑分为多个线程,成为⼏个独⽴或半独⽴的运⾏部分,这样的程序会利于理解和修改。
  下⾯我们先来尝试编写⼀个简单的多线程程序。
简单的多线程编程
  Linux系统下的多线程遵循POSIX线程接⼝,称为pthread。编写Linux下的多线程程序,需要使⽤头⽂件pthread.h,连接时需要使⽤库libpthread.a。顺便说⼀下,Linux下pthread的实现是通过系统调⽤clone()来实现的。clone()是Linux所特有的系统调⽤,它的使⽤⽅式类似fork,关于clone()的详细情况,有兴趣的读者可以去查看有关⽂档说明。下⾯我们展⽰⼀个最简单的多线程程序example1.c。
/* example.c*/
#include <stdio.h>
#include <pthread.h>
void thread(void)
{
  int i;
  for(i=0;i<3;i++)
    printf("This is a pthread.\n");
}
int main(void)
{
  pthread_t id;
  int i,ret;
  ret=pthread_create(&id,NULL,(void*) thread,NULL); // 成功返回0,错误返回错误编号
  if(ret!=0) {
    printf ("Create pthread error!\n");
    exit (1);
  }
  for(i=0;i<3;i++)
    printf("This is the main process.\n");
  pthread_join(id,NULL);
  return (0);
}
我们编译此程序:
gcc example1.c -lpthread -o example1
运⾏example1,我们得到如下结果:
This is the main process.
This is a pthread.
This is the main process.
This is the main process.
This is a pthread.
This is a pthread.
进程间通信和线程间通信的区别
再次运⾏,我们可能得到如下结果:
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
This is a pthread.
This is the main process.
前后两次结果不⼀样,这是两个线程争夺资源的结果。上⾯的⽰例中,我们使⽤到了两个函数,  pthread_create和pthread_join,并声明了⼀个pthread_t型的变量。
pthread_t在头⽂件/usr/include/bits/pthreadtypes.h中定义:
typedef unsigned long int pthread_t;
它是⼀个线程的标识符。函数pthread_create⽤来创建⼀个线程,它的原型为:
extern int pthread_create __P ((pthread_t *__thread, __const pthread_attr_t *__attr,
void *(*__start_routine) (void *), void *__arg));
第⼀个参数为指向线程标识符的指针,第⼆个参数⽤来设置线程属性,第三个参数是线程运⾏函数的起始地址,最后⼀个参数是运⾏函数的参数。这⾥,我们的函数thread不需要参数,所以最后⼀个参数设为空指针。第⼆个参数我们也设为空指针,这样将⽣成默认属性的线程。对线程属性的设定和修改我们将在下⼀节阐述。当创建线程成功时,函数返回0,若不为0则说明创建线程失败,常见的错误返回代码为EAGAIN和EINVAL。前者表⽰系统限制创建新的线程,例如线程数⽬过多了;后者表⽰第⼆个参数代表的线程属性值⾮法。创建线程成功后,新创建的线程则运⾏参数三和参数四确定的函数,原来的线程则继续运⾏下⼀⾏代码。
函数pthread_join⽤来等待⼀个线程的结束。函数原型为:
extern int pthread_join __P ((pthread_t __th, void **__thread_return));
第⼀个参数为被等待的线程标识符,第⼆个参数为⼀个⽤户定义的指针,它可以⽤来被等待线程的返回值。这个函数是⼀个线程阻塞的函数,调⽤它的函数将⼀直等待到被等待的线程结束为⽌,当函数返回时,被等待线程的资源被收回。⼀个线程的结束有两种途径,⼀种是象我们上⾯的例⼦⼀样,函数结束了,调⽤它的线程也就结束了;另⼀种⽅式是通过函数pthread_exit来实现。它的函数原型为:
  extern void pthread_exit __P ((void *__retval)) __attribute__ ((__noreturn__));
唯⼀的参数是函数的返回代码,只要pthread_join中的第⼆个参数thread_return不是NULL,这个值将被传递给thread_return。最后要说明的是,⼀个线程不能被多个线程等待,否则第⼀个接收到信号的线程成功返回,其余调⽤pthread_join的线程则返回错误代码ESRCH。
在这⼀节⾥,我们编写了⼀个最简单的线程,并掌握了最常⽤的三个函数pthread_create,pthread_join和pthread_exit。下⾯,我们来了解线程的⼀些常⽤属性以及如何设置这些属性。
修改线程的属性
  在上⼀节的例⼦⾥,我们⽤pthread_create函数创建了⼀个线程,在这个线程中,我们使⽤了默认参数,即将该函数的第⼆个参数设为NULL。的确,对⼤多数程序来说,使⽤默认属性就够了,但我们还是有必要来了解⼀下线程的有关属性。
  属性结构为pthread_attr_t,它同样在头⽂件/usr/include/pthread.h中定义,喜欢追根问底的⼈可以⾃⼰去查看。属性值不能直接设置,须使⽤相关函数进⾏操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调⽤。属性对象主要包括是否绑定、是否分离、堆栈地址、堆栈⼤⼩、优先级。默认的属性为⾮绑定、⾮分离、缺省1M的堆栈、与⽗进程同样级别的优先级。
  关于线程的绑定,牵涉到另外⼀个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于⽤户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,⼀个轻进程可以控制⼀个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为⾮绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在⼀个轻进程之上。被绑定的线程具有较⾼的响应速度,这是因为时间⽚的调度是⾯向轻进程的,绑定的线程可以保证在需要的时候它总有⼀个轻进程可⽤。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满⾜诸如实时反应之类的要求。
  设置线程绑定状态的函数为pthread_attr_setscope,它有两个参数,第⼀个是指向属性结构的指针,第⼆个是绑定类型,它有两个取值:PTHREAD_SCOPE_SYSTEM(绑定的)和PTHREAD_SCOPE_PROCESS(⾮绑定的)。下⾯的代码即创建了⼀个绑定的线程。
#include <pthread.h>
pthread_attr_t attr; pthread_t tid; /*初始化属性值,均设为默认值*/
pthread_attr_init(&attr);
pthread_attr_setscope(&attr, PTHREAD_SCOPE_SYSTEM);
pthread_create(&tid, &attr, (void*) my_function, NULL);
  线程的分离状态决定⼀个线程以什么样的⽅式来终⽌⾃⼰。在上⾯的例⼦中,我们采⽤了线程的默认属性,即为⾮分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终⽌,才能释放⾃⼰占⽤的系统资源。⽽分离线程不是这样⼦的,它没有被其他的线程所等待,⾃⼰运⾏结束了,线程也就终⽌了,马上释放系统资源。程序员应该根据⾃⼰的需要,选择适当的分离状态。设置线程分离状态的函数为pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)。第⼆个参数可选为PTHREAD_CREATE_DETACHED(分离线程)和 PTHREAD _CREATE_JOINABLE(⾮分离线程)。这⾥要注意的⼀点是,如果设置⼀个线程为分离线程,⽽这个线程运⾏⼜⾮常快,它很可能在pthread_create函数返回之前就终⽌了,它终⽌以后就可能将线程号和系统资源移交给其他的线程使⽤,这样调⽤pthread_create的线程就得到了错误的线程号。要避免这种情况可以采取⼀定的同步措施,最简单的⽅法之⼀是可以在被创建的线程⾥调⽤pthread_cond_timewait函数,让这个线程等待⼀会⼉,留出⾜够的时间让函数pthread_create返回。设
置⼀段等待时间,是在多线程编程⾥常⽤的⽅法。但是注意不要使⽤诸如wait()之类的函数,它们是使整个进程睡眠,并不能解决线程同步的问题。
  另外⼀个可能常⽤的属性是线程的优先级,它存放在结构sched_param中。⽤函数pthread_attr_getschedparam和函数
pthread_attr_setschedparam进⾏存放,⼀般说来,我们总是先取优先级,对取得的值修改后再存放回去。下⾯即是⼀段简单的例⼦。
#include <pthread.h>
#include <sched.h>
pthread_attr_t attr;
pthread_t tid;
sched_param param;
int newprio=20;
pthread_attr_init(&attr);
pthread_attr_getschedparam(&attr, ¶m);
param.sched_priority=newprio;
pthread_attr_setschedparam(&attr, ¶m);
pthread_create(&tid, &attr, (void*)myfunction, myarg);
线程的数据处理
  和进程相⽐,线程的最⼤优点之⼀是数据的共享性,各个进程共享⽗进程处沿袭的数据段,可以⽅便的获得、修改数据。但这也给多线程编程带来了许多问题。我们必须当⼼有多个不同的进程访问相同的变量。许多函数是不可重⼊的,即同时不能运⾏⼀个函数的多个拷贝(除⾮使⽤不同的数据段)。在函数中声明的静态变量常常带来问题,函数的返回值也会有问题。因为如果返回的是函数内部静态声明的空间的地址,则在⼀个线程调⽤该函数得到地址后使⽤该地址指向的数据时,别的线程可能调⽤此函数并修改了这⼀段数据。在进程中共享的变量必须⽤关键字volatile来定义,这是为了防⽌编译器在优化时(如gcc中使⽤-OX参数)改变它们的使⽤⽅式。为了保护变量,我们必须使⽤信号量、互斥等⽅法来保证我们对变量的正确使⽤。下⾯,我们就逐步介绍处理线程数据时的有关知识。
4.1 线程数据
  在单线程的程序⾥,有两种基本的数据:全局变量和局部变量。但在多线程程序⾥,还有第三种数据类型:线程数据(TSD: Thread-Specific Data)。它和全局变量很象,在线程内部,各个函数可以象使⽤全局变量⼀样调⽤它,但它对线程外部的其它线程是不可见的。这种数据的必要性是显⽽易见的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是⼀个局部变量,⼏乎每个函数都应该可以调⽤它;但它⼜不能是⼀个全局变量,否则在A线程⾥输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使⽤线程数据。我们为每个线程数据创建⼀个键,它和这个键相关联,在各个线程⾥,都使⽤这个键来指代线程数据,但在不同的线程⾥,这个键代表的数据是不同的,在同⼀个线程⾥,它代表同样的数据内容。
和线程数据相关的函数主要有4个:创建⼀个键;为⼀个键指定线程数据;从⼀个键读取线程数据;删除键。
创建键的函数原型为:
extern int pthread_key_create __P ((pthread_key_t *__key,
void (*__destr_function) (void *)));
  第⼀个参数为指向⼀个键值的指针,第⼆个参数指明了⼀个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调⽤这个函数来释放绑定在这个键上的块。这个函数常和函数pthread_once ((pthread_once_t*once_control, void (*initroutine) (void)))⼀起使⽤,为了让这个键只被创建⼀次。函数pthread_once声明⼀个初始化函数,第⼀次调⽤pthread_once时它执⾏这个函数,以后的调⽤将被它忽略。
  在下⾯的例⼦中,我们创建⼀个键,并将它和某个数据相关联。我们要定义⼀个函数createWindow,这个函数定义⼀个图形窗⼝(数据类型为Fl_Window *,这是图形界⾯开发⼯具FLTK中的数据类型)。由于各个线程都会调⽤这个函数,所以我们使⽤线程数据。
/* 声明⼀个键*/
pthread_key_t myWinKey;
/* 函数 createWindow */
  Fl_Window * win;
  static pthread_once_t once= PTHREAD_ONCE_INIT;
  /* 调⽤函数createMyKey,创建键*/
  pthread_once ( &once, createMyKey) ;
  /*win指向⼀个新建⽴的窗⼝*/
  win=new Fl_Window( 0, 0, 100, 100, "MyWindow");
  /* 对此窗⼝作⼀些可能的设置⼯作,如⼤⼩、位置、名称等*/
  setWindow(win);
  /* 将窗⼝指针值绑定在键myWinKey上*/
  pthread_setpecific ( myWinKey, win);
}
/* 函数 createMyKey,创建⼀个键,并指定了destructor */
void createMyKey ( void )
{
  pthread_keycreate(&myWinKey, freeWinKey);
}
/* 函数 freeWinKey,释放空间*/
void freeWinKey ( Fl_Window * win)
{
  delete win;
}
  这样,在不同的线程中调⽤函数createMyWin,都可以得到在线程内部均可见的窗⼝变量,这个变量通过函数pthread_getspecific得到。在上⾯的例⼦中,我们已经使⽤了函数pthread_setspecific来将线程数据和⼀个键绑定在⼀起。这两个函数的原型如下:
extern int pthread_setspecific __P ((pthread_key_t __key,__const void *__pointer));
extern void *pthread_getspecific __P ((pthread_key_t __key));
  这两个函数的参数意义和使⽤⽅法是显⽽易见的。要注意的是,⽤pthread_setspecific为⼀个键指定新的线程数据时,必须⾃⼰释放原有的线程数据以回收空间。这个过程函数pthread_key_delete⽤来删除⼀个键,这个键占⽤的内存将被释放,但同样要注意的是,它只释放键占⽤的内存,并不释放该键关联的线程数据所占⽤的内存资源,⽽且它也不会触发函数pthread_key_create中定义的destructor函数。线程数据的释放必须在释放键之前完成。
4.2 互斥锁
  互斥锁⽤来保证⼀段时间内只有⼀个线程在执⾏⼀段代码。必要性显⽽易见:假设各个线程向同⼀个⽂件顺序写⼊数据,最后得到的结果⼀定是灾难性的。
  我们先看下⾯⼀段代码。这是⼀个读/写程序,它们公⽤⼀个缓冲区,并且我们假定⼀个缓冲区只能保存⼀条信息。即缓冲区只有两个状态:有信息或没有信息。
void reader_function ( void );
void writer_function ( void );
char buffer;
int buffer_has_item=0;
pthread_mutex_t mutex;
struct timespec delay;
void main ( void )
{
  pthread_t reader;
  /* 定义延迟时间*/
  delay.tv_sec =2;
  delay.tv_nec =0;
  /* ⽤默认属性初始化⼀个互斥锁对象*/
  pthread_mutex_init (&mutex,NULL);
  pthread_create(&reader, pthread_attr_default, (void*)&reader_function), NULL);
  writer_function( );
}
  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,结构pthread_mutex_t为不公开的数据类型,其中包含⼀个系统分配的属性对象。函数
pthread_mutex_init⽤来⽣成⼀个互斥锁。NULL参数表明使⽤默认属性。如果需要声明特定属性的互斥锁,须调⽤函数
pthread_mutexattr_init。函数pthread_mutexattr_setpshared和函数pthread_mutexattr_settype⽤来设置互斥锁属性。前⼀个函数设置属性pshared,它有两个取值,PTHREAD_PROCESS_PRIVATE和PTHREAD_PROCESS_SHARED。前者⽤来不同进程中的线程同步,后者⽤于同步本进程的不同线程。在上⾯的例⼦中,我们使⽤的是默认属性PTHREAD_PROCESS_ PRIVATE。后者⽤来设置互斥锁
类型,可选的类型有PTHREAD_MUTEX_NORMAL、PTHREAD_MUTEX_ERRORCHECK、PTHREAD_MUTEX_RECURSIVE和PTHREAD _MUTEX_DEFAULT。它们分别定义了不同的上所、解锁机制,⼀般情况下,选⽤最后⼀个默认属性。
  pthread_mutex_lock声明开始⽤互斥锁上锁,此后的代码直⾄调⽤pthread_mutex_unlock为⽌,均被上锁,即同⼀时间只能被⼀个线程调⽤执⾏。当⼀个线程执⾏到pthread_mutex_lock处时,如果该锁此时被另⼀个线程使⽤,那此线程被阻塞,即程序将等待到另⼀个线程释放此互斥锁。在上⾯的例⼦中,我们使⽤了pthread_delay_np函数,让线程睡眠⼀段时间,就是为了防⽌⼀个线程始终占据此函数。(如果⽤sleep(2)则是让进程睡眠了)
  上⾯的例⼦⾮常简单,就不再介绍了,需要提出的是在使⽤互斥锁的过程中很有可能会出现死锁:两个线程试图同时占⽤两个资源,并按不同的次序锁定相应的互斥锁,例如两个线程都需要锁定互斥锁1和互斥锁2,a线程先锁定互斥锁1,b线程先锁定互斥锁2,这时就出现了死锁。此时我们可以使⽤函数pthread_mutex_trylock,它是函数pthread_mutex_lock的⾮阻塞版本,当它发现死锁不可避免时,它会返回相应的信息,程序员可以针对死锁做出相应的处理。另外不同的互斥锁类型对死锁的处理不⼀样,但最主要的还是要程序员⾃⼰在程序设计注意这⼀点。
4.3 条件变量
  前⼀节中我们讲述了如何使⽤互斥锁来实现线程间数据的共享和,互斥锁⼀个明显的缺点是它只有两种状态:锁定和⾮锁定。⽽条件变量通过允许线程阻塞和等待另⼀个线程发送信号的⽅法弥补了互斥锁的不⾜,它常和互斥锁⼀起使⽤。使⽤时,条件变量被⽤来阻塞⼀个线程,当条件不满⾜时,线程往往解开相应的互斥锁并等待条件发⽣变化。⼀旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒⼀个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满⾜。⼀般说来,条件变量被⽤来进⾏线承间的同步。
条件变量的结构为pthread_cond_t,函数pthread_cond_init()被⽤来初始化⼀个条件变量。它的原型为:
extern int pthread_cond_init __P ((pthread_cond_t *__cond,__const pthread_condattr_t *__cond_attr));
其中cond是⼀个指向结构pthread_cond_t的指针,cond_attr是⼀个指向结构pthread_condattr_t的指针。结构pthread_condattr_t是条件变量的属性结构,和互斥锁⼀样我们可以⽤它来设置条件变量是进程内可⽤还是进程间可⽤,默认值是PTHREAD_
PROCESS_PRIVATE,即此条件变量被同⼀进程内的各个线程使⽤。注意初始化条件变量只有未被使⽤时才能重新初始化或被释放。释放⼀个条件变量的函数为pthread_cond_ destroy(pthread_cond_t c
ond)。 

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。