线程与线程属性

  线程与线程属性

  这两章介绍了线程(thread)的概念,线程使用方法和线程的属性。线程是进程内部的一个执行序列,线程在调度时上下文切换快速且开销很小,更适用于嵌入式系统。平时项目中使用的平台基本上都是基于线程的,一般称之为task。
       一,线程概念

  线程是进程的细化,一个进程可以有多个线程,并且至少包含一个控制线程。进程是资源管理的最小单位,线程是程序执行的最小单位。进程利于管理数据和资源,而线程更利于调度切换和程序执行。

  在操作系统发展的过程中,为了提高程序执行的并发性,产生了进程的概念。进程包含很多资源,如代码,内存,打开的文件等等,因此进程切换需要耗费比较多的时间。为了进一步提高效率,演化出了线程的概念。

  线程有自己的栈空间,以及线程自身的基本信息,包括线程ID,一组寄存器,调度优先级策略,信号屏蔽字,errno变量以及线程私有数据。除此以外,线程共享所属进程的虚拟地址空间中所有内容,包括进程的代码,数据(全局变量),堆,信号句柄,文件描述符和当前目录状态。进程定义的全局变量,所有进程内线程都可以使用;进程打开的文件,所有进程内线程都可以访问。线程访问这些共享资源需要使用同步机制,书上讲到了几种同步机制。另外,如果使用多核处理器,线程将会被分配到不同的处理器上执行,实现真正的并行。

  二,线程控制

  1,线程ID

  线程ID的类型是pthread_t,这是一个结构(不同于进程ID是整数),所以不能直接判断两个线程ID是否相等。需要使用特定的函数。

  Int pthread_equal(pthread_t tid1, pthread_t tid2);

  相等返回非0,否则返回0。

  可以下面函数获得自身ID

  pthread_t pthread_immolation(void);

  2,创建线程

  使用下面的函数创建新线程

  int pthread_create(pthread_t *tidp, pthread_attr_t *attr, void *(*start_rtn)(void *), void *arg);

  不同于创建子进程,新线程从入口函数start_rtn处开始执行,这个和平时常用的创建task的接口类似。pthread_attr_t *attr是线程属性,填NULL时表示以默认属性创建线程(非分离状态)。

  3,结束线程

  有三种方式结束线程:

  (1)线程从入口函数中返回;

  (2)线程被同一进程中的其他线程取消;

  (3)线程调用pthread_exit。pthread_exit函数原型如下。

  Void pthread_exit(void *rval_ptr);

  参数rval_ptr是线程的返回码,进程中的其他线程可以通过pthread_join函数获取他。

  Int pthread_join(pthread_t thread, void **rval_ptr);

  调用此函数的线程将一直阻塞,直到指定线程结束,rval_ptr被设置为返回码。如果指定线程是被其他线程取消而结束的,rval_ptr会被设置为PTHREAD_CANCELED。如果只想等待线程结束,不想获取线程的终止状态,则可以把rval_ptr填NULL。

  pthread_join函数除了等待指定线程结束,获取结束码,同时还会释放指定线程占用的资源。但是如果线程已处于分离状态,线程结束后就会立即释放资源,其他线程对其调用pthread_join时会失败,返回EINVAL。默认情况下线程属于非分离状态。

  线程通过调用pthread_cancel函数来请求取消同一进程中的其他线程。

  Int pthread_cancel(pthread_t tid);

  这个函数只是提出请求,并不等待线程终止。而且通过设置,线程可以忽略此请求。默认情况下,线程收到取消请求后会在特定的取消点处终止。

  4,线程退出处理

  线程可以安排其终止时调用的清理函数,以执行善后操作。这个功能和进程的atexit函数有点类似。可以注册多个清理函数,他们都被记录在栈中,所以他们的执行顺序和注册顺序相反。使用下面两个函数实现清理函数的入栈和出栈功能。

  Void pthread_cleanup_push(void (*rtn)(void *), void *arg);

  Void pthread_cleanup_pop(int execute);

  这个两个函数是用宏实现的:

  #define pthread_cleanup_push(routine,arg) \

  { struct _pthread_cleanup_buffer _buffer; \

  _pthread_cleanup_push (&_buffer, (routine), (arg));

  #define pthread_cleanup_pop(execute) \

  _pthread_cleanup_pop (&_buffer, (execute)); }

  其中一个含有'{',另一个含有'}',所以这个函数必须成对出现,而且必须位于程序中同一级别的代码中,否则会出现编译错误。

  调用pthread_cleanup_pop时会将清理函数出栈,不过是否执行清理函数则需要看参数int execute。当execute为非0时清理函数会被调用执行,execute为0时清理函数只是出栈但不被调用。

  另外,当线程在pthread_cleanup_push和pthread_cleanup_pop之间的代码中退出终止时(调用pthread_exit或响应取消请求),所有注册的清理函数都将自动出栈并被调用执行。

  5,分离状态

  前面提到线程的分离状态,在分离状态下线程结束时会立即释放资源,如果其他线程对其调用pthread_join函数将会返回错误。
        调用pthread_detach函数将使线程进入分离状态。

  Int pthread_detach(pthread_t tid);

  注意,一旦线程进入分离状态,将无法再返回非分离状态。

        6,非分离状态
 
        默认属性情况下创建的线程是处于非分离状态的。处于非分离状态下的线程,终止后必须由其他线程调用pthread_join后释放资源,线程才会真正结束,这起到了保持线程同步的作用。

    三,线程同步

  线程可以共享进程的数据空间,所以必须使用同步和互斥机制。书上主要讲到了信号量,互斥量,读写锁和条件变量。

  1,信号量

  Unix有两组信号量接口函数,较早的一组信号量函数是用于进程间通信的,在系统V中就存在了。另外一组是POSIX实时扩展的用于线程间同步的信号量,本章讲到的就是这种信号量。

  通用的信号量是计数信号量,有一个信号量值,是有符号整型数。当信号量值大于零时常表示可用资源的数目,等于零时表示资源全部被占用,小于零时除了表示资源都被占用,另外,负数的绝对值还表示当前有多少线程正在等待此资源。比如假设系统中有5台打印机供所有线程使用,那么信号量的初值可以设为5,线程得到打印机时使信号量减1,释放打印机时使信号量加1。

  二进制信号量是计数信号量的子集,只有0和1两种取值,一般用于互斥和同步。用于互斥时,二进制信号量初始化为可用,一个线程得到信号量后,其他期望得到信号量的线程都将被阻塞,直至信号量被释放。用于同步时,信号量初始化为不可用,待某一时刻释放信号量,则等待的线程开始执行。例如中断服务程序在没有中断时不运行,一直在等待某个信号量,当中断产生时释放该信号量,则中断服务程序开始运行,起到了通知的作用。

  下面是linux线程信号量的接口函数。

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

  创建信号量,value是信号量初值,pshared是共享属性,目前Linux还不支持这个特性,只能赋0。 Int sem_wait(sem_t *sem); Int sem_trywait(sem_t *sem); Int sem_post(sem_t *sem); 获取和释放信号量,这些都是原子操作。其中sem_trywait也是获取信号量,不过如果当前信号量不可用,调用此函数的线程不会被阻塞。

  Int sem_destroy(sem_t *sem);

  删除信号量,并释放其资源。注意,如果当前信号量正在被占用,或者被wait,那么删除将会失败。所以,如果线程占用了信号量,却中途退出,那么必须在线程清理函数中释放这些信号量,否则信号量将永远不能被删除,资源得不到释放。

  2,互斥量

  互斥量的本质是一把锁,所以又叫互斥锁。在访问共享资源时先对互斥量加锁,访问结束后解锁。他其实是二进制信号量的特殊形式,实现二进制信号量的互斥功能。下面是接口函数。

  Int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);

  Int pthread_mutex_destroy(pthread_mutex *mutex);

  创建互斥量,删除互斥量。

  Int pthread_mutex_lock(pthread_mutex_t *mutex);

  Int pthread_mutex_trylock(pthread_mutex_t *mutex);

  Int pthread_mutex_unlock(pthread_mutex_t *mutex);

  加锁和解锁,pthread_mutex_trylock不阻塞调用线程。

  这些接口函数和信号量函数很相似,但是互斥量的使用更加严格。互斥量只能初始化为解锁状态,必须先加锁再解锁,而且必须是在同一个线程中加锁和解锁。书上还讲到了互斥量的属性。在创建互斥量函数中有pthread_mutexattr_t *attr变量,这就是互斥量的属性,填NULL时表示默认属性。

  互斥量有共享属性和类型属性这两种属性。使用pthread_mutexattr_setpshared函数设置共享属性,共享属性表示互斥量能否被其他进程共享,Linux中支持互斥量的进程间共享使用。

  互斥量有四种类型属性,书上列了一张表格,说明了非正常使用互斥量时四种属性的反应。其中有一种属性PTHREAD_MUTEX_RECURSIVE表示线程可以递归占用互斥量,同一线程可以多次调用加锁函数而不会出现死锁,但是释放时必须调用相同数目的解锁函数。最好不要使用递归加锁。

  互斥量在使用时,不能在两个不同的线程中分别加锁和解锁,不能对已解锁的互斥量再解锁,这些行为都将导致不可预料的后果。当系统中有很多互斥量时要小心出现死锁。如果线程之间对所有的信号量都按照相同的顺序加锁那么死锁将不会发生。如果不能对所有锁进行排序,那么可以使用pthread_mutex_trylock函数先测试锁,如果不能获取锁,可以先释放已占有的锁,过一段时间后再重新尝试加锁,这样可以避免出现死锁。

  3,读写锁

  读写锁和互斥量类似,不过分成了写模式锁和读模式锁。一次只能有一个线程可以占有写模式的锁,但可以有多个线程同时占有读模式的锁。就是对保护的资源不能同时写,不过可以同时读,这样避免了数据的不一致。

  当写模式锁被占用,所有读模式锁将不能被获取。当有读模式锁被占用,且写模式锁正在被申请获取时,其他读模式锁也将不能被获取。这是为了防止有太多的读请求时,写请求长时间得不到满足。

  读写锁适用于有很多线程需要读资源,只有少量线程需要写资源的情况。书上有读写锁的接口函数原型。

  4,条件变量

  条件变量是线程间的一种同步机制。一个或多个线程休眠并等待条件变为真。当条件变为真时,另一个线程通过发送信号的方式唤醒这些休眠等待的线程。这避免了线程需要不断的轮询条件。

  线程用pthread_cond_wait函数进入休眠状态,另一个线程在条件为真时发送信号,使用pthread_cond_signal函数。

  Int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

  Int pthread_cond_signal(pthread_cond_t *cond);

  这里的条件是某种全局数据,在一个线程中被修改,在其他若干等待线程中被检测,所以就需要对其使用某种形式的互斥。pthread_cond_wait函数中的互斥量mutex就是用作于此的。线程在调用pthread_cond_wait函数前首先占用锁,并检查条件,如果条件不为真,则进入函数。函数内会释放锁,并将线程放到等待条件的线程列表上,然后进入休眠状态。

  另一个线程想改变条件时,也需要事先占用锁。改变完成后再释放锁,然后以发送信号的方式通知等待条件的线程。等待线程接收到信号从pthread_cond_wait函数返回前又会占用锁,然后线程开始检测条件,检测后当然需要再释放锁。

  使用pthread_cond_signal函数发送信号,等待条件的线程队列中的第一个线程会收到此信号。还有一个函数是发送广播信号的,等待条件的所有线程都将收到信号,书上有此函数原型。

  四,线程属性

  1,线程创建属性

  线程创建函数pthread_create中的参数attr表示线程的属性,线程属性包含在一个结构pthread_attr_t中。其中主要包括下面几项。

  (1),_detachstate,表示线程的分离属性,缺省为PTHREAD_CREATE_JOINABLE表示非分离状态,而PTHREAD_CREATE_DETACH表示分离状态,之前提到在线程创建并运行后其他线程可以使用pthread_detach()函数使之进入分离状态。

  (2),_schedpolicy,表示线程的调度策略,SCHED_OTHER(普通,非实时),SCHED_RR(实时,轮转法),SCHED_FIFO(实时,先进先出)。缺省为SCHED_OTHER。可以调用pthread_attr_setschedpolicy()函数改变。

  (3),_schedparam,当线程是实时线程时,这个属性表示线程的优先级,缺省为0。可以调用pthread_attr_setschedparam()函数改变。

  (4),_inheritsched,表示新建的线程是否继承调用pthread_create函数的线程的调度策略和参数(即_schedpolicy和_schedparam属性)。PTHREAD_EXPLICIT_SCHED表示不继承,PTHREAD_INHERIT_SCHED表示继承。可以调用pthread_attr_setinheritsched()函数改变。

  (5),_scope,表示实时线程的优先级有效范围,目前Linux只支持实时线程与系统中的所有实时线程竞争cpu时间。

  另外线程属性还有线程的堆栈地址和大小,以上这些属性都各自有一套读取和设置的函数接口,太多了,这里没有写出来。线程属性的创建(并初始化)和删除用下面两个函数。

  Int pthread_attr_init(pthread_attr_t *attr);

  Int pthread_attr_destroy(pthread_attr_t *attr);

  2,其他属性

  还有一些属性并没有包含在pthread_attr_t结构中。前面提到,线程可以忽略其他线程提出的取消请求。通过下面函数实现。

  Int pthread_setcancelstate(int state, int *oldstate);

  设置为PTHREAD_CANCEL_ENABLE表示会响应其他线程提出的取消请求,设置为PTHREAD_CANCEL_DISABLE表示不响应取消请求。

  另外,还可以设置线程是立即响应取消请求退出线程,还是运行到取消点时再退出。用下面函数设置。

  Int pthread_setcanceltype(int type, int *oldtype);

  设置为PTHREAD_CANCEL_DEFERRED表示线程运行到取消点时被取消,设置为PTHREAD_CANCEL_ASYNCHRO
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值