Unix环境高级编程-11-线程

进程,线程模型

Unix环境高级编程-11-线程_第1张图片

线程对象

  • 线程对象拥有的域如下:

  • 线程标识,用于在一个进程中区分多个线程,所以线程标识只要求在一个进程内是唯一的即可.

    • pthread 中使用 pthread_t 来保存线程标识,

    • pthread_self(),获取当前线程的线程标识.

    • pthread_equal(tid1,tid2),用来比较2个线程标识是否相同.

  • 栈,每个线程都有自己的栈,见上.

  • 屏蔽信号集,每个线程都有自己的屏蔽信号集,此时进程对象中的屏蔽信号集被忽略.

  • 未决信号集,每个线程都有自己的未决信号集,此时进程对象中的未决信号集仍然有效,参见 sigpending().

线程创建

  • int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);用于创建一个线程,其中新线程的线程标识将存入 thread 指向的缓冲区中.各参数:

    • attr 用于指定新线程的线程属性,若 attr 为0,则表示新线程取默认属性.

    • start_routine,arg 新线程的入口函数,正如 main() 函数为主线程的入口函数一样.

  • 新线程的线程对象的属性大多数都是直接继承自调用线程(即调用 pthread_create() 的线程),除了:

    • 栈,

    • 未决信号集,在新线程中,未决信号集被清空.

线程属性

  • pthread_attr_t 对象中存放了线程属性的值,在创建线程时可以传递一个 pthread_attr_t 对象来为线程对象指定属性值.接口:

    • int pthread_attr_init(pthread_attr_t *attr);将 attr 中线程属性的值初始化为实现支持的默认值.

    • int pthread_attr_destroy(pthread_attr_t *attr);该接口主要进行2件事:

      1. 若在 pthread_attr_init() 中分配了资源,则会在此时释放资源;

      2. 将 attr 标记为不可用,防止 attr 再一次用于 pthread_create().

线程具有以下属性

  • detachstate,线程分离状态,参见'线程的分离状态';若为 PTHREAD_CREATE_DETACHED,则表明线程处于分离状态;若为 PTHREAD_CREATE_JOIN;接口:

    • int pthread_attr_getdetachstate(const pthread_attr_t *attr,int *state);获取 detachstate 属性的值,并将其存入 *state 中.

    • int pthread_attr_setdetachstate(pthread_attr_t *attr,int state);将 state 设置为 detachstate 属性的值.

线程终止

线程终止的情况

  • 在线程执行的任一时刻调用 pthread_exit(),都会使线程终止.

    • void pthread_exit(void *retval);终止当前线程,retval 本身将设置为线程的退出状态;

  • 从线程的入口函数 start_routine() 中返回.

  • 被其他线程取消.

    • int pthread_cancel(pthread_t thread);向线程 thread 发送取消请求,然后返回,并不等待线程被取消.

线程清理处理程序

  • 线程清理处理程序,在线程终止时调用;以栈的形式保存.

    • void pthread_cleanup_push(void (*routine)(void *),void *arg);安装线程清理处理程序 routine(arg),即将 routine() 放入栈顶.

    • void pthread_cleanup_pop(int execute);移除位于栈顶的线程清理处理程序,若 execute 不为0,则执行该线程清理处理程序,否则不执行.

    • pthread_cleanup_push()/pthread_cleanup_pop() 由于实现,一般要求成对出现.

线程的分离状态

  • 当线程终止时,

    • 若线程处于分离状态,则线程的底层资源会被直接释放,

    • 否则,会保存线程的底层资源直至其他线程调用了 pthread_join() 回收该线程

  • int pthread_detach(pthread_t thread);将线程 thread 设置为分离状态.

收到取消请求后

  1. 首先根据 cancelstate 的值来判断是否阻塞取消请求;

    1. 若 cancelstate 为 PTHREAD_CANCEL_DISABLE,则会阻塞取消请求直至 cancelstate 重新设置为 PTHREAD_CANCEL_ENABLE,此时才会将取消请求递送给线程;

    2. 若 cancelstate 为 PTHREAD_CANCEL_ENABLE,则会立刻递送取消请求.

  2. 当取消请求递送到线程之后,再根据 canceltype 的值来处理这个取消请求,

    1. 若 canceltype 为 PTHREAD_CANCEL_DEFERRED,则此时并不会立刻终止当前线程,而是设置一个标志(设为 is_cancel 标志),直至取消点时才会取消线程.

    2. 若 canceltype 为 PTHREAD_CANCEL_ASYNCHRONOUS,则此时会立刻终止当前线程,

  • pthread_setcancelstate()/pthread_setcanceltype() 用于设置/获取 cancelstate,或者 canceltype 的值.

  • int pthread_testcancel(),该函数会测试是否设置了取消标志,若设置,则立刻终止当前线程,该函数的实现大致如下:

    int pthread_testcancel(){
        if(is_cancel) // 此时表明线程已经收到取消请求.
            return pthread_exit(PTHREAD_CANCELED);// 调用 pthread_exit() 来终止当前线程
        return 0;
    }
  • 取消点,就是一个函数;所有可能会阻塞当期线程的函数(如 read(),pthread_cond_wait())都是取消点;这样当线程因此而阻塞时,接受会取消请求,就会即使被取消;这些函数的实现我猜测可能是:

void func(){
    while(true){
        pthread_testcancel();
        if(条件满足)
            return;
    }
}

线程私有数据

  • 线程私有数据,是一个内存块,线程在访问其私有数据时不需要担心与其他线程的同步访问问题,即可以认为线程私有数据只能被拥有者线程访问.

  • pthread_key_t,键,当一个键创建之后,可以被所有线程使用,即每一个线程都可以将私有数据的地址存放在该键中,接口:

    • int pthread_key_create(pthread_key_t *key,void (*destructor)(void*));创建键 key,其中:

      1. destructor 指定了键的析构函数,若为0,则说明键不需要析构函数.

    • int pthread_setspecific(pthread_key_t key,const void *private_buf);线程调用该函数将其私有数据地址保存在键 key 中.

    • void* pthread_getspecific(pthread_key_t key);线程调用该函数获取其保存在键 key 中的私有数据地址;若线程从未在 key 中保存过私有数据地址(即从未调用 pthread_setspecific()),则此时返回0.

    • int pthread_delete(pthread_key_t *key);线程调用该函数删除其在 key 中保存的私有数据地址(我认为相当于调用 pthread_setspecific(key,0));此时不会调用键 key 的析构函数.

  • 键的析构函数,当线程终止时,会遍历当前存在的所有键,对于每一个键,若线程在其中保存的私有数据地址不为0,则以私有数据地址为参数调用键的析构函数;整个过程会重复若干次直至线程在当前存在的所有键中保存的私有数据地址均为0.

线程与信号

  • 与信号有关的属性,信号处理方式,屏蔽信号集,未决信号集;在多线程环境中:

    • 信号处理方式,只存在于进程对象中,为所有线程共享

    • 屏蔽信号集,存在于每一个线程对象中,并不存在于进程对象中

    • 未决信号集,存在于每一个线程对象与进程对象中,

  • 一般情况下,信号都是发送至进程对象;此时进程对象会选择第一个未屏蔽该信号的线程来处理该信号(即若对该信号的处理方式为捕捉,则在此线程来运行信号处理函数;若信号的处理方式为忽略,则忽略信号;若处理方式为默认,则忽略或者终止进程或者暂停进程).若所有线程均屏蔽了信号,则将信号加入到进程对象的未决信号集中.

    • 实际测试发现,进程对象总会选择 main() 函数所在线程来处理信号;若 main 线程屏蔽了该信号,则将信号加入到进程的未决信号集中.

  • 在以下几种情况下,信号是直接递送给线程,此时若线程屏蔽了该信号,则将信号加入到线程的未决信号集中;否则在该线程中处理信号.

    • 由线程中某条指令执行后产生的信号,如:SIGBUS,SIGFPE,...

    • 当线程对一个断开链接的套接字写入数据时,产生的 SIGPIPE 信号

    • 当前进程中其他线程调用 pthread_kill()时

若干个接口

  • int pthread_sigmask(int how,const sigset_t *set,sigset_t *oldset);等同于 sigprocmask(),只是 pthread_sigmask() 改变的是调用线程的屏蔽信号集.

  • int pthread_kill(pthread_t thread,int signo);将信号 signo 发送给线程 thread.

  • int sigwaitinfo(const sigset_t *set, siginfo_t *info);挂起当前线程直至 set 中有一个信号处于未决状态,此时为该信号生成 siginfo_t 信息,并存入 info 指向的缓冲区中.该函数的行为如下:

while(true){
    if(set 中有一个信号存在与线程,或者进程的未决信号集中)
        从未决信号集中移除该信号;
        为该信号生成 siginfo_t 信号,并存入 info 指向的缓冲区中;
        return 信号值;
    else
        挂起当前线程一段时间;// 即主动放弃 CPU;
}

线程与 fork()

  • 当父进程中存在多个线程时,此时调用 fork() 创建的子进程中只有一个线程,即父进程中调用 fork() 的线程.同时由于子进程完全复制父进程的内存空间,所有子进程同时继承了父进程中所有互斥量,读写锁,条件变量的状态,如下:

Unix环境高级编程-11-线程_第2张图片

  • 假设在父进程中存在两个互斥量 mutex1,mutex2;其中 mutex1 被线程B拥有,mutex2被线程C拥有锁;在线程A调用 fork() 创建子进程之后;由于子进程完全继承父进程的地址空间,所以在子进程中仍存在 mutex1,mutex2;但是mutex1,mutex2的拥有线程却在子进程中不存在,所以子进程无法释放,并且销毁 mutex1,mutex2;

  • int pthread_atfork(void (*prepare)(void),void (*parent)(void),void (*child)(void));建立 fork() 处理程序.

    • prepare,在创建子进程之前,在父进程中调用,并且调用顺序与安装顺序相反.

    • parent,在创建子进程之后,在父进程中调用,并且调用顺序与安装顺序相同.

    • child,在创建子进程之后,在子进程中调用,并且调用顺序与安装顺序相同.

Unix环境高级编程-11-线程_第3张图片

线程同步

锁的模型

  • 多个进程同时读写一块内存区域,可能会造成冲突问题,解决方法是为该内存区域加上一把锁,任何线程在读写这块内存区域之前必须获得锁;在任一时刻,锁只能被一个线程拥有.这样就可以避免了冲突.

互斥量

  • 互斥量,就是那把保护内存区域的"锁";此时锁有2个状态:加锁,未加锁.任何线程在读写内存区域之前都必须加锁;

接口

  • pthread_mutex_t,数据类型,用来保存一个互斥量对象.

  • int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *mutex_attr);

  • int pthread_mutex_destroy(pthread_mutex_t *mutex);释放在 init() 期间分配的资源,将 mutex 设置为无效.

  • int pthread_mutex_lock(pthread_mutex_t *mutex);对 mutex 进行加锁,若 mutex 已经加锁,则阻塞直至其上的锁被释放.

  • int pthread_mutex_unlock(pthread_mutex_t *mutex);释放 mutex 上的锁,此时 mutex 变为未加锁状态.

  • int pthread_mutex_trylock(pthread_mutex_t *mutex);对 mutex 进行加锁,若 mutex 已经加锁,则出错返回,而不会阻塞.

属性

  • pthread_mutexattr_t,互斥量的属性存放在 pthread_mutexattr_t 对象中,可以在调用 pthread_mutex_init() 时传递一个 pthread_mutexattr_t 对象来初始化新建互斥量 mutex 的值.接口:

    • int pthread_mutexattr_init(pthread_mutexattr_t *attr);将 attr 保存的互斥量属性初始化为默认值.

    • int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);该函数主要做2件事,如下:

      1. 释放在 pthread_mutexattr_init() 时分配的资源;

      2. 将 attr 标记为不可用,防止其再次用于 pthread_mutex_init().

互斥量具有以下属性:

  • pshared,进程间共享,若 pshared 属性的值为 PTHREAD_PROCESS_PRIVATE,则此时互斥量不是进程间共享,即不能用于同步多个进程.若 pshared 属性的值为 PTHREAD_PROCESS_SHARED,则此时互斥量可以用于同步多个进程,此时互斥量必须位于多个进程都可以访问的共享内存中.接口:

    • int pthread_mutexattr_getpshared(const pthread_mutexattr_t *attr,int *pshared_ptr);获取 pshared 属性的值,并将其存入 pshared_ptr 中.

    • int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared_val);将 pshared_val 设置为 pshared 属性的值.

  • type,互斥量的类型,互斥量具有三种类型:PTHREAD_MUTEX_NORMAL,PTHREAD_MUTEX_ERRORCHECK,PTHREAD_MUTEX_RECURSIVE(递归),关于三种类型互斥量的区别,只需要了解这三种类型的互斥量在以下两种操作下的行为即可.见表:

Unix环境高级编程-11-线程_第4张图片

    • 当互斥量为 PTHREAD_MUTEX_RECURSIVE 时,则此时需要与加锁同样次数的解锁动作才能释放互斥量上的锁,即若加锁2次,此需要释放锁2次才能释放互斥量上的锁,如下:

pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr,PTHREAD_MUTEX_RECURSIVE);
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,&mutex_attr);
pthread_mutex_lock(&mutex);
pthread_mutex_lock(&mutex);
pthread_mutex_unlock(&mutex);// 此时并没有释放 mutex 上的锁.
pthread_mutex_unlock(&mutex);// 这里才真正释放了 mutex 上的锁.

    • int pthread_mutexattr_gettype(const pthread_mutexattr_t *mutexattr,int *type);获取互斥量的属性,并将其存入 *type 中.

    • int pthread_mutexattr_settype(pthread_mutexattr_t *mutexattr,int type_val);将 type_val 设置为互斥量的属性.

读写锁

  • 读写锁,也是保护内存区域的那把锁,有3个状态,加读锁,加写锁,未加锁.

Unix环境高级编程-11-线程_第5张图片

接口

  • pthread_rwlock_t,数据类型,用来保存一个读写锁对象.

  • int pthread_rwlock_init (pthread_rwlock_t * rwlock,const pthread_rwlockattr_t * attr);

  • int pthread_rwlock_destroy (pthread_rwlock_t *rwlock);

  • int pthread_rwlock_rdlock (pthread_rwlock_t *rwlock);参见上图'请求操作=加读锁'一列

  • int pthread_rwlock_tryrdlock (pthread_rwlock_t *rwlock);

  • int pthread_rwlock_wrlock (pthread_rwlock_t *rwlock);参见上图'请求操作=加写锁'一列

  • int pthread_rwlock_trywrlock (pthread_rwlock_t *rwlock);

  • int pthread_rwlock_unlock (pthread_rwlock_t *rwlock);参见上图'请求操作=释放锁'一列

属性

  • pthread_rwlockattr_t,读写锁的属性存放在 pthread_rwlockattr_t 中,在调用 pthread_rwlock_init() 初始化一个读写锁时可以传递一个 pthread_rwlockattr_t 对象来设置读写锁的属性,接口:

    • int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);

    • int pthread_rwlockattr_destroy(pthread_rwloackattr_t *attr);参见 pthread_mutexattr_init()/pthread_mutexattr_destroy(),这里就不在再次说明了.

读写锁的属性如下:

  • pshared,同互斥量的 pshared 属性一样,接口:

    • int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,int pshared_val);

    • int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *attr,int *pshared_ptr);

条件变量

  • 条件变量,线程使用条件变量来等待特定的条件发生,其上有2种操作:

    • 当条件未满足时,将线程挂起在条件变量上.

    • 当条件满足时,唤醒挂起在条件变量上的线程.

接口

  • int pthread_cond_init (pthread_cond_t *cond,const pthread_condattr_t *cond_attr);

  • int pthread_cond_destroy (pthread_cond_t *cond);

  • int pthread_cond_signal (pthread_cond_t *cond);唤醒挂起在 cond 上的一个线程.参见 pthread_cond_wait().

  • int pthread_cond_broadcast (pthread_cond_t *cond);唤醒挂起在 cond 上的所有线程.参见 pthread_cond_wait().

  • int pthread_cond_wait (pthread_cond_t *cond,pthread_mutex_t *mutex);释放 mutex 上的锁,将当前线程挂起在条件变量 cond 上,这2步是一个原子操作;直至被 signal()/broadcast() 唤醒;当唤醒时,首先获取 mutex 上的锁,然后再返回.

    • mutex 当前线程必须已经拥有 mutex 上的锁,才能调用 pthread_cond_wait().

  • int pthread_cond_timedwait (pthread_cond_t *cond,pthread_mutex_t *mutex,const struct timespec *abstime);等同于 pthread_cond_wait(),只不过加了个超时机制;当超时时,首先获取 mutex 上的锁,然后返回.

    • abstime,是一个绝对时间,如要想挂起在 cond 上三分钟,则应首先通过 gettimeofday() 获取当前时间,然后再加上三分钟,如下:

struct timeval current_time;
gettimeofday(&current_time,0);
struct timespec wait_time;
wait_time.tv_sec = current_time.tv_sec + 3*60;/* 当前时间+180s. */
wait_time.tv_nsec = current_time.tv_usec * 1000;
pthread_cond_timedwait(cond,mutex,&wait_time);

属性

  • pthread_condattr_t,条件变量的属性都存放在 pthread_condattr_t 对象中,接口:

    • int pthread_condattr_init(pthread_condattr_t *attr);

    • int pthread_condattr_destroy(pthread_condattr_t *attr);

条件变脸的属性

  • pshared,同互斥量的 pshared 一样,接口:

    • int pthread_condattr_getpshared(const pthread_condattr_t *attr,int *pshared_ptr);

    • int pthread_condattr_setpshared(pthread_condattr_t *attr,int pthread_val);







你可能感兴趣的:(Unix环境高级编程-11-线程)