linux 线程详解

线程

是计算机中独立运行的最小单位,运行时占用很少的系统资源。可以把线程看成是操作系统分配CPU时间的基本单元。一个进程可以拥有一个至多个线程。它线程在进程内部共享地址空间、打开的文件描述符等资源。同时线程也有其私有的数据信息,包括:线程号、寄存器(程序计数器和堆栈指针)、堆栈、信号掩码、优先级、线程私有存储空间。

为什么有了进程的概念后,还要再引入线程呢?使用多线程到底有哪些好处?什么的系统应该选用多线程?我们首先必须回答这些问题。
  使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

  使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击,这些正是编写多线程程序时最需要注意的地方。

  除了以上所说的优点外,不和进程比较,多线程程序作为一种多任务、并发的工作方式,当然有以下的优点:
  1) 提高应用程序响应。这对图形界面的程序尤其有意义,当一个操作耗时很长时,整个系统都会等待这个操作,此时程序不会响应键盘、鼠标、菜单的操作,而使用多线程技术,将耗时长的操作(time consuming)置于一个新的线程,可以避免这种尴尬的情况。
  2) 使多CPU系统更加有效。操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  3) 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。

创建线程

1 #include <pthread.h>
2 int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*start_routine)(void *),void *arg);
 pthread_t pthread_self(void);
 int pthread_equal(pthread_t thread1,pthread_t thread2);
 int pthread_once(pthread_once_t *once_control,void(*init_routine)(void));

linux系统支持POSIX多线程接口,称为pthread。编写linux下的多线程程序,需要包含头文件pthread.h,链接时需要使用库libpthread.a。

如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成两个部分执行。线程的创建通过函数pthread_create来完成。成功返回0

参数:

thread: 参数是一个指针,当线程成功创建时,返回创建线程ID。

attr: 用于指定线程的属性

start_routine: 该参数是一个函数指针,指向线程创建后要调用的函数。

arg: 传递给线程函数的参数。

一个简单的创建线程程序:

$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> int * thread(void *arg)
{
printf("thread id is %d.\n",pthread_self());
return NULL;
} int main()
{
pthread_t id;
printf("Main thread id is %d \n",pthread_self());
if(!pthread_create(&id,NULL,(void *)thread,NULL))
{
printf("succeed!\n");
return 0;
}
else
{printf("Fail to Create Thread");
return -1;
}
}
$ ./main
Main thread id is 1
succeed!
thread id is 2.
 

线程终止

两种方式终止线程。

第一通过return从线程函数返回,

第二种通过调用pthread_exit()函数使线程退出。

需要注意的地方:一是,主线程中如果从main函数返回或是调用了exit函数退出主线程,则整个进程终止,此时所有的其他线程也将终止。另一种是,如果主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,其他线程也不会结束,知道所有的线程都结束时,进程才结束。

线程属性

1 /* man pthread_attr_init */
2 typedef struct
3 {
4   int                    detachstate;    //是否与其他线程脱离同步
5   int                    schedpolicy;    //新线程的调度策略
6   struct sched_param    schedparam;        //运行优先级等
7   int                    inheritsched;    //是否继承调用者线程的值
8   int                    scope;            //线程竞争CPU的范围(优先级的范围)
9   size_t                guardsize;        //警戒堆栈的大小
10   int                    stackaddr_set;    //堆栈地址集
11   void *                stackaddr;        //堆栈地址
12   size_t                stacksize;        //堆栈大小
13 } pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化的函数为pthread_attr_init,这个函数必须在pthread_create函数之前调用。

关于线程的绑定,牵涉到另外一个概念:轻进程(LWP:Light Weight Process)。轻进程可以理解为内核线程,它位于用户层和系统层之间。系统对线程资源的分配、对线程的控制是通过轻进程来实现的,一个轻进程可以控制一个或多个线程。默认状况下,启动多少轻进程、哪些轻进程来控制哪些线程是由系统来控制的,这种状况即称为非绑定的。绑定状况下,则顾名思义,即某个线程固定的"绑"在一个轻进程之上。被绑定的线程具有较高的响应速度,这是因为CPU时间片的调度是面向轻进程的,绑定的线程可以保证在需要的时候它总有一个轻进程可用。通过设置被绑定的轻进程的优先级和调度级可以使得绑定的线程满足诸如实时反应之类的要求。
  设置线程绑定状态的函数为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进行存放,一般说来,我们总是先取优先级,对取得的值修改后再存放回去。

线程等待——正确处理线程终止

1 #include <pthread.h>
2 void pthread_exit(void *retval);
3 void pthread_join(pthread_t th,void *thread_return);       //挂起等待th结束,*thread_return=retval;
4 int pthread_detach(pthread_t th);

线程只能被一个线程等待终止(第一个能正常返回),并且应处于join状态(非DETACHED)。

在 Linux 平台下,当处理线程结束时需要注意的一个问题就是如何让一个线程善始善终,让其所占资源得到正确释放。在 Linux 平台默认情况下,虽然各个线程之间是相互独立的,一个线程的终止不会去通知或影响其他的线程。但是已经终止的线程的资源并不会随着线程的终止而得到释放,我们需要调用 pthread_join() 来获得另一个线程的终止状态并且释放该线程所占的资源。

调用该函数的线程将挂起,等待 th 所表示的线程的结束。 thread_return 是指向线程 th 返回值的指针。需要注意的是 th 所表示的线程必须是 joinable 的,即处于非 detached(游离)状态;并且只可以有唯一的一个线程对 th 调用 pthread_join() 。如果 th 处于 detached 状态,那么对 th 的 pthread_join() 调用将返回错误。

如果你压根儿不关心一个线程的结束状态,那么也可以将一个线程设置为 detached 状态,从而来让操作系统在该线程结束时来回收它所占的资源。将一个线程设置为 detached 状态可以通过两种方式来实现。一种是调用 pthread_detach() 函数,可以将线程 th 设置为 detached 状态。其申明如清单 10 。

另一种方法是在创建线程时就将它设置为 detached 状态,首先初始化一个线程属性变量,然后将其设置为 detached 状态,最后将它作为参数传入线程创建函数 pthread_create(),这样所创建出来的线程就直接处于 detached 状态。方法如清单 11 。

     创建 detach 线程:

pthread_t       tid; 
    pthread_attr_t  attr; 
    pthread_attr_init(&attr); 
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); 
    pthread_create(&tid, &attr, THREAD_FUNCTION, arg);

总之为了在使用 Pthread 时避免线程的资源在线程结束时不能得到正确释放,从而避免产生潜在的内存泄漏问题,在对待线程结束时,要确保该线程处于 detached 状态,否着就需要调用 pthread_join() 函数来对其进行资源回收。

线程私有数据

进程内的所有线程共享进程的数据空间,因此全局变量为所有线程所共有。但有时线程也需要保存自己的私有数据,这时可以创建线程私有数据(Thread-specific Date)TSD来解决。在线程内部,私有数据可以被各个函数访问,但对其他线程是屏蔽的。例如我们常见的变量errno,它返回标准的出错信息。它显然不能是一个局部变量,几乎每个函数都应该可以调用它;但它又不能是一个全局变量,否则在A线程里输出的很可能是B线程的出错信息。要实现诸如此类的变量,我们就必须使用线程数据。我们为每个线程数据创建一个键,它和这个键相关联,在各个线程里,都使用这个键来指代线程数据,但在不同的线程里,这个键代表的数据是不同的,在同一个线程里,它代表同样的数据内容。

线程私有数据采用了一键多值的技术,即一个键对应多个数值,访问数据时好像是对同一个变量进行访问,但其实是在访问不同的数据。

创建私有数据的函数有4个:pthread_key_create(创建), pthread_setspecific(设置), pthread_getspecific(获取), pthread_key_delete(删除)。

1 #include <pthread.h>
2 int pthread_key_creadte(pthread_key_t *key,void (*destr_fuction) (void *));
3 int pthread_setspecific(pthread_key_t key,const void * pointer));
4 void * pthread_getspecific(pthread_key_t key);
5 int pthread_key_delete(ptherad_key_t key);

线程同步

线程的最大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和异步信号。

1)互斥锁(mutex)

通过锁机制实现线程间的同步。同一时刻只允许一个线程执行一个关键部分的代码。

1 int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutex_attr_t *mutexattr);
2 int pthread_mutex_lock(pthread_mutex *mutex);
3 int pthread_mutex_destroy(pthread_mutex *mutex);
4 int pthread_mutex_unlock(pthread_mutex *
      (1)先初始化锁init()或静态赋值pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIER
      attr_t有:
      PTHREAD_MUTEX_TIMED_NP:其余线程等待队列
      PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,允许线程多次加锁,不同线程,解锁后重新竞争
      PTHREAD_MUTEX_ERRORCHECK_NP:检错,与一同,线程请求已用锁,返回EDEADLK;
      PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,解锁后重新竞争
      (2)加锁,lock,trylock,lock阻塞等待锁,trylock立即返回EBUSY
      (3)解锁,unlock需满足是加锁状态,且由加锁线程解锁
      (4)清除锁,destroy(此时锁必需unlock,否则返回EBUSY,//Linux下互斥锁不占用资源内存
$ cat main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
int a; int * thread(void *arg)
{
printf("thread id is %d.\n",pthread_self());
pthread_mutex_lock(&mutex);
a=10;
printf("a changed to %d.\n",a);
pthread_mutex_unlock(&mutex);
return NULL;
} int main()
{
pthread_t id;
printf("Main thread id is %d \n",pthread_self());
a=3;
printf("In main func a=%d\n",a); if(!pthread_create(&id,NULL,(void *)thread,NULL))
{
{
printf("Create thread succeed!\n");
}
else
{printf("Fail to Create Thread");
return -1;
}
pthread_join(&id,NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
-bash-3.00$ ./main
Main thread id is 1
In main func a=3
Create thread succeed!
-bash-3.00$

2)条件变量(cond)

利用线程间共享的全局变量进行同步的一种机制。


1 int pthread_cond_init(pthread_cond_t *cond,pthread_condattr_t *cond_attr);     
2 int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
3 int pthread_cond_timewait(pthread_cond_t *cond,pthread_mutex *mutex,const timespec *abstime);
4 int pthread_cond_destroy(pthread_cond_t *cond);  
5 int pthread_cond_signal(pthread_cond_t *cond);
6 int pthread_cond_broadcast(pthread_cond_t *cond);  //解除所有线程的阻塞
      (1)初始化.init()或者pthread_cond_t cond=PTHREAD_COND_INITIALIER;属性置为NULL
      (2)等待条件成立.pthread_wait,pthread_timewait.wait()释放锁,并阻塞等待条件变量为真
      timewait()设置等待时间,仍未signal,返回ETIMEOUT(加锁保证只有一个线程wait)
      (3)激活条件变量:pthread_cond_signal,pthread_cond_broadcast(激活所有等待线程)
      (4)清除条件变量:destroy;无线程等待,否则返回EBUSY

对于int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond
pthread_mutex_t *mutex, const struct timespec *abstime);
一定要在mutex的锁定区域内使用。

如果要正确的使用pthread_mutex_lock与pthread_mutex_unlock,请参考
pthread_cleanup_push和pthread_cleanup_pop宏,它能够在线程被cancel的时候正确的释放mutex!

另外,posix1标准说,pthread_cond_signal与pthread_cond_broadcast无需考虑调用线程是否是mutex的拥有者,也就是所,可以在lock与unlock以外的区域调用。如果我们对调用行为不关心,那么请在lock区域之外调用吧。

1 #include <stdio.h>
2 #include <pthread.h>

4 pthread_mutex_t mutex;
5 pthread_cond_t cond;
6 void *thread1(void *arg)
7 {
8     pthread_cleanup_push(pthread_mutex_unlock,&mutex);
9     //提供函数回调保护
10     while(1){
11         printf("thread1 is running\n");
12         pthread_mutex_lock(&mutex);
13         pthread_cond_wait(&cond,&mutex);
14         printf("thread1 applied the condition\n");
15         pthread_mutex_unlock(&mutex);
16         sleep(4);
17     }
18     pthread_cleanup_pop(0);
19 }
20 
21 void *thread2(void *arg)
22 {
23     while(1){
24         printf("thread2 is running\n");
25         pthread_mutex_lock(&mutex);
26         pthread_cond_wait(&cond,&mutex);
27         printf("thread2 applied the condition\n");
28         pthread_mutex_unlock(&mutex);
29         sleep(1);
30     }
31 }
32 int main()
33 {
34     pthread_t thid1,thid2;
35     printf("condition variable study!\n");
36     pthread_mutex_init(&mutex,NULL);
37     pthread_cond_init(&cond,NULL);
38     pthread_create(&thid1,NULL,(void*)thread1,NULL);
39     pthread_create(&thid2,NULL,(void*)thread2,NULL);
40     do{
41         pthread_cond_signal(&cond);
42     }while(1);
43     sleep(20);
44     pthread_exit(0);
45     return 0;

3)信号量

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。

信号量函数的名字都以"sem_"打头。线程使用的基本信号量函数有四个。
#include <semaphore.h>
int sem_init (sem_t *sem , int pshared, unsigned int value);
这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。

两个原子操作函数:
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
这两个函数都要用一个由sem_init调用初始化的信号量对象的指针做参数。
sem_post:给信号量的值加1;
sem_wait:给信号量减1;对一个值为0的信号量调用sem_wait,这个函数将会等待直到有其它线程使它不再是0为止。

int sem_destroy(sem_t *sem);
这个函数的作用是再我们用完信号量后都它进行清理。归还自己占有的一切资源。

上一篇:2015年4月 非常干货之Python资源大全


下一篇:新手讲树:证明任意二叉树度为零的节点n0,永远比度为2的节点n2多1个