Linux下线程概述

来源:互联网 发布:js 上传图片 编辑:程序博客网 时间:2024/05/02 01:35

1.1、线程和进程的关系     

         线程是计算机中独立运行的最小单位,运行时占用很少的系统资源,每个线程占用的CPU时间是由系统分配的,因此可以把线程看成是操作系统分配CPU时间的基本单位。在用户看来,多个线程是同时执行的,但从操作系统的调度上看,各个线程是交替执行的。系统不停在各个线程之间切换,每个线程只有在系统分配给它的时间片内才能取得CPU的控制权,执行线程中的代码。

        注意:这里只是单CPU单核的情况,在多CPU多核的主机上,多个线程是可以同时运行的。

        Linux系统是支持多线程的,它在一个进程内生成了许多个线程,一个进程可以拥有一至多个线程。那么为什么在支持多进程的情况下又要引入多线程呢?这是因为多线程相对于多进程有以下优点:

  • 在多进程情况下,每个进程都有自己独立的地址空间,而在多线程情况下,同一进程内的线程共享进程的地址空间。因此,进程的资源开销要比线程大得多
  • 系统调度方面,由于进程地址空间独立而线程共享同一个进程的地址空间,线程间的切换速度要远快于进程间的切换速度
  • 通信机制方面,进程间的数据空间相互独立,彼此通信要用专门的通信方式进行,通信时必须通过操作系统。而同一进程内的多个线程共享数据空间,一个线程的数据可以直接提供给另一个线程使用,而不必经过操作系统。因此,线程间的通信更加方便和省时。
        以上线程的优点可以用2个字概括:“节约”:节约资源、节约时间。这些对操作系统的设计来说是非常重要的,线程还有以下优点:
  • 提高应用程序的响应速度,在图形界面程序中,如果有一个非常耗时的操作,它会导致其它操作不能进行而等待,这时界面响应用户的操作速度就会变得很慢。多线程环境下,可以将这个非常耗时的操作放到一个单独的线程中来完成
  • 可以提高多处理器效率
  • 可以改善程序的结构,对于要处理多个命令的应用程序,可以将对每个命令的处理设计为一个线程,从而避免设计大程序时,造成的程序结构复杂
        虽然线程共享进程的地址空间、打开的文件描述符等资源,但是线程也有自己私有的数据信息,包括:
  • 线程号(Thread ID):每个线程都有一个唯一的线程ID号
  • 寄存器:包括程序计数器和堆栈指针
  • 堆栈
  • 信号掩码
  • 优先级
  • 线程私有的存储空间
        Linux支持POSIX多线程接口,称为pthread(Posix Thread的简称)。编写Linux下的多线程应用程序,需要使用头文件pthread.h,链接时需要使用库pthread.a.
1.2、创建线程
1.2.1、线程创建函数pthread_create
        如果在主线程里面创建线程,程序就会在创建线程的地方产生分支,变成2个分支执行,这似乎和多进程一样,其实不然,子进程是通过拷贝父进程的地址空间来实现的;而线程与进程内的线程共享程序代码,一段代码可以同时被多个线程执行。
        线程的创建通过函数pthread_create来完成,该函数的声明如下:
#include <pthread.h>int pthread_create(pthread_t *thread,pthread_attr_t *attr, void* (*start_routine)(void*), void* arg);
参数含义如下:
  • thread:该参数是一个指针,当线程创建成功时,用来返回创建的线程ID
  • attr:该参数用于指定线程的属性,NULL表示使用默认属性
  • start_routine:该参数为一个函数指针,指向线程创建后要调用的函数。这个被线程调用的函数也称为线程函数
  • arg:该参数指向传递给线程函数的参数
注意:线程创建成功时,pthread_create函数返回0,若不为0则说明线程创建失败,常见的错误码为EAGAIN和EINVAL。前者表示系统限制创建新的线程,例如:线程数目过多;后者表示第二个参数代表的线程属性值非法。线程创建成功后,创建的线程开始运行第三个参数所指向的函数,原来的线程继续运行。
pthread_t pthread_self(void);//获取本线程的线程IDint pthread_equal(pthread_t thread1, pthread_t thread2);//判断2个线程的ID是否指向同一个线程int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));//用来保证init_routine线程函数在进程中仅执行一次
线程创建过程示例代码:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>void* thread(void* arg){pthread_t newthid;printf("this is new thread, thread id = %x\n", pthread_self());return NULL;}int main(void){pthread_t thid;printf("main thread ID is %x\n", pthread_self());//main thread idif(pthread_create(&thid, NULL, (void*)thread, NULL) != 0){printf("thread creation failed.\n");exit(1);}sleep(1);return 0;}
运行结果如下:

说明:程序首先打印出主线程的ID,然后打印新创建的线程ID。
        有些情况下,函数执行次数被限制为一次,这种情况下要使用pthread_once函数,下面例子说明了该函数的用法:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <pthread.h>pthread_once_t once = PTHREAD_ONCE_INIT;void run(){printf("Function run is running in thread %u\n", pthread_self());}void* thread1(void* arg){pthread_t thid = pthread_self();printf("Current thread's ID is %x\n", thid);pthread_once(&once,run);printf("thread1 ends\n");}void* thread2(void* arg){pthread_t thid = pthread_self();printf("Current thread's ID is %x\n", thid);pthread_once(&once,run);printf("thread2 ends\n");}int main(void){pthread_t thid1,thid2;pthread_create(&thid1, NULL, thread1, NULL);pthread_create(&thid2, NULL, thread2, NULL);printf("main thread exit.\n");//main thread idsleep(3);return 0;}
函数执行结果如下:

从执行结果可以看出来,线程函数执行了一次。
2.2.2、线程属性
        线程创建函数pthread_create有一个参数类型为pthread_attr_t,该结构定义如下:
typedef struct{        int                datachstate;        int                schedpolicy;        struct sched_param schedparam;        int                inheritsched;        int                scope;        size_t             quardsize;        int                stackaddr_set;        void*              stackaddr;        size_t             stacksize}pthread_attr_t;
各个字段的含义如下:
  • datachstate:表示新创建的线程是否与进程中的其它线程脱离同步。datachstate的缺省值为PTHREAD_CREATE_JOINABLE状态,这个属性也可以用函数pthread_detach()来设置。如果将datachstate设置为PTHREAD_CREATE_DETACH状态,则datachstate不能再恢复到PTHREAD_CREATE_JOINABLE状态。
  • schedpolicy:表示新线程的调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)、SCHED_FIFO(实时、先入先出)这三种,缺省为SCHED_OTHER(正常、非实时),后两种调度策略仅对超级用户有效。
  • schedparam:一个struct sched_param结构体,其中有一个sched_priority成员,表示线程的运行优先级。这个参数仅当调度策略为实时(SCHED_RR或SCHED_FIFO)时才有效,缺省为0.
  • inheritsched:有两种值可供选择,PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程显式指定调度策略和调度参数(即attr中的值),后者表示继承调用者线程的值,缺省为PTHREAD_EXPLICIT_SCHED.
  • scope:表示线程间竞争CPU的范围,也就是说,线程优先级的有效范围,POSIX标准中定义了两个值,PTHREAD_SCOPE_SYSTEM:表示与系统中所有线程一起竞争CPU。PTHREAD_SCOPE_PROCESS:表示仅与同进程中的线程竞争CPU。
  • guardsize:警戒堆栈的大小
  • stackaddr_set:堆栈地址集
  • stacksize:堆栈的大小
2.3、线程终止
    Linux下有2种方式可以使线程终止,一种是return从线程函数直接返回,另一种是通过调用函数pthread_exit()使线程退出。pthread_exit()在头文件pthread.h中声明,该函数的原型如下:
#include <pthread.h>void pthread_exit(void* retval);
    有2种情况需要注意:一种情况是:在主线程中,如果从main函数返回或是调用了exit函数退出主线程,则整个进程终止,此时进程中的所有线程也将终止。因此在主线程中不能过早的从main函数返回;另一种情况是主线程调用pthread_exit函数,则仅仅是主线程消亡,进程不会结束,进程内的其它线程也不会终止,直到所有线程结束,进程才会结束。
    线程终止最重要的问题是资源释放的问题,特别是一些临界资源。临界资源在一段时间内只能被一个线程所持有,当线程需要使用临界资源时提出需求,如果该资源未被使用则申请成功,否则等待。临界资源使用完毕后要释放以便其它线程可以使用。例如,某线程要写一个文件,在该线程写文件时不允许其它线程对该文件进行写操作,否则会导致文件数据混乱,这里的文件就是一种临界资源,临界资源为一个线程所独占,当一个线程终止时如果不释放占用的临界资源,其它需要使用该资源的线程可能就会一直等待下去,这就形成了死锁,而往往这是灾难性的。
    为此,Linux系统提供了一对函数pthread_cleanup_push()和pthread_cleanup_pop()用于自动释放资源。从pthread_cleanup_push()的调用点到pthread_cleanup_pop()之间的程序段中的终止动作(如调用pthread_exit函数)都将执行pthread_cleanup_push()所指定的清理函数,这两个函数是以宏的形式提供的。
   注意pthread_cleanup_push()带有一个“{”,而pthread_cleanup_pop()带有一个“}”,因此这两个函数必须成对出现,而且必须位于程序的同一代码段中才能通过编译。
    线程终止的另外一个需要注意的问题是线程间的同步问题。一般情况下,进程中各个线程的运行是相互独立的,线程的终止并不会相互通知,也不会影响其它线程,终止的线程所占用的资源也不会随着线程的终止而归还系统,而是仍为线程所在的进程所持有。正如进程之间可以使用wait系统函数来等待其它进程结束一样,线程也有类似的函数,pthread_join()函数,该函数在pthread.h中声明:
#include <pthread.h>void pthread_exit(void* retval);int pthread_join(pthread_t th, void* thread_return);int pthread_detach(pthread_t th);
    函数pthread_join用来等待一个线程的结束。pthread_join的调用线程将被挂起并等待th线程终止,如果thread_return不为NULL,则*thread_return=retval。需要注意的是,一个线程仅允许一个线程使用pthread_join等待它的终止,并且被等待的线程处于可join状态,即非DETACHED状态。DETACHED状态是指对某个线程执行pthread_detach后所处的状态,处于DETACHED状态的线程无法由pthread_join同步。
    一个可join的线程所占用的内存仅当有线程对其执行了pthread_join后才会释放,因此为了避免内存泄露,所有的线程终止时,要么已被设为DETACHED,要么使用pthread_join来回收资源。
    注意:一个线程不能被多个线程等待,否则第一个接收到信号的线程成功返回,其余调用pthread_join的线程返回错误代码ESRCH.
示例代码如下:
#include <stdio.h>#include <pthread.h>void* assisthread(void* arg){printf("I am helping to do some jobs\n");sleep(3);pthread_exit(0);}int main(void){pthread_t assistthid;int status;pthread_create(&assistthid, NULL, (void*) assisthread, NULL);pthread_join(assistthid, (void*)&status);printf("assisthread's exit is caused %d\n", status);return 0;}
运行结果如下;

        从运行结果可以看出pthread_join会阻塞主线程,等待assistthread结束。pthread_exit结束时的退出码为0,pthread_join通过参数获得的status也是0,两者是一致的。
2.4、私有数据
        在多线程环境下,进程内的所有线程共享进程的数据空间,因此全局变量为所有线程共有,在程序设计中有时需要保存线程自己的全局变量,这个特殊的变量仅在某个线程内部有效。如常见的变量errno,它返回标准的出错代码。errno不应该是一个局部变量,几乎每个函数都应该可以访问它,但它又不能作为一个全局变量,否则在一个线程里输出的很可能是另一个线程的出错信息。这个问题可以通过创建线程的私有数据(Thread-specific Data 或TSD)来解决。在线程内部,线程私有数据可以被各个函数访问,但它对其它线程都是屏蔽的。
    线程私有数据采用了一种被称为一键多值的技术,即一个键对应多个值。访问数据时都是通过键来访问,就像是对一个变量进行访问,其实是在访问不同的数据。使用线程私有数据时,首先要为每个线程数据创建一个相关联的键。在各个线程内部,使用这个公用的键来指代线程数据,但是在不同的线程中,这个键代表的数据是不同的。操作线程私有数据的函数主要有4个,声明如下:
#include <pthread.h>int pthread_key_create(pthread_key_t *key, void (*destr_function) (void*));//创建一个键int pthread_setspecific(pthread_key_t key, const void* pointer);//为一个键设置线程私有数据void* pthread_getspecific(pthread_key_t key);//从一个键读取线程私有数据int pthread_key_delete(pthread_key_t key);//删除一个键
  • pthread_key_create:从Linux的TSD池中分配一项,将其值赋给key供以后访问使用,它的第一个参数key为指向键值的指针,第二个参数为一个函数指针,如果指针不为空,则在线程退出时,将以key所关联的数据作为参数调用destr_function(),释放分配的缓冲区。key一旦被创建,所有线程都可以访问它,但各个线程可以根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量,一键多值。一键多值靠的是一个关键数据结构数组,机TSD池,其结构如下:
    static struct pthread_key_struct pthread_keys[PTHREAD_KEYS_MAX] = {(0,NULL)};
    创建一个TSD就相当于将结构数组中的某一项设置为“in_use”,并将其索引返回个*key,然后设置destructor函数为destr_function。
  • pthread_setspecific:该函数将pointer的值(不是内容)与key相关联,用pthread_setspecific为一个键指定新的线程数据时,线程必须先释放原有的线程数据用以回收空间
  • pthread_getspecific:该函数获得与key相关联的数据
  • pthread_key_delete:该寒水鱼用来删除一个键,删除后,键所占用的内存将被释放。需要注意的是,键占用的内存被释放,与键关联的线程数据所占用的内存并不释放,因此,线程数据的释放必须在释放键之前完成。
    如何创建和使用线程的私有数据,具体示例代码如下:
#include <stdio.h>#include <string.h>#include <pthread.h>pthread_key_t key;void* thread2(void* arg){int tsd = 5;printf("Thread2 start running.\n");pthread_setspecific(key, (void*)tsd);printf("Thread2 end running return tsd is %d\n", pthread_getspecific(key));}void* thread1(void* arg){int tsd = 0;pthread_t thid2;printf("Thread1 start running.\n");pthread_setspecific(key, (void*)tsd);pthread_create(&thid2, NULL, thread2, NULL);sleep(1);printf("Thread1 end running return tsd is:%d\n",pthread_getspecific(key));}int main(void){pthread_t thid1;printf("main thread begins running\n");pthread_key_create(&key, NULL);pthread_create(&thid1, NULL, thread1, NULL);sleep(3);pthread_key_delete(key);printf("main thread exit.\n");return 0;}
运行结果如下所示:

说明:程序中,主线程创建了线程thread1,线程thread1创建了线程thread2,两个线程分别将tsd作为线程私有数据,并有key做索引,从运行结果可以看出来,两个线程虽然用了同一个key,但是tsd数据并不相同。thread2先于thread1结束。
2.5、线程同步
        线程最大的特点就是资源的共享,然而资源共享中的同步问题是多线程编程的难点。Linux系统提供了多种方式处理线程间的同步问题,其中最常用的有互斥锁、条件变量、异步信号,下面主要介绍这3种同步技术的使用。
2.5.1、互斥锁
    互斥锁通过锁机制来实现线程间的同步,在同一时刻它通常只允许一个线程执行关键部分代码。下面互斥锁的几个常用函数,这些函数在pthread.h中定义:
pthread_mutex_init函数----------初始化一个互斥锁pthread_mutex_destroy函数-------注销一个互斥锁pthread_mutex_lock函数----------加锁,如果不成功,阻塞等待pthread_mutex_unlock函数--------解锁pthread_mutex_trylock函数-------测试加锁,如果不成功则立即返回,错误码为EBUSY
    使用互斥锁前必须进行初始化操作,初始化有2种方式,一种是静态赋值法,将宏结构常亮PTHREAD_MUTEX_INITIALIZER;赋给互斥锁,操作语句如下:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
另外一种方式是通过pthread_mutex_init函数初始化互斥锁,该函数原型如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
其中参数mutexattr表示互斥锁的属性,如果为NULL则使用默认属性,互斥锁的属性及意义,可查看该函数文档。
        初始化以后就可以使用互斥锁了,加锁有2个函数,pthread_mutex_lock和pthread_mutex_trylock().它们的函数原型如下:
int pthread_mutex_lock(pthread_mutex_t *mutex);int pthread_mutex_trylock(pthread_mutex_t * mutex);
      用pthread_mutex_lock加锁时,如果mutex已经被锁住,当前加锁的线程就会阻塞,直到互斥锁被其它线程释放。当pthread_mutex_lock函数返回时,说明互斥锁已经被当前线程成功加锁。pthread_mutex_trylock函数则不同,如果mutex已经被加锁,它将立即返回,返回的错误码为EBUSY,而不是阻塞等待。
      注意:加锁时不论哪种类型的锁,都不可能被两个不同的线程同时得到,其中一个必须等待解锁,在同一进程中的线程,如果加锁后没有解锁,则其它线程就无法再获得该锁。
      pthread_mutex_unlock用来解锁,函数原型如下:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
用该函数解锁时,要满足两个条件,一是互斥锁必须处于加锁状态,调用该函数的线程必须是给互斥锁加锁的线程,解锁后如果有其它线程在等待互斥锁,等待队列中的第一个线程将获得互斥锁。
当一个互斥锁使用完毕后,必须进行清除,清除互斥锁使用函数pthread_mutex_destroy,该函数的原型如下:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
清除一个互斥锁意味着释放它所占用的资源,清除锁时要求该锁处于开放状态,如果锁处于锁定状态,该函数返回EBUSY,该函数成功执行时返回0。由于在Linux中互斥锁并不占用内存,因此该函数除了只是清除互斥锁,并没有其它操作。
下面的代码展示了互斥锁的用法:
#include <stdio.h>#include <pthread.h>pthread_mutex_t number_mutex;intglobalnumber;void write_globalnumber(){pthread_mutex_lock(&number_mutex);globalnumber++;pthread_mutex_unlock(&number_mutex);}int read_globalnumber(){int temp;pthread_mutex_lock(&number_mutex);temp = globalnumber;pthread_mutex_unlock(&number_mutex);return temp;}
        上面代码中,两个函数对全局变量进行读写操作,write函数使用互斥锁保证在修改变量的时候操作一次执行完毕,不会中断。而read函数使用互斥锁保证在读取数据的时候,全局变量不会被修改,确保读到正确的数据。

2.5.2、条件变量
    条件变量是利用线程间共享的全局变量进行同步的一种机制。条件变量宏观上类似if语句,符合条件就能执行某段代码,否则只能等待条件成立。使用条件变量主要包括2个动作,一个等待使用资源的线程等待“条件变量被设置为真”;另一个线程在使用完资源后“设置条件为假”,这样就可以保证线程同步了。这种机制存在一个问题,就是要保证条件变量能被正确的修改,条件变量要受到特殊的保护,实际使用中互斥锁扮演着这样一个保护者的角色。Linux也提供了一系列对条件变量操作的函数,如下:
  • pthread_cond_init函数----------初始化条件变量
  • pthread_cond_wait函数----------基于条件变量阻塞,无条件等待
  • pthread_cond_timedwait函数-----阻塞直到指定事件发生,计时等待
  • pthread_cond_signal函数--------解除特定线程的阻塞,存在多个等待线程时按入队顺序激活其中一个
  • pthread_cond_broadcast函数-----解除所有线程阻塞
  • pthread_cond_destroy函数-------清除条件变量
与互斥锁一样,条件变量的初始化也有2种方式,一种是静态赋值法,将宏结构常量PTHREAD_COND_INITIALIZER赋给互斥锁,操作语句如下:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
另一种方式是使用函数pthread_cond_init,它的原型如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
其中cond_attr参数是条件变量的属性,由于其并没有得到实现,所以它的值通常是NULL。
等待条件成立有2个函数,函数原型如下:
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);int pthread_cond_timewait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec* abstime);
pthread_cond_wait函数释放由mutex指向的互斥锁,同时使当前线程关于cond指向的条件变量阻塞,直到条件信号被唤醒。通常条件表达式在互斥锁的保护下求值,






















0 0
原创粉丝点击