线程是比进程更小的单位,可以认为进程是由一个或多个线程组成的。线程是程序运行的通道, 操作系统通过线程, 按照一定顺序去逐步执行程序。
多线程编程有如下好处:
(1)、通过为每种事件类型的处理分配单独的线程,能够简化处理异步事件的代码。
(2)、多个线程自动地可以访问相同的存储地址空间和文件描述符。
(3)、有些问题可以通过将其分解从而改善整个程序的吞吐量。
(4)、 交互程序可以通过使用多线程实现响应时间的改善。
Linux下的线程实质上是轻量级进程(light weighted process),线程生成时会生成对应的进程控制结构,只是该结构与父线程的进程控制结构共享了同一个进程内存空间。 同时新线程的进程控制结构将从父线程(进程)处复制得到同样的进程信息,如打开文件列表和信号阻塞掩码等。创建线程比创建新进程成本低,因为新创建的线程使用的是当前进程的地址空间。相对于在进程之间切换,在线程之间进行切换所需的时间更少,因为后者不包括地址空间之间的切换。
线程创建
1.1 创建线程
POSIX通过pthread_create()函数创建线程,API定义如下:
int pthread_create(pthread_t *tidp, const pthread_attr_t *attr, void *(*start_rtn)(void), void *arg)
由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于定制各种不同的线程属性。与fork()调用创建一个进程不同,新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。同时,start_rtn可以返回一个void *类型的返回值,并由pthread_join()获取。
1.2 线程属性
pthread_create()函数中的attr参数是一个结构体指针,结构中的元素分别对应着新线程的运行属性。如果该参数传入NULL,则会创建如下缺省属性的线程:进程范围、非分离、缺省栈和缺省栈大小和零优先级。可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建的线程联系起来。可以使用pthread_attr_init()函数初始化该结构、使用pthread_attr_destroy()函数清除对该结构的初始化。
我们可以在创建一个线程时设置线程的属性,也可以在线程运行时更改这些属性。常见的线程属性主要包括以下几项:
分离状态,表示新线程是否与进程中其他线程脱离同步,如果置位则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。线程的分离状态决定了一个线程以什么样的方式来终止自己。缺省为PTHREAD_CREATE_JOINABLE状态。这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。这个属性也可以在线程创建并运行后用pthread_attr_setdetachstate()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。
调度策略,主要包括SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用pthread_setschedparam()来改变。
调度参数,存放在sched_param结构中,仅支持优先级参数。这个参数仅当调度策略为实时(即SCHED_RR或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。我们总是先取优先级,修改后再放回去。
堆栈空间,影响线程可以调用的函数数量。通常,线程栈是从页边界开始的。任何指定的大小都被向上舍入到下一个页边界。一般情况下,不需要为线程分配栈空间。系统会为每个线程的栈分配 1 MB(对于 32 位系统)或 2 MB(对于 64 位系统)的虚拟内存,而不保留任何交换空间。极少数情况下需要指定栈或栈大小。可以使用pthread_attr_setstacksize()和pthread_attr_setstack设置相关属性。
线程终止
如果进程中的任一线程调用了exit、_Exit或_exit,那么整个进程就会终止。与此类似,如果信号的默认动作是终止进程,那么,把该信号发送到线程会终止整个进程。单个线程可以通过下列三种方式退出而不终止整个进程:
1、 线程只是从启动例程中返回,返回值是线程的退出码。
2、 线程可以被同一进程中的其他线程取消。
3、 线程调用pthread_exit
2.1 线程取消
线程取消的方法是向目标线程发Cancel信号,但是目标线程可以自己选择怎样处理Cancel信号。下面是与线程取消相关的API:
int pthread_cancel(pthread_t thread)
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_setcancelstate(int state, int *oldstate)
设置本线程对Cancel信号的反应,state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int *oldtype)
设置本线程取消动作的执行时机,type有两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出);oldtype如果不为NULL则存入运来的取消动作类型值。
void pthread_testcancel(void)
检查本线程是否处于Canceld状态,如果是,则进行取消动作,否则直接返回。
2.2 pthread_join
void pthread_join(pthread_t thread, void **rval_ptr)
第一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止。如果线程只是从它的启动例程返回,rval_ptr将包含返回码;如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。
线程可以安排它退出时需要调用的函数,这样的函数称为线程清理处理程序。线程可以建立多个清理处理程序,处理程序记录在栈中,也就是说它们的执行顺序与它们注册时的顺序相反。相关API如下:
void pthread_cleanup_push(void (*rtn)(void *),void *arg)
void pthread_cleanup_pop(int execute)
线程同步
当多个线程同时读写同一份共享资源的时候,可能会引起冲突。线程同步其实就是“排队”:几个线程之间一个一个对共享资源进行操作。因此只有共享资源的读写访问才需要同步,只有变量才需要同步,如果变量是只读也不需要同步。
3.1 互斥量
互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。
3.1.1 创建和销毁
互斥变量用pthread_mutex_t数据类型表示,有两种方法创建互斥锁,静态方式和动态方式。静态方式就是把它置为常量PTHREAD_MUTEX_INTIALIZER,动态方式就是调用pthread_mutex_init函数进行初始化,API定义如下:
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
pthread_mutex_destroy()用于注销一个互斥锁,由于在Linux中,互斥锁并不占用任何资源,因此Linux中的pthread_mutex_destroy()除了检查锁状态以外(锁定状态则返回EBUSY)没有其他动作。
3.1.2 互斥量属性
互斥量的属性在创建互斥量的时候指定,值得注意的是类型属性。类型属性控制着互斥量的特性,POSIX定义了四种类型:
PTHREAD_MUTEX_NORMAL是标准的互斥量类型,并不做任何特殊的错误检查或死锁检测
PTHREAD_MUTEX_ERRORCHECK提供错误检查
PTHREAD_MUTEX_RECURSIVE允许同一线程在互斥量解锁之前对该互斥量进行多次加锁。
PTHREAD_MUTEXDEFAULT可以用于请求默认语义
3.1.2 信号量操作
信号量操作主要包括加锁pthread_mutex_lock()、解锁pthread_mutex_unlock()和测试加锁pthread_mutex_trylock()三个,不论哪种类型的锁,都不可能被两个不同的线程同时得到,而必须等待解锁。
int pthread_mutex_lock(pthread_mutex_t *mutex)
int pthread_mutex_unlock(pthread_mutex_t *mutex)
int pthread_mutex_trylock(pthread_mutex_t *mutex)
pthread_mutex_trylock()语义与pthread_mutex_lock()类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待。
3.2 条件变量
条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待'条件变量的条件成立'而挂起;另一个线程使'条件成立'(给出条件成立信号)。为了防止竞争,条件变量的使用总是和一个互斥锁结合在一起。
条件变量的标准使用方式:回调函数保护、等待条件前锁定、pthread_cond_wait()返回后解锁
由于pthread_cond_wait()和计时等待pthread_cond_timedwait()被实现为取消点,因此需要使用回调函数保护(即在加锁前调用pthread_cleanup_push(pthread_mutex_unlock,&mutex))。
3.2.1. 创建和销毁
条件变量和互斥锁一样,都有静态动态两种创建方式,静态方式使用PTHREAD_COND_INITIALIZER常量,如下:pthread_cond_t cond=PTHREAD_COND_INITIALIZER
动态方式调用pthread_cond_init()函数,API定义如下:
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
销毁一个条件变量需要调用pthread_cond_destroy(),只有在没有线程在该条件变量上等待的时候才能销毁这个条件变量,否则返回EBUSY。因为Linux实现的条件变量没有分配什么资源,所以注销动作只包括检查是否有等待线程。API定义如下:
int pthread_cond_destroy(pthread_cond_t *cond)
3.2.2. 等待和激发
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)
等待条件有两种方式:无条件等待pthread_cond_wait()和计时等待pthread_cond_timedwait(),其中计时等待方式如果在给定时刻前条件没有满足,则返回ETIMEOUT,结束等待,其中abstime以与time()系统调用相同意义的绝对时间形式出现。无论哪种等待方式,都必须和一个互斥锁配合,以防止多个线程同时请求pthread_cond_wait()(或pthread_cond_timedwait())的竞争条件。激发条件有两种形式,pthread_cond_signal()激活一个等待该条件的线程,存在多个等待线程时按入队顺序激活其中一个;而pthread_cond_broadcast()则激活所有等待线程。
3.3 读写锁
读写锁与互斥量类似,不过读写锁允许更高的并行性。读写锁有三种状态:读模式下加锁状态、写模式下加锁状态和不加锁状态。一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁。读写锁也叫共享——独占锁,当读写锁以读模式锁住时,它是以共享模式锁住的;当它以写模式锁住时,它是以独占模式锁住的。读写锁非常适合于对数据结构读的次数远大于写的情况。
3.3.1 创建和销毁
读写锁通过调用pthread_rwlock_init进行初始化,API定义如下:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)
销毁一个读写锁需要调用pthread_rwlock_destroy()。
3.3.2 读写锁操作
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
在实现读写锁的时候可能会对共享模式下可获取的锁的数量进行限制,所以需要检查pthread_rwlock_rdlock的返回值。
线程私有数据
线程私有数据是存储和查询与某个线程相关数据的一种机制。线程私有数据机制有个用处:第一、维护基于每个线程的数据;第二、提供让基于进程的接口适应多线程环境的机制。进程中的所有线程都可以访问进程的整个地址空间,除了使用寄存器外,线程没有办法阻止其他线程访问它的数据,线程私有数据也一样。虽然底层的实现部分并不能阻止这种访问能力,但管理线程私有数据的函数可以提高线程间的数据独立性。也就是说线程私有数据并不是真正的私有。
4.1 创建和注销
在分配线程私有数据之前,需要创建与该数据关联的键。由于自动分配存储空间对进程下的其他线程是不可见的,所以每个线程都可以用同一个键去关联一个自动分配的存储空间。
int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *))
创建的键存放在key指向的内存单元中,如果destr_function不为空,在线程退出(pthread_exit())时将以key所关联的数据为参数调用destr_function(),以释放分配的缓冲区。不论哪个线程调用pthread_key_create(),所创建的key都是所有线程可访问的,但各个线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量。
对所有的线程,都可以调用pthread_key_delete来取消键与线程私有数据值之间的关联关系。
int pthread_key_delete(pthread_key_t *key)
注意调用pthread_key_delete并不会激活与键相关联的析构函数。
4.2 访问
线程私有数据的读写都通过专门的Posix Thread函数进行,其API定义如下:
int pthread_setspecific(pthread_key_t key, const void *value)
void * pthread_getspecific(pthread_key_t key)
写入(pthread_setspecific())时,将value的值(不是所指的内容)与key相关联,而相应的读出函数则将与key相关联的数据读出来。数据类型都设为void *,因此可以指向任何类型的数据。
阅读(1894) | 评论(0) | 转发(0) |