• 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博客
  • 相关阅读:
    灵活修改页面控件
    玫瑰颜色各代表什么含义?
    转:TCP端口详解
    将数据导出成text文件
    将.net布署在运行Apache服务器上
    在多线程下请注意代码的安全
    OpenWrt 设置IP地址
    WordPress 内存缓存加速插件:Batcache
    PVE 开启https 及免费SSL证书申请
    获取OpenWrt mac地址
  • 原文地址:https://www.cnblogs.com/maxiaowei0216/p/14250294.html
Copyright © 2020-2023  润新知