[关闭]
@ltlovezh 2020-02-15T09:50:50.000000Z 字数 6161 阅读 1111

Posix pthread

C++ pthread


虽然C++ 11增加了标准线程库:std::thread。但是在一些不需要跨平台的场景,或者一些历史库中,依然大量使用着pthread库,所以两者都要学,两者都要硬。

基础知识

Posix Thread是操作系统级的API规范,用来定义线程及线程间同步操作,采用C语言定义,主要在unix like系统上实现。

首先看一个最简单的多线程例子:

  1. void *func(void *t) {
  2. // 获取创建线程传递的参数
  3. int arg = *((int *) t);
  4. cout << "Sleeping in thread " << endl;
  5. sleep(1);
  6. cout << "Thread with arg: " << arg << endl;
  7. }
  8. int main(){
  9. int arg = 10;
  10. pthread_t thread;
  11. int result = pthread_create(&thread, nullptr, func, (void *) &arg);
  12. if (result) {
  13. cout << "unable to create thread: " << result << endl;
  14. exit(-1);
  15. }
  16. // 阻塞主线程,等待子线程执行结束
  17. pthread_join(thread, nullptr);
  18. cout << "Main: program exiting." << endl;
  19. return 0;
  20. }

主线程首先通过pthread_create创建子线程,然后通过pthread_join等待子线程运行结束。如果没有pthread_join,那么程序会立即结束,程序结束后,所有子线程都会被终止(子线程的线程函数可能还没执行完)。

pthread_create的函数原型如下所示:

  1. // 创建pthread线程,返回0表示成功,否则表示失败
  2. int pthread_create(pthread_t _Nullable * _Nonnull __restrict,
  3. const pthread_attr_t * _Nullable __restrict,
  4. void * _Nullable (* _Nonnull)(void * _Nullable),
  5. void * _Nullable __restrict);

四个参数:

  1. 第一个参数:pthread_t*表示线程标识符指针,pthread_t是线程的唯一标识符。
  2. 第二个参数:pthread_attr_t *表示对线程设置的属性,后面详细介绍。
  3. 第三个参数:void*(void*)表示线程启动的线程函数,函数结束后,线程也将终止。
  4. 第四个参数:void*表示向线程函数传递的参数。

线程属性pthread_attr_t

通过pthread_create函数的第二个参数可以设置线程属性,该属性是pthread_attr_t结构体,如下所示:

  1. typedef struct {
  2. // 线程分离状态
  3. int detachstate;
  4. // 线程调度策略
  5. int schedpolicy;
  6. // 线程调度参数
  7. struct sched_param schedparam;
  8. // 线程继承策略
  9. int inheritsched;
  10. // 线程优先级作用域
  11. int scope;
  12. // 线程堆栈的保护区大小
  13. size_t guardsize;
  14. int stackaddr_set;
  15. // 线程堆栈地址
  16. void * stackaddr;
  17. // 线程堆栈大小
  18. size_t stacksize;
  19. }pthread_attr_t;

一般情况下,先通过pthread_attr_init初始化线程属性,此时所有线程属性都是默认值,然后通过pthread_create函数的第二个参数为线程设置定制属性值,最后通过pthread_attr_destroy销毁线程属性,下面看下各个线程属性。

detachstate

线程的分离状态决定一个线程以什么样的方式来终止自己,有两种取值:

可以通过以下函数设置或者获取线程的分离状态:

schedpolicy

线程的调度策略,有三种取值:

可以通过以下函数设置或者获取线程的调度策略:

schedparam

线程的调度参数,是一个包含了线程优先级的结构体:

  1. struct sched_param {
  2. // 线程优先级
  3. int sched_priority;
  4. char __opaque[__SCHED_PARAM_SIZE__];
  5. };

线程的优先级范围可以通过以下函数获取:

  1. // 线程的最低优先级,
  2. int sched_get_priority_min(int);
  3. // 线程的最高优先级
  4. int sched_get_priority_max(int);

函数参数是不同的调度策略,例如:SCHED_FIFO、SCHED_RR和SCHED_OTHER,可见不同调用策略下的线程优先级范围可能是不同的。

可以通过以下函数设置或者获取线程的调度参数:

调度策略和线程优先级是一件非常复杂的事情,如果不正确使用,很容易导致死锁,慎用。

inheritsched

线程针对调度策略和调度参数的继承策略,有两个取值:

可以通过以下函数设置或者获取线程的继承策略:

如果需要显式设置线程的调度策略和参数,那么必须在设置之前先将inheritsched属性设置为PTHREAD_EXPLICIT_SCHED

scope

线程优先级的作用域,即线程间竞争CPU的有效范围,有两个取值:

目前Linux只实现了PTHREAD_SCOPE_SYSTEM

可以通过以下函数设置或者获取线程优先级的作用域:

线程堆栈

线程可以共享进程内存空间,同时也有自己私有的堆栈空间。线程内的局部变量分配在私有堆栈上,线程堆栈的大小在线程创建时就确定了,若局部变量占用空间超过栈空间,就会引起coredump。

线程堆栈最小值是PTHREAD_STACK_MIN,一般是8K或者16K。可以通过pthread_attr_setstacksize设置堆栈大小,通过pthread_attr_getstacksize获取堆栈大小。这种情况下,由内核分配和释放堆栈内存。

除此之外,我们还可以自己管理堆栈内存,通过pthread_attr_setstackaddr设置堆栈地址,通过pthread_attr_getstackaddr获取堆栈地址。堆栈地址必须以Linux页面大小对齐,可以使用posix_memalign分配页面对齐的内存。此外,还可以通过pthread_attr_setstack设置堆栈地址和大小,通过pthread_attr_getstack获取堆栈地址和大小,下面看一个例子:

  1. int main(){
  2. pthread_attr_t attr;
  3. pthread_attr_init(&attr);
  4. size_t stacksize = -1;
  5. void *stack_addr = nullptr;
  6. // 获取缺省的堆栈地址和大小
  7. pthread_attr_getstack(&attr, &stack_addr, &stacksize);
  8. cout << "default stack addr: " << stack_addr << endl;
  9. cout << "default stack size: " << stacksize << endl;
  10. void *stackAddr = nullptr;
  11. //获取linux页大小
  12. int paseSize = getpagesize();
  13. // 设置的堆栈大小
  14. int size = paseSize * 2;
  15. cout << "paseSize: " << paseSize << endl;
  16. posix_memalign(&stackAddr, paseSize, size);
  17. // 设置新的堆栈地址和大小
  18. pthread_attr_setstack(&attr, stackAddr, size);
  19. // 获取当前堆栈地址和大小
  20. pthread_attr_getstack(&attr, &stack_addr, &stacksize);
  21. cout << "new stack addr: " << stack_addr << endl;
  22. cout << "new stack size: " << stacksize << endl;
  23. pthread_attr_destroy(&attr);
  24. return 0;
  25. }
  26. // 输出为
  27. default stack addr: 0x0
  28. default stack size: 524288
  29. paseSize: 4096
  30. new stack addr: 0x7f7f66002000
  31. new stack size: 8192

上述代码获取的默认的堆栈大小是512K,新设置的堆栈大小是8K。

guardsize

线程堆栈的保护区大小,即当线程堆栈不够用时,在堆栈的溢出端分配的额外内存。此额外内存的作用与缓冲区一样,可以防止栈溢出。

如果guardsize为0,则不会为线程提供溢出保护区。如果guardsize大于零,则会为对应线程提供至少guardsize字节的溢出保护区。缺省情况下,guardsize大于零。可以通过以下函数设置或者获取线程堆栈的保护区大小:

线程同步

pthread_mutex_t

pthread_mutex_t表示互斥量,用于控制同一时刻只能有一个线程访问临界区。
相关函数如下所示:

pthread_mutex_lockpthread_mutex_unlock一定要成对调用。

pthread_cond_t

pthread_cond_t表示条件变量,用于线程协同(同步)工作。

不管是在指定条件变量上wait,还是唤醒(signal和broadcast)指定条件变量上的线程,都必须先获得对应锁(pthread_mutex_t)。

wait的流程:

  1. pthread_mutex_lock
  2. pthread_cond_wait or pthread_cond_timedwait
  3. pthread_mutex_unlock

signal和broadcast的流程:

  1. pthread_mutex_lock
  2. pthread_cond_signal or pthread_cond_broadcast
  3. pthread_mutex_unlock

pthread_cond_timedwait使用范例:

  1. // 获取当前时间
  2. struct timeval now;
  3. gettimeofday(&now, nullptr);
  4. // 指定向后延迟5S(绝对时间)
  5. struct timespec spec;
  6. // 秒
  7. spec.tv_sec = now.tv_sec + 5;
  8. // 纳秒
  9. spec.tv_nsec = 0;
  10. // 先加锁
  11. pthread_mutex_lock(&mLock);
  12. // wait到指定的绝对时间:若被提前唤醒,则返回0,否则返回非0
  13. int result = pthread_cond_timedwait(&mCond, &mLock, &spec);
  14. // 释放锁
  15. pthread_mutex_unlock(&mLock);

参考文档

  1. Posix多线程编程—线程属性
  2. pthread_attr_init线程属性
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注