POSIX 多线程开发
一 线程的创建
POSIX API中,创建线程的函数是 pthread_create,声明如下:
int pthread_create(pthread_t *pid, const pthread_attr_t *attr, (void*) (*start_routine)(void*), void *arg);
其中,参数Pid指向返回线程的ID,attr是指向属性的指针,如果为NULL,则使用默认属性。start_routine是线程函数的地址,arg指向传给线程函数的参数。如果执行成功,返回0。
POSIX 还提供了pthread_join函数来等待子线程结束,并回收子线程资源。声明如下:
int pthread_join(pthread_t pid, void **value_ptr);
value_ptr用来获得子线程的返回值。如果执行成功,返回0。
举例:创建一个线程,并传递结构体作为参数
#include <pthread.h>
#include <stdio.h>
typedef struct
{
int n;
char *str;
}MYSTRUCT;
void *ThreadFun(void *arg)
{
MYSTRUCT *p = (MYSTRUCT *)arg;
printf("in Thread function: n = %d, str = %s\n", p->n, p->str);
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t threadId;
int ret;
MYSTRUCT mystruct;
mystruct.n = 10;
mystruct.str = (char *)("hello world");
ret = pthread_create(&threadId, NULL, ThreadFun, (void *)&mystruct);
if (ret)
{
printf("pthread_create failed: %d\n", ret);
return -1;
}
pthread_join(threadId, NULL);
printf("in main: thread is created\n");
return 0;
}
终端编译时,需要连接linux下pthread库,输入命令:
g++ -o test test.cpp -lpthread
二 线程的属性
POSIX标准规定线程有多个属性。主要包括:分离状态、调试策略和参数、作用域、栈尺寸、栈地址、优先级等。Linux将线程属性用一个联合体 pthread_attr_t 表示,用函数 pthread_getattr_np 来获取结构体值。声明如下:
#define _GNU_SOURCE // 必须在头文件之前定义宏
#include <pthread.h>
int pthread_getattr_np(pthread_t thread, pthread_attr_t *attr);
当获得的属性的结构体不再需要的时候,要用函数 pthread_attr_destroy进行销毁,从而释放相关的资源。
int pthread_attr_destroy(pthread_attr_t *attr);
另外提供 pthread_attr_init 函数来初始化一个线程属性结构体。声明如下:
int pthread_attr_init(pthread_attr_t *attr);
1 分离状态
POSIX线程的分离状态决定一个线程以什么样的方式来终止自己。分离状态只有两种:
- 分离的(detached):一个分离的是不可能被其它线程回收或杀死的,它所占的资源在终止时由系统自动释放。
- 可连接的(joinable):默认情况下创建的线程是可连接的,一个可连接的线程结束时不会自动回收线程资源(线程所占用的堆栈、线程描述符等),必须等待其它线程通过 pthread_join 函数回收。
将线程转为可分离线程可以通过 pthread_detach 函数 或者通过 函数 pthread_attr_setdetachstate 设置线程分离状态属性。声明如下:
int pthread_detach(pthread_t thread);
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
POSIX还提供了 pthread_exit 函数让线程退出。但是:当主线程使用此函数时,并不会结束进程,如果有其它创建正在运行的话,它只会提前终止主线程。
举例:创建一个可分离的线程,且 main 线程先退出。
#include <iostream>
#include <pthread.h>
using namespace std;
void *ThreadFun(void *arg)
{
cout << "sub thread is running" << endl;
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t threadId;
pthread_attr_t threadAttr;
int res;
res = pthread_attr_init(&threadAttr);
if (res)
{
cout << "pthread_attr_init failed" << endl;
}
res = pthread_attr_setdetachstate(&threadAttr, PTHREAD_CREATE_DETACHED); //设置分离状态
if (res)
{
cout << "pthread_attr_setdetachstate failed:" << res << endl;
}
res = pthread_create(&threadId, &threadAttr, ThreadFun, NULL);
if (res)
{
cout << "pthread_create failed:" << res << endl;
}
cout << "main thread will exit" << endl;
pthread_exit(NULL); // 主线程退出,但进程不会退出,下面的语句不会再执行。
cout << "main thread has exited, this line will not run\n" << endl;
return 0;
}
举例:把可连接线程转换为可分离线程。
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#endif
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
static void *ThreadStart(void *arg)
{
int i,s;
pthread_attr_t pthreadAttr;
s = pthread_getattr_np(pthread_self(), &pthreadAttr);
if (s != 0)
{
printf("pthread_getattr_np failed\n");
}
s = pthread_attr_getdetachstate(&pthreadAttr, &i);
if (s)
{
printf("pthread_attr_getdetachstate failed\n");
}
printf("Detach state = %s\n",
(i == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" :
(i == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" :
"???");
pthread_detach(pthread_self()); // 转换线程为可分离线程
s = pthread_getattr_np(pthread_self(), &pthreadAttr);
if (s != 0)
{
printf("pthread_getattr_np failed\n");
}
s = pthread_attr_getdetachstate(&pthreadAttr, &i);
if (s)
{
printf("pthread_attr_getdetachstate failed\n");
}
printf("after pthread_detach,\n Detach state = %s\n",
(i == PTHREAD_CREATE_DETACHED) ? "PTHREAD_CREATE_DETACHED" :
(i == PTHREAD_CREATE_JOINABLE) ? "PTHREAD_CREATE_JOINABLE" :
"???");
pthread_attr_destroy(&pthreadAttr); // 销毁属性
}
int main(int argc, char *argv[])
{
pthread_t threadID;
int s;
s = pthread_create(&threadID, NULL, &ThreadStart, NULL);
if (s != 0)
{
printf("pthread_create failed\n");
return 0;
}
pthread_exit(NULL); // 主线程退出,但进程并不马上结束
}
2 栈尺寸
线程的默认堆栈大小是1MB,局部变量、函数参数、返回地址等都存放在栈空间里,动态分配的内存属于堆空间。因此,线程函数中的局部变量大小不能超过默认栈尺寸大小。获取线程栈尺寸属性的函数是 pthread_attr_getstacksize,声明如下:
int pthread_attr_getstacksize(pthread_attr_t *attr, size_t *stacksize);
stacksize的单位是字节,如果执行成功,返回0,否则返回错误码。
举例:获得线程默认栈尺寸和最小尺寸
#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* To get pthread_getattr_np() declaration */
#endif
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
static void *ThreadFunc(void *arg)
{
int i,res;
size_t stackSize;
pthread_attr_t attr;
res = pthread_getattr_np(pthread_self(), &attr);
if (res)
{
printf("pthread_getattr_np failed\n");
}
res = pthread_attr_getstacksize(&attr, &stackSize);
if (res)
{
printf("pthread_attr_getstacksize failed\n");
}
printf("Default stack size is %u byte; minimum is %u byte;\n", stackSize, PTHREAD_STACK_MIN);
pthread_attr_destroy(&attr);
}
int main(int argc, char *argv[])
{
pthread_t threadID;
int res;
res = pthread_create(&threadID, NULL, &ThreadFunc, NULL);
if (res)
{
printf("pthread_create failed\n");
return 0;
}
pthread_join(threadID, NULL);
return 0;
}
运行结果:
Default stack size is 8392704 byte; minimum is 16384 byte;
3 调度策略
如何管理多个线程去抢占CPU,这就是线程的调度。
Linux下线程的调度策略有三种:
SHED_OTHER(分时调试策略):这是一种非实时调试策略,系统会为每个线程分配时间片。没有优先级之分,每个线程轮流占用CPU。
SHED_FIFO(先来先服务调度策略):支持优先级抢占。CPU会让一个先来的线程执行完再调度下一个线程,顺序就是按照创建线程的先后。线程一旦占用CPU就会一直运行,直到有更高优先级的任务到达或自己放弃CPU。如果有和正在运行的线程具有同样优先级的线程已就绪,就必须等等正在运行的线程主动放弃后才可以运行这个就绪的线程。
SHED_RR(实时调度策略,时间片轮转):支持优先级抢占,因此也是一种实时调试策略。CPU会给每个线程分配一个特定的时间片,当线程的时间片用完时,系统将重新分配时间片,并将线程置于实时线程就绪队列的尾部,保证其它相同优先级的线程能被公平的调度。
获取调度策略最小和最大优先级的函数是 sched_get_priority_min 和 sched_get_priority_max,声明如下:
int sched_get_priority_min (int policy);
int sched_get_priority_max (int policy);
policy的取值可以为SHED_OTHER、SHED_FIFO、SHED_RR。
举例:获取线程3种调度策略下可设置的最小和最大优先级
#include <stdio.h>
#include <sched.h>
int main()
{
printf("Valid priority range for SCHED_OTHER: %d - %d\n",
sched_get_priority_min(SCHED_OTHER),
sched_get_priority_max(SCHED_OTHER));
printf("Valid priority range for SCHED_FIFO: %d - %d\n",
sched_get_priority_min(SCHED_FIFO),
sched_get_priority_max(SCHED_FIFO));
printf("Valid priority range for SCHED_RR: %d - %d\n",
sched_get_priority_min(SCHED_RR),
sched_get_priority_max(SCHED_RR));
return 0;
}
运行结果:
Valid priority range for SCHED_OTHER: 0 - 0
Valid priority range for SCHED_FIFO: 1 - 99
Valid priority range for SCHED_RR: 1 - 99
三 线程的结束
线程结束有以下四种方式:
- 在线程函数中调用了pthread_exit函数。
- 线程所属的进程结束了,比如进程调用了exit。
- 线程函数执行结束后返回了。
- 线程被同一进程中的其他线程通知结束或取消。
其中1和3是线程主动结束,2,4属于被动结束,如果被动结束时线程资源没有回收,就会引发资源泄漏。
pthread_exit函数声明如下:
void pthread_exit (void *retval);
retval是返回值,类型是void *,另外,在main线程中调用 pthread_exit 函数时,将结束 main 线程,但进程并不立即退出。线程间发送信号的函数是 pthread_kill,它需要接收信号的线程先用 sigaction 函数注册该信号的处理函数。声明如下:
void pthread_kill (pthread_t threadId, int signal);
signal 通常大于0,如果等于0就是用来检测线程是否存在。函数如果执行成功,返回0,否则返回错误码。如果没注册信号处理函数,则整个进程退出。
另外还有其它两个函数,pthread_cancel 函数:取消线程的执行(也是发一个请求,不一定成功。) pthread_testcancel 函数:检查是否有取消请求发送给当前线程,如果有,就算是在死循环中,也能将线程停止下来。声明如下:
void pthread_cancel (pthread_t threadId);
void pthread_testcancel (void);
举例:通知取消线程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void *ThreadFun(void *arg)
{
int i = 1;
printf("thread start ---------------\n");
while (1)
{
i++;
pthread_testcancel(); // 让系统测试取消请求
}
return NULL;
}
int main()
{
void *ret = NULL;
int res = 0;
pthread_t threadId;
res = pthread_create(&threadId, NULL, ThreadFun, NULL);
if (res)
{
printf("pthread_create failed\n");
return 0;
}
sleep(1);
pthread_cancel(threadId);
pthread_join(threadId, &ret);
if (ret == PTHREAD_CANCELED)
{
printf("thread has stopped, and exit code is %d\n", ret);
}
else
{
printf("some error occured\n", ret);
}
return 0;
}
运行结果:
thread start ---------------
thread has stopped, and exit code is -1
四 线程退出时的清理机会
如何保证线程终止时能顺利的释放掉自己所占用的资源,特别是锁资源,是线程退出时要解决的问题。
POSIX线程库提供了函数 pthread_cleanup_push 和 pthread_cleanup_pop。前者会把一个函数压入清理函数调用栈,后者用来弹出栈顶的清理函数,并根据参数来决定是否执行清理函数。两个 函数必须成对出现在同一函数中,否则编译不过。声明如下:
void pthread_cleanup_push (void (*routine)(void *), void *arg);
void pthread_cleanup_pop (int execute);
execute 取0时表示不执行清理函数,非0时执行清理函数。pthread_cleanup_push 压入的函数会在下面3种情况下执行:
- 线程主动结束时,比如return 或 pthread_exit。
- 调用pthread_cleanup_pop并且其参数不为0时。
- 线程被其它线程取消时,也就是有其它的线程对该线程调用pthread_cancel函数。
举例:pthread_cleanup_pop调用清理函数
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void CleanFunc(void *arg)
{
printf("clean func num: %d\n", *(int *)arg);
}
void *ThreadFun(void *arg)
{
int n = 1;
int m = 2;
printf("sub thread begin\n");
pthread_cleanup_push(CleanFunc, &m);
pthread_cleanup_push(CleanFunc, &n);
pthread_cleanup_pop(1); // 保证编译通过
pthread_cleanup_pop(1); // 虽然两次都是取1,但是因为thread正常return,所以会调用清理函数两次。
return NULL;
}
int main(void)
{
pthread_t threadID;
int res;
res = pthread_create(&threadID, NULL, ThreadFun, NULL);
if (res)
{
printf("pthread_create failed\n");
return 0;
}
pthread_join(threadID, NULL);
return 0;
}
运行结果:
sub thread begin
clean func num: 1
clean func num: 2