APUE笔记之11-12章_线程_线程控制

第11章 线程 

即使在单处理器上,某些线程阻塞的时候,其他线程还是可以运行的,因此多线程仍然可以改善响应时间和吞吐量。

线程私有的:

  • 线程ID
  • 一组寄存器值
  • 调度优先级和策略
  • 信号屏蔽字
  • error变量(见1.7节)
  • 线程私有数据(key,见12.6节)

进程内所有线程共享的:

  • 代码段
  • 全局内存和堆内存
  • 文件描述符

线程相关操作

线程比较 & 获取线程自己的id

int pthread_equal(pthread_t tid1, pthread_t tid2);   // 若相等,返回非0; 不等,返回0
pthread_t pthread_self(void);    // 返回线程自己的线程id

进程ID是用 pid_t 数据类型来表示的,是一个非负整数。线程ID是用pthread_t数据类型来表示的,实现的时候可以用一个结构来代表pthread_t数据类型,所以可移植的操作系统实现不能把它作为整数处理。因此必须使用一个函数来对两个线程ID进行比较,即 pthread_equal. 

创建线程

int pthread_create(pthread_t *restrict tidp,    // 存储所创建线程的线程id
                   const pthread_attr_t *restrict attr,    // 定制线程属性
                   void *(*start_rtn)(void *), void *restrict arg);  // 线程函数及参数

新创建的线程从 start_rtn 函数的地址开始运行,该函数只有一个无类型指针参数arg. 如果需要向start_rtn函数传递的参数多于一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。

扫描二维码关注公众号,回复: 8897704 查看本文章

线程创建时并不能保证哪个线程会先运行:是先创建的线程,还是调用线程。

新创建的线程继承调用线程的浮点环境和信号屏蔽字,但是挂起信号集会被清除。

示例程序

#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>

pthread_t ntid1, ntid2, ntid3;

void printids(const char *s){
    pid_t pid;
    pthread_t tid;
    pid = getpid();
    tid = pthread_self();
    printf("%s pid %lu tid %lu (0x%lx)\n", 
            s , (unsigned long)pid, 
            (unsigned long)tid, (unsigned long)tid);
}

void * thr_fn(void *arg){
    printids("new thread: ");
    sleep(20);
    return ((void *)0);
}

int main()
{
    int err;
    err = pthread_create(&ntid1, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("Cannot create thread: %d", err);
    }
    err = pthread_create(&ntid2, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("Cannot create thread: %d", err);
    }
    err = pthread_create(&ntid3, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("Cannot create thread: %d", err);
    }
    printids("main thread:");
    sleep(1);  // 主线程需要休眠,否则看不到子线程的打印,因为主线程可能先退出。
    
    return 0;
}

线程退出

void pthread_exit(void * rval_ptr); 

如果线程简单地启动例程返回,那么rval_ptr就包含返回码;如果线程被取消,由rval_ptr指定的内存单元就被设置为PTHREAD_CANCELED. 

线程终止:

如果进程中任意线程调用了exit, _Exit, _exit, 那么整个进程就会终止。

单个线程可以通过3种方式退出:

  1. 普通地从启动例程中返回,返回值是线程的退出码
  2. 可以被同一进程中的其他线程取消
  3. 调用 pthread_exit 

join以阻塞调用者

int pthread_join(pthread_t thread, void **rval_ptr); // 成功返回0;否则返回错误编号

调用线程将一直阻塞,直到指定的线程结束。

关于rval_ptr:

  • 如果线程简单地从启动例程返回,rval_ptr就包含返回码;
  • 如果线程被取消,则rval_ptr指定的内存单元就被设置为PTHREAD_CANCELED
  • 如果对线程的返回值不感兴趣,可将rval_ptr设置为NULL

当一个线程通过调用pthread_exit 退出或者简单地从启动例程中返回,则进程中的其他线程可以通过调用pthread_join函数获得该线程的退出状态。

取消其他线程

int pthread_cancel(pthread_t tid); // 若成功返回0;否则返回错误编号

但是,被取消的线程可以选择忽略取消或者控制如何被取消

pthread_cancel并不等待线程终止,它仅仅提出请求。

线程清理程序

void pthread_cleanup_push(void (*rtn)(void *), void * arg);
void pthread_cleanup_pop(int execute);

线程可以安排它退出时需要调用的函数,这样的函数称为线程清理程序。一个线程可以有多个清理程序,它们在栈中,即执行顺序与注册顺序相反。

当线程: 

  • 调用pthread_exit
  • 或响应取消请求
  • 或用非零execute参数调用pthread_cleanup_pop()时

清理函数rtn就会被调用。换句话说,如果线程正常返回(return),则不会调用线程清理程序。

分离线程

int pthread_detach(pthread_t, tid); // 若成功返回0;否则返回错误编号

如果线程被分离,则其底层存储资源可以在线程终止时被收回。

线程分离后,不能再用pthread_join()等待它的终止状态,否则会产生未定义行为。

进程与线程的函数比较

进程原语

线程原语

描述

fork

pthread_create

创建新的控制流

exit

pthread_exit

从现有控制流中退出

waitpid

pthread_join

阻塞,以等待其他控制流结束

atexit

pthread_cleanup_push

注册退出控制流时调用的函数

getpid

pthread_self

获取控制流的id

abort

pthread_cancel

请求控制流的非正常退出

线程同步

1.互斥量 (pthread_mutex_t)

静态分配互斥量:(2种方法) 

  • 设置为 PTHREAD_MUTEX_INITIALIZER
  • 调用 pthread_mutex_init()

动态分配互斥量:

  • 比如,通过malloc函数。在释放内存时,需要调用pthread_mutex_destroy(). 具体是:先pthread_mutex_unlock,然后pthread_mutex_destroy, 最后free内存。

初始化互斥量

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutex_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

若成功,返回0;若失败,返回错误编号。

若要用默认的属性初始化互斥量,只需把attr设为NULL. 

对互斥量加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);// 若成功,则锁住并返回0;若失败,返回EBUSY. 
int pthread_mutex_unlock(pthread_mutex_t *mutex);

绑定线程阻塞时间
 

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, 
                            const struct timespec *restrict tsptr); 
// 成功返回0,失败返回错误编码

示例程序:

struct timespec tout;
strcut tm* tmp;
char buf[64];

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
clock_gettime(CLOCK_REALTIME, &tout);
tmp = localtime(&tout.tv_sec); 
strftime(buf, sizeof(buf), "%r", tmp);
printf("the time is now %s\n", buf);
tout.tv_sec += 100;  // 100 seconds from now on
pthread_mutex_timedlock(&lock, &tout);

避免死锁

如果线程对同一个互斥量加锁2次,那么它就会陷入死锁。

若有多个锁,按顺序上锁,反序解锁。

2. 读写锁 (reader-writer lock)

与互斥量类似,不过读写锁允许更高的并行性。读写锁,也叫做共享互斥锁(shared-exclusive lock)。

读写锁非常适合于读的次数远大于写的次数的情况。

读写锁有3种状态:

  • 读模式下加锁状态
  • 写模式下加锁状态
  • 不加锁状态 

一次只有一个线程可以占有写模式的读写锁;但是多个线程可以同时占有读模式的读写锁

当读写锁处于读模式锁住时,这时有一个线程试图以写模式获取锁,读写锁通常会阻塞随后到来的读模式锁请求

读写锁的初始化

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

若成功,返回0;否则返回错误编号

静态初始化,使用 PTHREAD_RWLOCK_INITIALIZER常量。

在释放读写锁占用的内存之前,需要调用 pthread_rwlock_destroy 来做清理工作。如果pthread_rwlock_init为读写锁分配了资源,pthread_rwlock_destroy将释放这些资源。

读模式锁定、写模式锁定、解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);

int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, 
                               const struct timespec *restrict tsptr);
int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, 
                               const struct timespec *restrict tsptr);

若成功,返回0;否则返回错误编号。

3. 条件变量 (pthread_cond_t)

条件变量的初始化方式有2种:

  • PTHREAD_COND_INITIALIZER 来静态初始化条件变量。
  • pthread_cond_init() 函数来动态初始化条件变量。

初始化与反初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond, 
                      const pthread_condattr_t *restrict attr);  
// attr为NULL表示使用条件变量的默认属性

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件变量为真

int pthread_cond_wait(pthread_cond_t *restrict cond, 
                      pthread_mutex_t *restrict mutex);  
// 注意,pthread_cond_wait函数的第2个参数是一个互斥量

int ptread_cond_tiimedwait(pthread_cond_t *restrict cond, 
                           pthread_mutex_t *restrict mutex, 
                           const struct timespec *restrict tsptr);

注意:pthread_cond_wait函数做的是:

  • 把自己放到等待条件为真的线程列表上
  • 对互斥量解锁 (若不解锁,别的线程没有办法设置等待条件为真)
  • 阻塞等待条件
  • 一旦条件为真,再次对互斥量加锁

通知线程条件已满足

// 唤醒至少一个线程
int pthread_cond_signal(pthread_cond_t *cond);

// 唤醒所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);

如果只有互斥量,而没有条件变量,则在生产者消费者问题中,消费者无法准确知道何时能去消费,因此只能轮询。

而有了条件变量,相当于用信号去唤醒消费者消费。

以下是生产者消费者代码: 

struct msg *workq; 
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;


// 消费者
void process_msg(void)
{
    struct msg *mp;
    for(;;) {
        pthread_mutex_lock(&qlock); 
        // 1. unlock 2. 阻塞直到条件满足 3. lock 
        while(workq == NULL) pthread_cond_wait(&qready, &qlock);  
        mp = workq;  // 消费
        workq = mp->m_next;  // 消费
        pthread_mutex_unlock(&qlock);  // 解锁,以让生产者拿到锁进行生产
        // then process message here...
    }
}


// Producer
void enqueue_msg(struct msg *mp)
{
    pthread_mutex_lock(&qlock);  // 当操作临界资源workq的时候需要加锁
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);  // 释放锁,然后通知等待条件为真(workq不再为NULL)的进程 
    pthread_cond_signal(&qready);
}

4. 自旋锁 (忙等)

自旋锁类似于互斥量。但是它不是通过休眠使进程阻塞,而是在获取锁之前一直忙等(即自旋)以处于阻塞状态。

自旋锁可以用于以下情况: 锁被持有的时间短,而且线程不希望在重新调度上花费太多成本。

自旋锁通常作为底层原语用于实现其他类型的锁。在用户层,自旋锁不是很有用。

实时系统,都是非抢占式。而中断相当于是一种抢占。所以,在非抢占式内核中,除了提供互斥机制外,自旋锁还可以阻塞中断。在这种类型的内核中,中断处理程序不能休眠,因此它们能用的同步原语只能是自旋锁。

int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);

pthread_spin_init()函数的第2个参数 pshared 表示进程共享属性:

  • 如果把它设为 PTHREAD_PROCESS_SHARED,则自旋锁能被可以访问锁底层内存的线程所获取,即使这些线程属于不同的进程
  • 如果把它设为 PTHREAD_PROCESS_PRIVATE,则自旋锁只能被初始化该锁的进程内部的线程访问。

5. 屏障(barrier)

屏障(barrier)是用户协调多个线程并行工作的同步机制。

屏障允许每个线程都等待,直到所有的合作线程都到达某一点,然后从该点继续执行。

pthread_join也是一种屏障,允许等待一个线程;但屏障的概念更广,它允许任意数量的线程等待,直到所有的线程都完成工作,且线程不需要退出。所有线程到达屏障后可以继续工作

int pthread_barrier_init(pthread_barrier_t *restrict barrier, 
                         const pthread_barrierattr_t *restrict attr, 
                         unsigned int count);
int pthread_barrier_destroy(pthread_barrier_t *barrier); 

int pthread_barrier_wait(pthread_barrier_t *barrier); 

初始化屏障时,可以使用count参数指定,在允许所有线程继续运行之前,必须达到屏障的数目。

调用pthread_barrier_wait的线程在屏障计数未满足条件时,会进入休眠状态。如果该线程是最后一个调用pthread_barrier_wait的,那就满足了计数,所有线程将被唤醒。

一旦达到了屏障计数的数值,而且线程处于非阻塞状态,屏障就可以被重用;但屏障计数不会被改变。


第12章 线程控制

1. 线程属性

管理属性的函数都遵循相同的模式:

  1. 一个初始化函数
  2. 一个销毁属性对象的函数
  3. 一个GET: 每个属性都有一个从属性对象中获取属性值的函数。成功返回0,失败返回错误编号。
  4. 一个SET: 每个属性都有一个设置属性值的函数。属性值作为参数按值传递。

POSIX.1的线程属性:

  • detachstate: 线程的分离状态的属性
  • guardsize: 线程栈末尾的警戒缓冲区的大小(字节数)
  • stackaddr: 线程栈的最低地址
  • stacksize: 线程栈的最小长度(字节数)

线程属性管理相关的函数:

int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);

int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr, int *detachstate);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int *detachstate);

在创建线程之前,可以使用pthread_attr_setdetachstate 函数把线程属性detachstate设置为成以下2个合法值之一,然后再调用pthread_create函数创建线程:

  • PTHREAD_CREATE_DETACHED (以分离状态启动线程)
  • PTHREAD_CREATE_JOINABLE (正常启动线程,应用程序可以获取到线程的终止状态)

线程栈属性进行管理

int pthread_attr_getstack(const pthread_attr_t *restrict attr, 
                          void **restrict stackaddr, 
                          size_t *restrict stacksize);

int pthread_attr_setstack(pthread_attr_t *attr, 
                          void *stackaddr, 
                          size_t stacksize);

希望改变默认的栈大小,又不希望自己处理线程栈的分配问题

int pthread_attr_getstacksize(const pthread_attr_t *restrict attr, 
                              size_t *restrict stacksize);

int pthread_attr_setstacksize(pthread_attr_t *attr, 
                              size_t stacksize);

对于进程来说,虚地址空间的大小是固定的。但对于线程来说,虚地址空间要被所有的线程栈共享。

  • 一方面,如果应用程序使用了许多线程,以致这些线程栈的总大小超过了可用的虚地址空间,就需要减少默认的线程栈的大小。
  • 另一方面,如果线程调用的函数分配了大量的自动变量,或者调用的函数涉及许多很深的栈桢(stack frame),那么需要的栈的大小就比默认的要大。

线程属性guardsize控制着线程栈末尾之后用以避免栈溢出的扩展内存的大小。这个值由具体实现定义,但一般都是系统页大小

int pthread_attr_getguardsize(const pthread_attr_t *restrict attr, 
                              size_t *restrict guardsize);

int pthread_attr_setguardsize(pthread_attr_t *attr, 
                              size_t guardsize); 

2. 同步属性

2.1 互斥量属性

互斥量有3个属性:

1. 进程共享属性: 即是否在进程间共享该互斥量: 

  • PTHREAD_PROCESS_PRIVATE:默认行为,只在本进程的线程间共享互斥量。
  • PTHREAD_PROCESS_SHARED:不同进程的线程间互斥。

2. 健壮属性

  • PTHREAD_MUTEX_STALLED: 默认行为。当持有互斥量的进程终止时,使用互斥量的行为是未定义的。
  • PTHREAD_MUTEX_ROBUST: 持有互斥量的线程终止了且没有释放互斥量,另一个进程使用pthread_mutex_lock获取锁的时候,返回值为EOWNERDEAD而不是0. 

3. 类型属性

     POSIX.1定义了4种类型:

  1. PTHREAD_MUTEX_NORMAL: 标准互斥量,不做特殊的错误检查或死锁检测。
  2. PTHREAD_MUTEX_ERRORCHECK: 提供错误检查。
  3. PTHREAD_MUTEX_RECURSIVE: 递归互斥量。
  4. PTHREAD_MUTEX_DEFAULT: 提供默认特性和行为。Linux3.2.0把该类型映射为普通互斥量类型。

图12-5 展示了特殊情况下这4种互斥量的表现。

相关函数如下:

int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

int pthread_mutexattr_getshared(const pthread_mutexattr_t *restrict attr, 
                                int *restrict pshared);
int pthread_mutexattr_setshared(pthread_mutexattr_t *attr, 
                                int pshared);

int pthread_mutexattr_getrobust(const pthread_mutexattr_t *restrict attr, 
                                int *restrict robust);
int pthread_mutexattr_setrobust(pthread_mutexattr_t *attr, 
                                int robust); 

int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, 
                              int *restrict type);
int pthread_mutexattr_settype(pthread_mutexattr_t *attr, 
                              int type); 

2.2 读写锁属性

读写锁支持的唯一属性是进程共享属性

int pthread_rwlockattr_getshared(const pthread_rwlockattr_t *restrict attr, 
                                 int *restrict pshared);
int pthread_rwlockattr_setshared(pthread_rwlockattr_t *attr, 
                                 int pshared);

2.3 条件变量属性

条件变量有2个属性:

  • 进程共享属性
  • 时钟属性。

与其他同步属性一样,条件变量支持进程共享属性。它控制着条件变量是可以被单进程的多个线程使用,还是可以被多进程的线程使用。

int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

int pthread_condattr_getshared(const pthread_condattr_t *restrict attr, 
                               int *restrict pshared);
int pthread_condattr_setshared(pthread_condattr_t *attr, int pshared);

int pthread_condattr_getclock(const pthread_condattr_t *restrict attr, 
                              clockid_t *restrict clockid);
int pthread_condattr_setclock(pthread_condattr_t *attr, 
                              clockid_t clock_id);

2.4 屏障属性

目前屏障只有进程共享属性, 它控制着屏障是可以被多进程的线程使用,还是只能被初始化屏障的进程内的多线程使用。

  • PTHREAD_PROCESS_SHARED: 多进程中的多个线程可用
  • PTHREAD_PROCESS_PRIVATE: 只有初始化屏障的那个进程内的多个线程
#include <pthread.h>

int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);

int pthread_barrierattr_getshared(const pthread_barrierattr_t * restrict attr, 
                                  int * restrict pshared);
int pthread_barrierattr_setshared(pthread_barrierattr_t * attr, 
                                  int * pshared);

3. 重入

线程安全:如果一个函数在相同的时间点可以被多个线程安全地调用,那么该函数是线程安全的。

很多函数并不是线程安全的,因为它们返回的数据存放在静态的内存缓冲区中。通过修改接口,要求调用者自己提供缓冲区可以使函数变为线程安全的。

如果一个函数对于多个线程是可重入的,那么这个函数就是线程安全的。但这并不能说明对信号处理程序来说,该函数也是可重入的。

如果函数对异步信号处理程序的重入是安全的,那么该函数是异步信号安全的。

假设有一个函数叫做 getenv_r, 其开头执行:

static pthread_once_t init_done = PTHREAD_ONCE_INIT; 

pthread_once(&init_done, thread_init); 

这里使用 pthread_once 函数来确保无论多少线程同时竞争调用 getenv_r (一个线程安全函数),thread_init 函数只会被调用一次。

一个函数是信号安全的,并不意味着它对信号处理程序是可重入的。如果使用的是非递归的互斥量,线程从信号处理程序中调用该函数,则可能出现死锁,因为原函数中加锁之后,信号处理程序中又调用了该函数,则形成死锁(若用的不是递归互斥量)。 所以,必须使用递归互斥量阻止其他线程改变需要的数据结构,还要阻止来自信号处理程序的死锁。

4. 线程特定数据

线程特定数据(thread-specific data),也称为线程私有数据(thread-private data),是存储和查询某个特定线程相关数据的一种机制。

除了使用寄存器以外,一个线程没有办法阻止另一个线程访问它的数据。线程特定数据也不例外。虽然底层的实现部分不能阻止这种访问能力,但管理线程特定数据的函数可以提高线程间的数据独立性,使得线程不太容易访问到其他线程的线程特定数据。

在分配线程特定数据之前,需要创建与该数据关联的键。这个键将用于获取对线程特定数据的访问。使用pthread_key_create创建一个键。

int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));  
// 成功返回0,否则返回错误编号

int pthread_key_delete(pthread_key_t key);  // 取消键与线程特定数据值之间的关联关系

pthread_once_t initflag = PTHREAD_ONCE_INIT;
int pthread_once(pthread_once_t *initflag, void (*initfn)(void));

initflag 必须是一个非本地变量(全局变量或静态变量),而且必须初始化为 PTHREAD_ONCE_INIT

如果每个线程都调用pthread_once, 系统就能保证初始化例程 initfn 只被调用一次。如下:

void destructor(void *);

pthread_key_t key;
pthread_once_t init_done = PTHREAD_ONCE_INIT;


void thread_init()
{
    pthread_key_create(&key, destructor);
}

int threadfunc(void *arg)
{
    pthread_once(&init_done, thread_init);
    ......
}

通过调用 pthread_setspecific 函数把键和特定数据关联起来; 通过pthread_getspecifig函数获得线程特定数据的地址。

void *pthread_getspecific(pthread_key_t key);
int pthread_setspecific(pthread_key_t key, const void* value); 

malloc函数不是异步信号安全的。任何调用了malloc的函数,也不是异步信号安全的。

5.线程的取消选项

有2个线程属性没有被包含在pthread_attr_t结构中:

  • 可取消状态
  • 可取消类型

可取消状态的属性可以是: 

  • PTHREAD_CANCEL_ENABLE
  • PTHREAD_CANCEL_DISABLE: pthread_cancel函数不会杀死线程

线程可以通过调用 pthread_setcancelstate来修改其取消状态。

int pthread_setcancelstate(int state, int * oldstate); 

pthread_cancel调用并不等待线程终止。默认情况下,线程A在线程B发出取消A的请求后还是会继续运行,直到线程A到达某个取消点。取消点是线程检查它是否被取消的一个位置,如果取消了,则按要求行事。

如果长时间不会调用一些包含取消点的函数,则可以调用 pthread_testcancel 函数在程序中添加自己的取消点。

void pthread_testcancel(void);

“取消类型”属性包括:

  • PTHREAD_CANCEL_DEFERRED: 推迟取消,即调用pthread_cancel之后,在没有到达取消点之前,并不会真正的取消。
  • PTHREAD_CANCEL_ASYNCHRONOUS: 异步取消,即线程可以在任何时间撤销,不是非得到取消点才能被取消。

可以通过调用 pthread_setcanceltype 函数来设置取消类型。

int pthread_setcanceltype(int type, int *oldtype);

6. 线程和信号

每个线程都有自己的信号屏蔽字。但是,信号的处理是进程中所有线程共享的。当某个线程修改了与某个给定信号相关的处理行为后,所有线程都必须共享这个处理行为的改变。

进程中的信号是递送到单个线程的。如果一个信号与硬件故障相关,那么该信号一般会被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。

进程使用 sigprocmask 来阻止信号的发送,但sigprocmask在多线程的进程中是没有定义的;线程必须使用 pthread_sigmask 来阻止信号的发送。

int pthread_sigmask(int how, 
                    const sigset_t *restrict set, 
                    sigset_t *restrict oset); 

线程可以通过调用 sigwait 来等待一个或多个信号的出现:

int sigwait(const sigset_t *restrict set, int *restrict signop);  

为了避免错误行为的发生,线程在调用sigwait之前必须阻塞那些它正在等待的信号。sigwait函数会原子地取消信号集的阻塞状态,直到有新的信号被递送。在返回之前,sigwait将恢复线程的信号屏蔽字。

使用sigwait的好处是它可以简化信号处理,允许把异步产生的信号用同步的方式处理。

为了防止信号中断线程,可以把信号加到每个线程的信号屏蔽字中。

如果多个线程在sigwait的调用中因等待同一个信号而阻塞,那么在信号递送的时候,就只有一个线程可以从sigwait中返回。

如果一个信号被捕获(例如进程通过使用sigaction建立了一个信号处理程序),而且一个线程正在sigwait调用中等待同一信号,那么此时将由操作系统决定以何种方式递送信号:操作系统可以让sigwait返回;也可以调用信号处理程序;但这2种情况不会同时发生。

把信号发送给进程,可以调用kill;把信号发送给线程,调用 pthread_kill. 

int pthread_kill(pthread_t thread, int signo); 

如果信号的默认动作是杀死进程,那么把这个信号发送给线程依然会杀死整个进程

7. 线程和fork

子进程通过继承整个地址空间的副本,从而从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程包含一个以上的线程,子进程在fork返回后,如果紧接着不是马上调用exec的话,则需要清理锁的状态。

在子进程内部,只存在一个线程,就是由父进程中调用fork的线程的副本构成的。如果父进程中占有锁,子进程将同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,因此子进程没有办法知道它占有了哪些锁、需要释放哪些锁。

如果子进程从fork返回后马上调用exec函数,则可以避免这样的问题。这种情况下,旧的地址空间就会被丢弃,所以锁的状态无关紧要。但如果fork后不调用exec,还要继续做处理工作,则就会遇到锁状态的问题。

在多线程的进程中,为了避免不一致状态的问题,POSIX.1声明,在fork返回和子进程调用exec函数之间,子进程只能调用异步信号安全的函数。这就限制了在调用exec之前子进程能做什么,但没有涉及子进程中锁状态的问题。

要清楚锁状态,可以调用 pthread_atfork 函数建立fork处理程序。

#include <pthread.h>

// 成功返回0,错误返回错误编号
int pthread_atfork(void (*prepare)(void), 
                   void (*parent)(void), 
                   void (*child)(void));  

pthread_atfork 函数最多可以安装3个帮助清理锁的函数: 

  • prepare fork处理程序由父进程在fork创建子进程前调用。这个fork处理程序的任务是获取父进程定义的所有锁。
  • parent fork处理程序是在fork创建子进程之后、返回之前,在父进程的上下文中调用的。它的任务是对prepare程序获得的所有锁进行解锁。
  • child fork处理程序在fork返回之前在子进程的上下文中调用。与parent程序一样,child程序也必须释放prepare程序中获取的所有的锁。

对于条件变量状态的清理,目前没有可移植的方法。

pthread_atfork机制的意图是使fork之后的锁状态保持一致,但它还是存在一些不足之处的(略,详见P370),因此只能在有限的情况下使用。

8. 线程与I/O 

pread 和  pwrite 函数,在多线程环境下是非常有用的,因为它们是原子操作,而进程中的所有线程共享相同的文件描述符

  • pread = lseek + read
  • pwrite = lseek + write

使用 pread ,使偏移量的设定和数据的读取成为一个原子操作: 

pread(fd, buf1, 100, 300);  // 100是读多少数据,300是lseek 

pwrite也是类似的原子操作。


(完)

发布了169 篇原创文章 · 获赞 332 · 访问量 48万+

猜你喜欢

转载自blog.csdn.net/nirendao/article/details/88322791