二、线程
1、线程的概念
一个正在运行的函数
posix线程是一套标准,而不是实现。
openmp线程
线程标识:pthread_t
线程,有时被称为轻量级进程(Lightweight Process,LWP),一个进程可以包含多个线程,同一个进程中的所有线程共享进程的地址空间.
在Linux的shell中运行ps –eLf即可看到很多线程,同一个进程中的线程的pid是一样的,但是他们的LWP(线程ID)却各不相同.
线程表示
比较两个线程ID是否相等:int pthread_equal(pthread_t t1, pthread_t t2);
返回值:相等返回非0,否则0
每一个线程都有一个ID,用下面的方法获得线程ID:pthread_t pthread_self(void);
返回值:返回调用线程ID
2、
2.1>线程的创建
线程的调度取决与调度器策略。
创建一个新线程:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数含义:
thread返回线程ID
attr是线程的属性,暂时可以指定为NULL(默认属性)
start_routine新线程的执行函数
arg传给新线程的参数
返回值:成功返回0,失败返回错误编号。
注意:在新线程中尽量不要使用erron这个全局变量(虽然每个线程其实都提供了errno的副本),新线程会继承调用线程的信号屏蔽字,但是新线程的挂起信号集被清除。
2.2>线程的终止:void pthread_exit(void *retval);
retva是线程的返回值
线程的3种终止方式:
1)线程从启动例程返回,返回值就是线程的退出码
2)线程可以被同一进程中的其他线程取消
3)线程调用pthread_exit()函数
pthread_join() ---> wait()
1.在线程中调用exit,_Exit,_exit函数会让整个进程都终止
2.如果线程收到了默认终止进程的信号,则整个进程终止
3.线程从执行函数返回,返回值是线程的退出值
4.线程被同一个进程中的其他线程取消
5.线程调用pthread_exit函数退出
2.3>阻塞等待同一个进程中的线程结束并获取其返回值(收尸):int pthread_join(pthread_t thread, void **retval);
返回值:成功返回0,失败返回错误编号。
如果线程已经被join过则该函数出错返回,如果线程已经结束,则该函数就不阻塞
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *func(void *p)
{
puts("Thread is wroking.");
sleep(10000);
pthread_exit(NULL);//终止
// return NULL;
}
int main()
{
pthread_t tid;
int err;
puts("Begin!");
err = pthread_create(&tid,NULL,func,NULL);//创建
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
pthread_join(tid,NULL);//收尸
puts("End!");
sleep(10000);
exit(0);
}
2.3>线程的取消选项
请求取消同一个进程中的其他线程(只是请求,线程不一定会终止):int pthread_cancel(pthread_t thread);
注意:只有线程执行到了取消点线程才会被取消,该函数一般不应该使用
线程有两种状态:允许和不允许
允许取消又分为:异步cancel,推迟cancel(默认)->推迟至cancel点再响应。
cancel点:POSIX定义的cancel点,都是可能引发阻塞的系统调用。
pthread_setcancelstate():设置是否允许取消
pthread_setcanceltype():设置取消方式
pthread_testcancel();本函数什么都不做,就是一个取消点。
2.3.1>把线程设置为分离状态:int pthread_detach(pthread_t thread);
置为分离状态的线程结束后内核会回收线程所占的资源,这种线程是不能被join的
2.4>栈的清理
2.4.1>安装线程退出清理函数:void pthread_cleanup_push(void (*routine)(void *), void *arg);
当遇到如下三种情况时候被安装的routine函数会被执行(可以安装多个,但执行顺序和安装顺序相反):
1.线程调用pthread_exit函数时
2.响应取消请求时
3.调用pthread_cleanup_pop(非零)时
注意:如果是调用return从线程返回时不会执行安装的线程清理函数
2.4.2>执行线程清理函数:void pthread_cleanup_pop(int execute);
如果execute是非0值,则最近安装的线程清理函数被执行
如果execute是0,则取消最近安装的线程清理函数,不执行
注意:pthread_cleanup_push和pthread_cleanup_pop一定要成对使用
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void cleanup_func(void *p)
{
puts(p);
}
void *func(void *p)
{
puts("Thread is wroking.");
pthread_cleanup_push(cleanup_func,"cleanup:1");
pthread_cleanup_push(cleanup_func,"cleanup:2");
pthread_cleanup_push(cleanup_func,"cleanup:3");
puts("push over!");
pthread_cleanup_pop(0);
pthread_cleanup_pop(1);
pthread_cleanup_pop(1);
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
int err;
puts("Begin!");
err = pthread_create(&tid,NULL,func,NULL);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
pthread_join(tid,NULL);
puts("End!");
exit(0);
}
//线程竞争实例
#include <stdio.h>
#include <stdlib.h>
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT-LEFT+1)
struct thr_arg_st
{
int n;
};
void *thr_primer(void *p)
{
int i,j,mark;
i =((struct thr_arg_st *)p)->n;
mark = 1;
for(j = 2 ; j <= i/2; j++)
{
if(i % j == 0)
{
mark = 0;
break;
}
}
if(mark)
printf("%d is a primer.\n",i);
pthread_exit(p);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
struct thr_arg_st *p;
for(i = LEFT; i <= RIGHT; i++)
{
p = malloc(sizeof(*p));
p->n = i;
err = pthread_create(tid+(i-LEFT),NULL,thr_primer,p);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
void *ptr;
for(i = LEFT; i <= RIGHT; i++)
{
pthread_join(tid[i-LEFT],&ptr);
free(ptr);
}
exit(0);
}
3、线程同步
线程同步的必要性:多个线程同时访问共享数据会发生错误,因此需要用锁来锁住代码的临界区.
3.1>互斥量
3.1.1互斥量:(锁)
互斥量的初始化
一种方法是静态初始化互斥量:pthread_mutex_t mutex =PTHREAD_MUTEX_INITIALIZER;
另外一种方法是调用如下函数:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
如果调用上面的函数初始化互斥量则需要用下面的函数来释放互斥量:int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁
如果加锁不成功便会阻塞等待:int pthread_mutex_lock(pthread_mutex_t *mutex);
注意:如果一个信号处理函数打断了pthread_mutex_lock(),该函数会自动的重新执行
如果加锁不成功出错返回:int pthread_mutex_trylock(pthread_mutex_t *mutex);
如果加锁不成功则阻塞,但当前时间等于abs_timeout时还没获得互斥量,函数出错返回(超时报错):int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);
互斥量解锁:int pthread_mutex_unlock(pthread_mutex_t *mutex);
//竞争故障
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#define THRNUM 20
#define FNAME "/tmp/out"
#define BUFSIZE 1024
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;//静态初始化互斥量
void *thr_add(void *p)
{
FILE *fp;
char buf[BUFSIZE];
fp = fopen(FNAME,"r+");
if(fp == NULL)
{
perror("fopen()");
exit(1);
}
pthread_mutex_lock(&mut);//加锁
fgets(buf,BUFSIZE,fp);
fseek(fp,0,SEEK_SET);//定位
// sleep(1);//放大竞争和冲突
fprintf(fp,"%d\n",atoi(buf)+1);
fclose(fp);
pthread_mutex_unlock(&mut);//解锁
pthread_exit(NULL);
}
int main()
{
int i,err;
pthread_t tid[THRNUM];
for(i = 0; i < THRNUM; i++)
{
err = pthread_create(tid+i,NULL,thr_add,NULL);
if(err)
{
fprintf(stderr,"pthread_create():%s\n",strerror(err));
exit(1);
}
}
for(i = 0; i < THRNUM; i++)
pthread_join(tid[i],NULL);
pthread_mutex_destroy(&mut);//释放互斥量
exit(0);
}
pthread_once();
3.1.2条件变量:(等待某一条件的发生,和信号一样。)
条件变量的初始化
3.1.1.1>一种方法是静态初始化pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
......2另外一种方法是调用如下函数:int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
......3如果用了上面的函数对条件变量进行了初始化,则需要用下面的函数进行销毁:int pthread_cond_destroy(pthread_cond_t *cond);
......4在该条件变量上睡眠等待其他线程工作完成通知(如果已经通知过了则不再睡眠):int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
注意:如果信号处理函数打断了pthread_cond_wait()(参见POSIX线程-条件变量),该函数要么自动重新自行(linux是这样实现的),或者返回0(这时应用要检查返回值,判断是否为假唤醒)。
......5在该条件变量上睡眠等待其他线程工作完成通知(如果已经通知过了则不再睡眠),但如果当前时间等于abstime时还没有等到完成信号,则函数出错返回
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
注意:在使用pthread_cond_wait和pthread_cond_wait_timedwait时,一定要先获得mutex锁,如果这两个函数由于条件变量cond要睡眠则会先释放mutex锁再睡眠,当然函数返回时会再次加锁mutex,那么在锁住mutex和调用这两个函数之间我们就可以判断一个条件(判断是否要调用这两个函数来睡眠等待),比如:
pthread_mutex_lock(&mutex);
while(a != 1)
pthread_cond_wait(&cond, &mutex)
pthread_mutex_unlock(&mutex)
通知其他线程工作完成(即发送完成信号)
......6唤醒至少一个由于该条件变量而睡眠的线程:int pthread_cond_signal(pthread_cond_t *cond);
......7唤醒所有由于该条件变量而睡眠的线程(但有些线程可能会因为得不到锁而继续睡眠):int pthread_cond_broadcast(pthread_cond_t *cond);
注意:pthread_cond_wait函数返回的条件是收到唤醒信号并且成功加锁,不然会继续睡眠
注意:多个线程调用pthread_cond_wait(&cond, &mutex)的时候,不要在一个条件变量上用多个锁(实测在调用pthread_cond_broadcast时候会出问题)
3.1.3信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
mysem例子
3.1.4读写锁
读锁相当于共享锁,写锁相当于互斥锁。
read 读 --> 共享
write 写 --> 互斥
4、线程属性
//线程属性结构如下:
typedef struct
{
int etachstate; //线程的分离状态
int schedpolicy; //线程调度策略
structsched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置
size_t stacksize; //线程栈的大小
}pthread_attr_t;
默认的属性为非绑定、非分离、缺省1M的堆栈、与父进程同样级别的优先级。
初始化线程属性:int pthread_attr_init(pthread_attr_t *attr);
销毁线程属性:int pthread_attr_destroy(pthread_attr_t *attr);
设置线程分离属性:int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
获取线程分离属性:int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);
线程同步的属性
互斥量属性
初始化互斥量属性:int pthread_mutexattr_init(pthread_mutexattr_t *attr);
反初始化互斥量属性:int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);
获得互斥量的类型属性:int pthread_mutexattr_gettype(const pthread_mutexattr_t *restrict attr, int *restrict type);
设置互斥量的类型属性:int pthread_mutexattr_settype(pthread_mutexattr_t *attr, int type);
5、重入
多线程站中的IO
线程与信号
修改线程的信号屏蔽字:int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
这个函数改变调用线程的信号屏蔽字,改变之后再创建的新线程都继承当前信号屏蔽字。
等待信号:int sigwait(const sigset_t *restrict set, int *restrict sig);
该函数会把set中的信号解除屏蔽然后阻塞等待set中的信号出现,信号出现后函数返回并且会恢复调用该函数之前的信号屏蔽字,如果在调用该函数前信号就发生了,
则这个函数不会阻塞,sig中返回收到的信号.
注意:在调用sigwait函数之前一定要先阻塞set中的信号
发送信号到特定线程:int pthread_kill(pthread_t thread, int sig);
如果是一个会杀死进程的信号,则线程收到信号后进程会终止。
在多线程程序中处理信号我们一般采用以下方案:单独启动一个线程调用sigwait等待信号和处理信号,其他线程都阻塞要处理的信号。
线程与fork
在一个多线程的进程中调用fork时候很容易引起死锁,因为fork会为子进程复制父进程的整个页表,
因此父进程的互斥量、读写锁、条件变量(以及他们当时的状态)等都会被复制到子进程,
但是只有调用fork的单一线程被复制到子进程(其他执行线程不复制),因此子进程很有可能是由于死锁而一直等待,
原因是子进程不太清楚锁的状态,万一在fork之前某一个执行线程获得了锁而fork之后调用的函数中也要获得锁,
那么这时候子进程就死锁了(因为在子进程中没有线程释放锁)在fork之后立马调用exec的话就没有问题.
Fork处理函数:int pthread_atfork(void (*prepare)(void), void (*parent)(void), void (*child)(void));
调用这个函数可以安排三个处理函数:
Prepare函数会在fork的正式代码开始执行之前被调用
Parent函数会在创建子进程之后fork返回之前由父进程调用
Child函数会在创建子进程之后fork返回之前由子进程调用
利用pthread_atfork提供的机制,我们可以在prepare函数中获得所有锁(确保fork出的子进程的锁的状态是确定的),
在parent中释放所有锁(只是父进程的),在child中释放所有锁(只是子进程的),
这样的话子进程中的锁就都是空闲的状态(没有被加锁),接下来子进程便可以正常使用从父进程中复制过来的锁了。
注意:但是pthread_atfork并不能解决条件变量的带来的问题,所有pthread_atfork函数还是不完善的,
因此在创建多个线程之后尽量不要fork(除非立即调用exec),如果多线程和多进程不得不共存,那么尽量在创建线程之前先创建好所有进程。
openmp --> www.OpenMP.org