UNIX环境高级编程(12-线程控制)

本章详细介绍了线程属性和同步原语属性。最后讨论基于进程的系统调用如何与线程进行交互。

属性

可以通过对每个对象关联的不同属性来细调线程和同步对象的行为。管理这些属性的函数大概有以下几类:

  • 初始化函数,负责给属性设置为默认值
  • 销毁函数,负责释放初始化函数分配的资源
  • 获取属性值的函数
  • 设置属性值的函数

线程属性

  • 初始化和销毁

    // Both return: 0 if OK, error number on failure
    int pthread_attr_init(pthread_attr_t *attr);
    int pthread_attr_destroy(pthread_attr_t *attr);
    

    destroy函数除了释放资源外,还会用无效的值初始化属性对象,这样当线程创建函数误用该对象时,会返回错误信息。

  • 分离状态属性detachstate

    // Both return: 0 if OK, error number on failure
    int pthread_attr_getdetachstate(const pthread_attr_t *restrict attr,
                                  int *detachstate);
    int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
    

    该状态可以设置成PTHREAD_CREATE_DETACHEDPTHREAD_CREATE_JOINABLE,分别表示以分离状态或正常方式启动线程。

  • 线程栈的相关属性

    // Both return: 0 if OK, error number on failure
    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);
    

    stackaddr参数指定的是栈的最低内存地址。

    如果不想手动设定栈地址,可以通过下面的函数来仅指定栈大小。

    // Both return: 0 if OK, error number on failure
    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);
    

    guardsize控制线程栈末尾之后用以避免栈溢出的扩展内存的大小。当此值设置为0或者修改了线程属性stackaddr后,系统不会提供警戒缓冲区。

    // Both return: 0 if OK, error number on failure
    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);
    

同步属性

互斥量属性

// Both return: 0 if OK, error number on failure
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
  • 进程共享属性(process-shared)

    默认情况下,仅相同进程的线程可以访问同一个同步对象(PTHREAD_PROCESS_PRIVATE),但是在某些情况下,需要多个进程访问同一个同步对象,这时候可以将属性设置为THREAD_PROCESS_SHARED

    // Both return: 0 if OK, error number on failure
    int pthread_mutexattr_getpshared(const pthread_mutexattr_t *
                               restrict attr, int *restrict pshared);
    int pthread_mutexattr_setpshared(pthread_mutexattr_t *attr,int pshared);
    
  • 健壮属性(robust)

    当某个线程在终止时没有释放持有的锁,那么当其他线程尝试获取该锁时,会发生问题。如果使用默认的设置(PTHREAD_MUTEX_STALLED),则请求的线程会一直阻塞。可以通过设置为PTHREAD_MUTEX_ROBUST解决这个问题,此时lock函数的返回值为EOWNERDEAD

    如果线程加锁时发现返回值为EOWNERDEAD,那么在解锁前需要调用consistent函数,声明互斥量的一致性(与该互斥量相关的状态在互斥量解锁之前是一致的)。如果没有调用consistent函数就解锁,那么互斥量将不再可用,其他线程调用lock函数会返回ENOTRECOVERABLE

    // All return: 0 if OK, error number on failure
    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_mutex_consistent(pthread_mutex_t * mutex);
    
  • 类型属性(type)

    控制互斥量的锁定特性。

    • PTHREAD_MUTEX_NORMAL :标准互斥量,不进行错误检查或死锁检测。
    • PTHREAD_MUTEX_ERRORCHECK :提供错误检查
    • PTHREAD_MUTEX_RECURSIVE :允许同一线程在解锁前多次加锁。
    • PTHREAD_MUTEX_DEFAULT :提供默认的特性和行为,操作系统可以将其映射为其他类型。
    // Both return: 0 if OK, error number on failure
    int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr,
                                int *restrict type);
    int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
    
    Mutex type Relock without unlock? Unlock when not owned? Unlock when unlocked?
    PTHREAD_MUTEX_NORMAL deadlock undefined undefined
    PTHREAD_MUTEX_ERRORCHECK returns error returns error returns error
    PTHREAD_MUTEX_RECURSIVE allowed returns error returns error
    PTHREAD_MUTEX_DEFAULT undefined undefined undefined

读写锁属性

读写锁仅支持进程共享属性。

// All return: 0 if OK, error number on failure
int pthread_rwlockattr_init(pthread_rwlockattr_t *attr);
int pthread_rwlockattr_destroy(pthread_rwlockattr_t *attr);

int pthread_rwlockattr_getpshared(const pthread_rwlockattr_t *restrict attr,
                                int *restrict pshared);
int pthread_rwlockattr_setpshared(pthread_rwlockattr_t *attr,
                                int pshared);

条件变量属性

支持进程共享属性和时钟属性

// All return: 0 if OK, error number on failure
int pthread_condattr_init(pthread_condattr_t *attr);
int pthread_condattr_destroy(pthread_condattr_t *attr);

int pthread_condattr_getpshared(const pthread_condattr_t *restrict attr,
                              int *restrict pshared);
int pthread_condattr_setpshared(pthread_condattr_t *attr, int pshared);

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

时钟属性用于控制pthread_cond_timedwait函数使用哪个系统时钟。

屏障属性

只有进程共享属性。

// All return: 0 if OK, error number on failure
int pthread_barrierattr_init(pthread_barrierattr_t *attr);
int pthread_barrierattr_destroy(pthread_barrierattr_t *attr);

int pthread_barrierattr_getpshared(const pthread_barrierattr_t *restrict attr,
                                 int *restrict pshared);
int pthread_barrierattr_setpshared(pthread_barrierattr_t *attr, int pshared);

线程特定数据

线程模型促进了进程中数据和属性的共享,但是在部分场景下,我们又希望线程的部分数据可以是私有的。

一个进程中的所有线程都可以访问进程的整个地址空间,因此线程没有办法阻止另一个线程访问它的数据(除非使用寄存器),即使是接下来介绍的线程特定数据(thread-specific data)机制,也不能做到这一点。但是通过这种机制,可以提高线程间的独立性,使得线程不太容易访问到其他线程的线程特定数据。

每个线程通过键(key)来访问线程特定数据,键在进程中被所有线程使用,每个线程把自己的线程特定数据和键关联起来。这样,通过同一个键,每个线程可以管理与自己关联的数据。

// Both return: 0 if OK, error number on failure
int pthread_key_create(pthread_key_t *keyp, void (*destructor)(void *));
int pthread_key_delete(pthread_key_t key);

创建新键时,每个线程的数据地址为空。同时,在创建的时候可以指定一个析构函数,当线程退出时,如果数据地址不为空,则会调用这个析构函数(参数是数据地址)。

所有的线程都可以调用删除函数来取消键与数据之间的关联,但是这不会触发析构函数

// Returns: thread-specific data value or NULL if no value has been associated with the key
void *pthread_getspecific(pthread_key_t key);
// Returns: 0 if OK, error number on failure
int pthread_setspecific(pthread_key_t key, const void *value);

我们可以通过get函数的返回值来确定是否需要调用set函数。

取消选项

有2个额外的线程属性并没有包含在上述的pthread_attr_t中,它们分别是可取消状态可取消类型

可取消状态

该属性可以设置成PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLE

// Returns: 0 if OK, error number on failure
int pthread_setcancelstate(int state, int *oldstate);

set函数把当前的可取消状态设置为state,同时将原来的状态通过oldstate返回。

11章在介绍pthread_cancle函数时,我们说到该函数仅仅是提出一个请求,而不保证线程被马上终止。在默认的情况下(即PTHREAD_CANCEL_ENABLE),线程在取消请求发出后,在到达某个取消点时前,都会一直运行。

在线程调用某些函数时(函数列表见ch12/Cancellation points-x.png),取消点就会出现。但是对于部分特殊的线程,可能很长一段时间都不会调用到这些函数,那么可以使用pthread_testcancel函数手动添加取消点。

void pthread_testcancel(void);

如果将状态设置为PTHREAD_CANCEL_DISABLE,那么调用pthread_cancle函数并不会杀死线程,取消请求会一直处于挂起状态,直到状态被设置为ENABLE。同理,此时调用pthread_testcancel没有任何效果。

可取消类型

该属性可以设置成PTHREAD_CANCEL_DEFERREDPTHREAD_CANCEL_ASYNCHRONOUS

// Returns: 0 if OK, error number on failure
int pthread_setcanceltype(int type, int *oldtype);

默认设置为PTHREAD_CANCEL_DEFERRED,即推迟取消,线程到达取消点之前不会被真正取消。如果设置为PTHREAD_CANCEL_ASYNCHRONOUS,即异步取消,那么线程可以在任意时间撤销,而不必等待到达取消点。

信号

每个线程有自己的信号屏蔽字,通过pthread_sigmask函数进行设置,参数与sigprocmask类似。

#include <signal.h>
// Returns: 0 if OK, error number on failure
int pthread_sigmask(int how, const sigset_t *restrict set,
                   sigset_t *restrict oset);

需要注意的是,如果在主线程中屏蔽了一些信号,那么被创建的线程会继承当前的信号屏蔽字。

线程可以通过sigwait函数等待一个或多个信号出现。如果多个线程通过该函数等待信号,则在传递信号的时候,只有一个线程可以从该函数返回。

// Returns: 0 if OK, error number on failure
int sigwait(const sigset_t *restrict set, int *restrict signop);

可以调用pthread_kill函数将信号发送给指定的线程(需属于同一进程)。

// Returns: 0 if OK, error number on failure
int pthread_kill(pthread_t thread, int signo);

另外,如果传递给signo的值是0,则可以用来检测线程是否存在。如果接收信号的线程没有对应的处理函数,则该信号会发送给主线程[1]。相关测试见ch12/pthread_kill.c,摘录主要代码如下:

int main()
{
  int err;
  sigset_t mask, old;
  pthread_t pt1, pt2;

  sigemptyset(&mask);
  sigaddset(&mask, SIGQUIT); /* 如果不屏蔽QUIT信号,则主线程会收到该信号 */
  sigaddset(&mask, SIGINT);
  err = pthread_sigmask(SIG_BLOCK, &mask, &old);
  assert(err == 0);

  signal(SIGQUIT, main_q); /* QUIT信号处理函数 */

  err = pthread_create(&pt1, NULL, th1, NULL);
  assert(err == 0);

  sleep(1);
  printf("main:send QUIT signal.
");
  // 线程1未屏蔽QUIT信号,但没有处理程序,会返回给主线程
  pthread_kill(pt1, SIGQUIT);

  sleep(10);

  return 0;
}
// 线程1
void* th1(void* a)
{
  int err, signo;
  sigset_t mask;

  sigemptyset(&mask);
  sigaddset(&mask, SIGINT);
  pthread_sigmask(SIG_BLOCK, &mask, NULL);

  while (1) {
    err = sigwait(&mask, &signo);
    assert(err == 0);
    switch (signo) {
      case SIGINT:
        printf("
th1:INT.
");
        break;
      default:
        printf("
th1:unexcepted signal %d.
", signo);
        break;
    }
  }
}

在多线程中,一般安排专用线程处理信号,通过互斥量的保护,信号处理线程可以安全地改动数据。

fork

线程调用fork时,为子进程创建了整个进程地址空间的副本,同时还继承了互斥量、读写锁和条件变量的状态。为此,子进程返回后,如果不是马上调用exec,则需要清理锁的状态。因为子进程中只含有调用fork的那个线程的副本,父进程中其他占有锁的线程在子进程中不存在。

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

// Returns: 0 if OK, error number on failure
int pthread_atfork(void (*prepare)(void), void (*parent)(void),
                  void (*child)(void));
  • prepare由父进程在fork创建子进程前调用。任务是获取父进程定义的所有锁。
  • parent在fork创建子进程后、返回之前在父进程上下文中调用。任务是对获取的所有锁进行解锁。
  • child在fork返回前在子进程上下文中调用。任务是释放所有的锁。

可以多次调用该函数以设置多套fork处理程序。对于不需要的某个处理程序,可以传入空指针。多次调用时,parentchild以注册时的顺序执行,而prepare的执行顺序与注册时相反。

使用方法参考ch12/pthread_atfork.c。

参考

  1. pthread_kill 使用方法_vah101的专栏-CSDN博客
原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250294.html