线程概念
线程就像是一个正在运行的函数,多线程就是一个正在运行的程序中有多个函数正在同时运行。有点像main函数
现在用的比较多的是posix线程,这里的posix指的是一套标准,而不是实现。还有另一套标准叫openmp。
有个线程标识:pthread_t,p表示是posix下的,thread表示线程。这个类型并不确定,可能是个整型,也可能是结构体、共用体。
因为类型不确定,所以一般不同标准都有自家的比较函数:
int pthread_equal(pthread_t t1, pthread_t t2);
用于比较两个线程id是否相同,如果相同则返回非0值,否则返回0
还有个函数用于获取当前线程的标识
pthread_t pthread_self(void);
注意:一般使用线程的程序编译链接时都需要加上选项-pthread
线程创建
虽说是创建线程,实际上是在确定调用该线程函数的入口点
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
第一个参数是创建的线程的标识。如果要确定线程的属性,则人为定义一个pthread_attr_t结构体然后传给attr,不需要可以设定为NULL。第三个参数表示新创建的线程从传入函数的地址开始运行,如果这个函数需要参数,则由第四个参数arg传过去。
如果成功返回0,失败则返回一个error number。所以现在用strerror来报错
使用样例
static void *func()
{
puts("Thread is working!");
return NULL;
}
int main()
{
pthread_t tid;
puts("Begin!");
int err = pthread_create(&tid, NULL, func, NULL)
if (err)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
puts("End!");
exit(0);
}
这里Thread is working!
可能不显示,可能显示在begin和end之间,也可能显示在end之后。原因是线程的调度取决于调度器的调度策略
线程终止
有三种方式终止线程:
- 线程从启动例程返回,返回值就是线程的退出码
- 线程可以被同一进程中的其他线程取消
- 线程调用pthread_exit()函数。如果这个线程是进程中最后一个线程,则线程也会终止
看看pthread_exit()函数
void pthread_exit(void *retval);
函数唯一的参数是函数的返回码,类似于exit(0)的0,只要pthread_join的第二个参数不是NULL,那么这个值会被传递给pthread_join的第二个参数。那么pthread_join又是什么函数呢?
这是个类似于进程中wait的函数
int pthread_join(pthread_t thread, void **retval);
wait会阻塞自己然后回收子进程,但是不会管是哪个子进程。pthread_join不一样,第一个参数会指定回收哪个线程,当然也是一直等待指定的线程结束自己才结束。
使用样例
改进之前写的函数
static void *func()
{
puts("Thread is working!");
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
puts("Begin!");
int err;
if ((err = pthread_create(&tid, NULL, func, NULL)) != 0)
{
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
pthread_join(tid, NULL);
puts("End!");
exit(0);
}
栈清理
线程可以安排他退出时需要调用的函数,这样的函数称为线程清理处理程序(类似于钩子函数atexit)。线程可以建立多个清理处理程序。处理程序记录在栈中,也就是说他们的执行顺序与他们注册的顺序相反。使用下面的函数来操作这个栈:
void pthread_cleanup_push(void (*routine)(void *), void *arg);
//传递函数,和函数的参数
void pthread_cleanup_pop(int execute);
//pop会弹出上次建立的清理函数,但是会根据execute的值决定调不调用这个函数
//execute是1则执行,否则不执行
这两个函数都是用宏实现的,而push宏中有一个左大括号,pop宏中有个右大括号,也就是说这两个函数必须成对使用,否则会出现语法错误。
static void cleanup_func(void *p)
{
puts(p);
}
static void *func(void *p)
{
puts("Thread is working");
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(1);
pthread_cleanup_pop(0);
pthread_cleanup_pop(0);
pthread_exit(NULL);
}
int main()
{
pthread_t tid;
puts("Begin!");
int 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);
}
输出:
Begin!
Thread is working
push over
cleanup:3
End!
线程取消
(在进程终止中的三条异常终止中有一项是最后一个线程对其取消请求做出响应
)
一般情况下,线程在其主体函数退出的时候会自动终止,但同时也可以因为接收到另一个线程发来的取消请求而强制终止,这就是线程取消
int pthread_cancel(pthread_t thread);
有个问题,如果一个要被取消的进程在取消之前打开了一个文件而没有关闭怎么办?可以用到我们上面介绍的pthread_cleanup_push/pop函数,把关闭操作放到栈中即可。但是治标不治本啊,如果在push之前被取消了怎么办?这里了解一下线程的取消机制:
线程取消的方法是向目标线程发Cancel信号,但目标信号有两种状态:允许取消和不允许取消,其中允许取消又分为异步取消和推迟取消。一般默认是推迟取消,意思是推迟到cancel点来响应。异步取消是可以在任何时候被取消。
什么是cancel点?在POSIX标准中,会引起阻塞的系统调用如read()、write()、pthread_join()、sigwait()等都是cancel点。到这里我们就知道答案了,由于push并不是一个阻塞的系统调用,所以收到信号后线程并不会被取消,而会向下寻找cancel点再取消。
根据上面的线程机制有几个函数
//设置是否允许取消
int pthread_setcancelstate(int state, int *oldstate);
//state取值如下
//PTHREAD_CANCEL_ENABLE 可取消
//PTHREAD_CANCEL_DISABLE 不可取消
//设置取消方式,即异步取消还是推迟取消
int pthread_setcanceltype(int type, int *oldtype);
//PTHREAD_CANCEL_DEFERRED 推迟取消
//PTHREAD_CANCEL_ASYNCHRONOUS 异步取消
//什么都不做,就表示为一个取消点
void pthread_testcancel(void);
线程分离
用于将一个线程分离
int pthread_detach(pthread_t thread);
linux中线程有两种状态,joinable和unjoinable。如果线程是joinable,当线程终止时不会释放资源,只有pthread_join后资源才会被释放回收。如果是unjoinable,线程终止后会自动释放资源,不需要join来回收。调用detach相当于和这个线程绝交,它的死活就不归我管了。
因为调用join必须要阻塞自己然后等待,但有些时候要求不仅要回收线程,还要在等待的过程中处理别的事,这时候就可以用pthread_detach
线程小实例
改写以前用200个子进程分别判断200个数是否为质数的程序,现在用200个线程
#define LEFT 30000000
#define RIGHT 30000200
#define THRNUM (RIGHT - LEFT + 1)
static void *thr_prime(void *p)
{
int n = *(int *)p, flag = 1;
for (int i = 2; i * i <= n; i++)
{
if (n % i == 0)
{
flag = 0;
break;
}
}
if (flag)
printf("%d is a prime\n", n);
pthread_exit(NULL);
}
int main()
{
pthread_t tid[201];
struct thr_arg_st *p;
void *ptr;
int i, j;
for (i = LEFT; i < RIGHT; i++)
{
int err = pthread_create(tid + i - LEFT, NULL, thr_prime, &i);
if (err)
{
//出错之后回收之前的线程
for (int j = 0; j < i; j++)
{
pthread_join(tid[j - LEFT], NULL);
free(ptr);
}
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
//回收线程
for (i = LEFT; i < RIGHT; i++)
pthread_join(tid[i - LEFT], NULL);
exit(0);
}
上面的程序看似没有问题,其实有个非常隐蔽且严重的问题——竞争。可以看到每个线程函数的参数都是一个指针,创建线程时传给这个参数的是for循环中i的地址,而这个i在内存中位置是固定的,也就是说会有201个线程在使用同一个地址,这会导致数据混乱。有人说把int i
放在for循环中不就行了,也不行!循环中的临时变量每次地址变不变不能保证,这要看编译选项,操作系统等多种因素,万一不变呢?所以要想每个线程都能拿到独立的空间,最好用动态内存
static void *thr_prime(void *p)
{
int n = *(int *)p, flag = 1;
for (int i = 2; i * i <= n; i++)
{
if (n % i == 0)
{
flag = 0;
break;
}
}
if (flag)
printf("%d is a prime\n", n);
pthread_exit(p);
}
int main()
{
pthread_t tid[THRNUM];
int *nump;
void *ptr;
for (int i = LEFT; i < RIGHT; i++)
{
nump = malloc(sizeof(*nump));
if (nump == NULL)
{
perror("malloc()");
exit(1);
}
*nump = i;
int err = pthread_create(tid + i - LEFT, NULL, thr_prime, nump);
if (err)
{
for (int j = 0; j < i; j++)
{
pthread_join(tid[j - LEFT], &ptr);
free(ptr);
}
fprintf(stderr, "pthread_create():%s\n", strerror(err));
exit(1);
}
}
for (int i = LEFT; i < RIGHT; i++)
{
pthread_join(tid[i - LEFT], &ptr);
free(ptr);
}
exit(0);
}
free最好和malloc一个模块,所以这里用pthread_exit(p);
将p的地址返回给pthread_join(tid[j - LEFT], &ptr);
的ptr,然后由ptr进行free。
线程同步
上面讲到了线程存在竞争和冲突,能解决这一问题就要靠线程同步,其中有一个机制——互斥量
互斥量
学过计算机系统的都知道,这里简单介绍下。其实就像一间屋子,假设线程只有在屋子里才能执行,且屋子只能容纳一个线程。屋子门口有一把锁,每个线程进去都把锁锁上,执行完后自己释放并解锁,由下一个线程进来再锁上,以此循环。其中,锁就是互斥量。互斥量操作需要下面的函数:
//以动态方式创建互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
//mutexattr指定了互斥锁的属性,如果是NULL则为默认属性
//还有以静态方式创建互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
//上锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//尝试上锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
//trylock是lock的非阻塞版。如果互斥锁已经被上锁了,再调用lock会阻塞自己
//直到互斥锁可用。而trylock会返回值描述互斥锁的状况
多线程互斥实例——abcd
实现一个程序不停的按照abcd这个顺序输出这四个字符。这里需要有顺序的输出,如果只是单纯的加互斥锁,那可能输出没有顺序(线程的调度依赖调度机制而定)。这里需要使用锁链。
static int next(int n)
{
if (n + 1 == THRNUM)
return 0;
return n + 1;
}
static void *thr_func(void *p)
{
int n = (int)p;
char c = 'a' + n;
while (1)
{
//因为每个互斥锁都加过锁了,这里会阻塞
pthread_mutex_lock(mut + n);
write(1, &c, 1);
pthread_mutex_unlock(mut + next(n));
//输出过后会给下一个互斥锁解锁,让下一个互斥锁的线程解除阻塞,以此形成锁链
}
pthread_exit(NULL);
}
int main()
{
pthread_t tid[THRNUM];
for (int i = 0; i < THRNUM; i++)
{
pthread_mutex_init(mut + i, NULL);
pthread_mutex_lock(mut + i);
//给每个互斥锁都锁上
int err = pthread_create(tid + i, NULL, thr_func, (void *)i);
if (err)
{
for (int j = 0; j < i; j++)
pthread_join(tid[j], NULL);
fprintf(stderr, "pthread_create(): %s", strerror(err));
exit(1);
}
}
//到for循环结束,每个线程现在都是阻塞状态
//解锁第一个互斥锁,同时第一个互斥锁线程解除阻塞
pthread_mutex_unlock(mut + 0);
alarm(1); //就输出1秒
for (int i = 0; i < THRNUM; i++)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(mut);
exit(0);
}
多线程互斥实例——mytbf
用多线程实现令牌桶。其中一个线程在旁边每过一秒就给数组中每个令牌桶的令牌减去cps
//mytbf.h
#ifndef MYTBF_H_
#define MYTBF_H_
#define MYTBF_MAX 1024
typedef void mytbf_t;
mytbf_t *mytbf_init(int cps, int burst);
int mytbf_fetchtoken(mytbf_t *, int);
int mytbf_returntoken(mytbf_t *, int);
int mytbf_destroy(mytbf_t *);
#endif
//mytbf.c
static int inited = 0;
static pthread_once_t init_once = PTHREAD_ONCE_INIT;
static pthread_t tid_alrm;
static pthread_mutex_t mut_job = PTHREAD_MUTEX_INITIALIZER;
static struct mytbf_st *job[MYTBF_MAX];
struct mytbf_st
{
int cps;
int burst;
int token;
int pos;
pthread_mutex_t mut;
//每个令牌桶都有一个互斥量
//一秒之后每个令牌桶都互斥的执行token加减
};
void *thr_alrm(void *p) //线程,实现之前alarm信号函数的功能
{
while (1)
{
pthread_mutex_lock(&mut_job);
for (int i = 0; i < MYTBF_MAX; i++)
{
if (job[i] != NULL)
{
pthread_mutex_lock(&job[i]->mut);
job[i]->token += job[i]->cps;
if (job[i]->token > job[i]->burst)
job[i]->token = job[i]->burst;
pthread_mutex_unlock(&job[i]->mut);
}
}
pthread_mutex_unlock(&mut_job);
sleep(1);
}
}
static void module_unload()
{
pthread_cancel(tid_alrm);
pthread_join(tid_alrm, NULL);
for (int i = 0; i < MYTBF_MAX; i++)
if (job[i] != NULL)
mytbf_destroy(job[i]);
pthread_mutex_destroy(&mut_job);
}
static void module_load()
{
int err = pthread_create(&tid_alrm, NULL, thr_alrm, NULL);
if (err)
{
fprintf(stderr, "pthread_create():%s", strerror(err));
exit(1);
}
atexit(module_unload);
}
static int get_free_pos_unlocked()
{
for (int i = 0; i < MYTBF_MAX; i++)
{
if (job[i] == NULL)
return i;
}
return -1;
}
static int min(int a, int b) { return a > b ? b : a; }
mytbf_t *mytbf_init(int cps, int burst)
{
struct mytbf_st *me;
/*
如果用之前这个写法,在多线程环境中
可能刚执行module_load()还没执行inited = 1时又来一个线程
此时inited还是0,还是会执行if。也就是说这段代码不原子
if (!inited)
{
module_load();
inited = 1;
}
*/
//用pthread_once代替上面的代码,使得module_load只执行一次
pthread_once(&init_once, module_load);
me = malloc(sizeof(*me));
if (me == NULL)
return NULL;
me->token = 0;
me->cps = cps;
me->burst = burst;
pthread_mutex_init(&me->mut, NULL);
//这里job数组只有一个,所以操作job数组的代码肯定都要放在临界区中
//而临界区代码越少越好,所以除pos的操作外都放在临界区外
pthread_mutex_lock(&mut_job);
int pos = get_free_pos_unlocked();
if (pos < 0)
{
pthread_mutex_unlock(&mut_job);
free(me);
return NULL;
}
me->pos = pos;
job[pos] = me;
pthread_mutex_unlock(&mut_job);
return me;
}
int mytbf_fetchtoken(mytbf_t *ptr, int size)
{
if (size <= 0)
return -EINVAL;
struct mytbf_st *me = ptr;
//对me的内容进行操作,防止多个线程操作同一个me,这里进行互斥处理
pthread_mutex_lock(&me->mut);
while (me->token <= 0)
{
//因为token不够,所以需要解锁,让能增加token的函数来抢到锁
pthread_mutex_unlock(&me->mut);
sched_yield();
pthread_mutex_lock(&me->mut);
}
int get = min(me->token, size);
me->token -= get;
pthread_mutex_unlock(&me->mut);
return get;
}
int mytbf_returntoken(mytbf_t *ptr, int size)
{
if (size <= 0)
return -EINVAL;
struct mytbf_st *me = ptr;
//对me的内容进行操作,防止多个线程操作同一个me,这里进行互斥处理
pthread_mutex_lock(&me->mut);
me->token += size;
if (me->token > me->burst)
me->token = me->burst;
pthread_mutex_unlock(&me->mut);
return size;
}
int mytbf_destroy(mytbf_t *ptr)
{
struct mytbf_st *me = ptr;
pthread_mutex_lock(&mut_job);
job[me->pos] = NULL;
pthread_mutex_unlock(&mut_job);
pthread_mutex_destroy(&me->mut);
free(ptr);
return 0;
}
//main.c
//出于简便,代码将函数出错的处理都进行了删除
#define CPS 10
#define BUFSIZE CPS
#define BURST 100
int main(int argc, char *argv[])
{
mytbf_t *tbf = mytbf_init(CPS, BURST);
int fds = open(argv[1], O_RDONLY), fdd = 1;
char buf[BUFSIZE];
int pos;
long int len = 0L, ret = 0L;
while (1)
{
int get = mytbf_fetchtoken(tbf, BUFSIZE);
if (len == 0)
break;
if (get - len > 0)
mytbf_returntoken(tbf, get - len);
pos = 0;
while (len > 0)
{
ret = write(fdd, buf + pos, len);
if (ret < 0)
{
perror("write()");
exit(1);
}
pos += ret;
len -= ret;
}
}
close(fds);
mytbf_destroy(tbf);
exit(0);
}
改成多线程版后出现了新的问题——忙等待。在mytbf_fetchtoken中有段代码:
while (me->token <= 0)
{
pthread_mutex_unlock(&me->mut);
sched_yield();
pthread_mutex_lock(&me->mut);
}
这段代码一直在等待线程增加token值,然而线程要一秒后才会加,所以这段时间一直在解锁-等待-加锁,每次都要执行一秒钟这样的操作。这就是轮询法的问题,需要不停的看token的值。如果改成通知法:在这里等着,token值增加后通知我再进行操作,可以避免这一问题。学习下面的机制可以修改成通知法
条件变量
条件变量用来自动阻塞一个线程,直到某特殊情况发生为止。通常和互斥锁一起使用,互斥锁用于上锁,条件变量则用于等待。
//和互斥量相似,分为动态初始化和静态初始化
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
//条件等待函数,不满足条件时调用
//wait会一直等待,直到满足条件
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
//timedwait在等待超时后返回
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
//条件通知函数
//当另一个线程修改了某参数可能使得条件变量所关联的条件变成真时
//它应该通知一或多个等待在条件变量等待队列中的线程
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒条件等待队列中的所有线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒条件等待队列中的一个线程
为什么wait和timedwait会传入互斥量?为了以防出现在等待条件改变之前条件就改变的情况。在调用这俩个函数之前要先手动给互斥量上锁,调用函数后会先将本线程放入等待队列,然后给互斥量解锁,等待条件的改变。互斥量就是这样防止线程错过条件的变化。
mytbf改造
将多线程的mytbf更改为通知法
struct mytbf_st
{
...
pthread_cond_t cond;
};
void *thr_alrm(void *p)
{
while (1)
{
pthread_mutex_lock(&mut_job);
for (int i = 0; i < MYTBF_MAX; i++)
{
if (job[i] != NULL)
{
pthread_mutex_lock(&job[i]->mut);
...
pthread_cond_broadcast(&job[i]->cond); //token增加了,发出信号
pthread_mutex_unlock(&job[i]->mut);
}
}
pthread_mutex_unlock(&mut_job);
sleep(1);
}
}
mytbf_t *mytbf_init(int cps, int burst)
{
...
pthread_cond_init(&me->cond, NULL);
...
}
int mytbf_fetchtoken(mytbf_t *ptr, int size)
{
...
pthread_mutex_lock(&me->mut);
while (me->token <= 0) //这里用while防止意外唤醒
pthread_cond_wait(&me->cond, &me->mut);
pthread_mutex_unlock(&me->mut);
...
}
int mytbf_destroy(mytbf_t *ptr)
{
...
pthread_cond_destroy(&me->cond);
...
}
abcd改造
利用条件变量改写abcd,不用锁链的形式。而是a输出完通知b,b输出完通知c这样的方式输出。
static int num;
static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
static pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER;
static int next(int n)
{
if (n + 1 == 4)
return 0;
return n + 1;
}
static void *thr_func(void *p)
{
int n = (int)p;
char c = 'a' + n;
while (1)
{
pthread_mutex_lock(&mut);
//假设现在应该输出a,但是线程是输出b的线程
//num是0,而n是b对应的1,则wait会让该线程等待
while (n != num)
pthread_cond_wait(&cond, &mut);
write(1, &c, 1);
num = next(num);
//更改完num后通知所有线程
pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
}
pthread_exit(NULL);
}
//main函数主要负责创建4个线程
int main()
{
pthread_t tid[4];
for (int i = 0; i < 4; i++)
{
int err = pthread_create(tid + i, NULL, thr_func, (void *)i);
if (err)
{
for (int j = 0; j < i; j++)
pthread_join(tid[j], NULL);
fprintf(stderr, "pthread_create(): %s", strerror(err));
exit(1);
}
}
alarm(1);
for (int i = 0; i < 4; i++)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(&mut);
pthread_cond_destroy(&cond);
exit(0);
}