本篇目录(参考Unix高级环境编程编写顺序)
一、线程基本概念及基础操作
二、线程同步基础概念
三、线程同步机制
3.1 互斥量(互斥锁,包括普通互斥锁、读写锁)
3.2 条件变量
一、线程的基本概念及基础操作
1、基本概念
进程下的控制流,是程序执行的最小单位。
进一步理解:典型的Unix进程可以看成只有一个线程,也就是说在同一时刻,该进程只能处理一件事情。如果一个进程有多个线程该程序在同一时刻就可以处理多件事情。
2.多线程概念
接上一步,如果一个进程中有多个线程,我们需要搞清楚这样几个问题
(1)为什么要使用多线程?
1.能够提高整个程序的吞吐量(得益于多线程的并行机制)
2.使程序运行更具有效率(这点不难理解,单线程同一时刻只处理一件事,而多线程可处理多件)
3.程序设计更简单(可以把不同性质的工作分配给多个线程)
4.改善响应时间(在交互式程序里,可以把用于接收用户的工作和其他工作分开)
(2)多线程下,各个线程和该进程的关系
<1>各个线程在同一进程里,它们共享进程的所有资源包括代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)
<2>每个线程都包含执行环境必要的信息,包括进程下标识的线程ID,一组寄存器值,栈,调度优先级和策略、信号屏蔽字、errorn变量以及线程私有程序。
3、线程的基本操作
(1)线程ID
每个线程都有一个ID,用来标识本线程,线程ID仅在它所属的上下文才有意义。用pthread_t表示,是一个非负整数。需要注意的是不能
将它当整数来处理,如果需要比较两个线程ID使用
int pthread_equal(pthread_t tid1, pthread_t tid2);
线程ID获取有两种方式
<1>pthread_t pthread_self(void)
<2>线程创建(稍后讲解)
(2)线程创建
int pthread_create(pthread *restrict tid,
const pthread_attr_t *restrict attr,
void*(*start_rtn)(void* ), void *restict arg)
tidp:当pthread_create成功返回的时候,新创建线程的线程ID会被设置成tidp指向的内存单元。因此用于获取新创建线程的ID。
attr:用于定制不同的线程属性。现阶段需要了解的属性是可分离/可结合属性,通常设置为NULL,默认为可结合属性。
start_rtn:新创建的线程会从start_rtn函数的地址开始运行,该参数被称为线程函数。
arg:如果start_rtn函数,需要传入参数,那就放入arg所指向的内存中,超过一个参数,将参数放入一个结构中,然后将结构的地址作为arg
返回值:如果创建失败是会返回错误码的,正常返回为0。
注意:如果线程执行很快,在该函数返回前,线程函数就已经执行结束,返回的线程ID会是无效的。
(3)线程终止
- 线程可以简单的从启动历程中返回,返回值是线程的退出码。
- 线程可以被同一个进程中的其他线程取消
- 线程自己调用pthread_exit
void pthread_exit(void *rval_ptr )
rval_ptr:进程中的其他线程调用pthread_join可以访问rval_ptr指向内容。
作用:调用该函数的线程会终止。
int pthread_join(pthread_t thread , void **rval_ptr );
thread :线程ID。
rval_ptr:接收 pthread_exit参数的内容。
作用:调用该函数的线程会一直阻塞在调用处,直到作为参数的thread终止,然后pthread_join返回。一般用于主线程等待子线程执行完毕,等待子线程资源回收完毕。
注意:在线程创建时,定制属性为可分离的或者该线程已经处于分离状态则不能使用该函数,由于可分离线程不会等待pthread_join
返回,自己就释放资源结束。
到这基本概念就讲完了,现在通过一个简单的例程来应用一下:
#include <ptread.h>
#include <stdio.h>
void* thr_fction1()
{
printf("thread 1 returning");
return (void*)1;
}
void* thr_fction2()
{
printf("thread 2 exiting");
pthread_exit((void*)2);
}
int main(int argc, char** argv)
{
int err;
pthread_t tid1, tid2;
void* tret;
err = pthread_create(&tid1,NULL, thr_fction1,NULL);
if (err != 0)
{
err_exit(err, "can't create thread 1");
}
err = pthread_create(&tid2, NULL, thr_fction2, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 2");
}
err = pthread_join(tid1,&tret);
if (err != 0)
{
err_exit(err, "can't join thread 1");
}
printf("thread 1 exit code %ld", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
{
err_exit(err, "can't join thread 2");
}
printf("thread 2 exit code %ld", (long)tret);
}
运行结果:
在上面我们应用了前述所讲述的函数,都是基础用法,有一点是,return的参数也能够被线程的thread_join函数捕捉到。
int pthread_cancel(pthread_t tid)
作用:在默认情况下,该函数会使得tid标识的线程的行为表现为如同调用了参数PTHEAD_CANCELED的pthread_exit函数,但是线程可以忽略取消,或者控制如何被取消。该函数仅仅是向tid所标识的线程提出请求。
void pthread_cleanup_push(void (*rtn)(void*),void *arg)
线程可以安排自己退出时需要调用到的函数。该函数便是线程清理函数当线程执行以下动作时,清理函数rtn由pthread_cleanup_push调度,调用时rtn的参数为arg:
- 调用pthread_exit时
- 响应cancel请求时
- 用非零excute参数调用pthread_cleanup_pop时
注意:线程清理函数可以注册多个,执行顺序和注册顺序相反。
(函数原型:void pthread_cleanup_pop(int execute))
通过一个例子练习:
#include <ptread.h>
#include <stdio.h>
//线程清理函数
//参数 void*
//返回值:无
void cleanup(void* arg)
{
printf("clean up :%s", (char*)arg);
}
//测试线程清理函数的线程函数1
//参数:arg :void* 0/1,决定触发线程清理的条件exit or pop
//返回值:1
void* thr_fction1(void* arg)
{
printf("thread 1 strating");
pthread_cleanup_push(cleanup, "tnread 1 handle");
printf("thread 1 push complete");
if(arg)
return (void*)1;
pthread_cleanup_pop(0);
return (void*)1;
}
//测试线程清理函数的线程函数2
//参数:arg :void* 0/1,决定触发线程清理的条件exit or pop
//返回值:2
void* thr_fction2(void* arg)
{
printf("thread 2 strating");
pthread_cleanup_push(cleanup, "tnread 2 handle");
printf("thread 2 push complete");
if (arg)
thread_exit((void*)2);
pthread_cleanup_pop(0);
thread_exit((void*)2);
}
int main(int argc, char** argv)
{
int err;
pthread_t tid1, tid2;
void* tret;
err = pthread_create(&tid1,NULL, thr_fction1,(void*)1);
if (err != 0)
{
err_exit(err, "can't create thread 1");
}
err = pthread_create(&tid2, NULL, thr_fction2, (void*)1);
if (err != 0)
{
err_exit(err, "can't create thread 2");
}
err = pthread_join(tid1,&tret);
if (err != 0)
{
err_exit(err, "can't join thread 1");
}
printf("thread 1 exit code %ld", (long)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
{
err_exit(err, "can't join thread 2");
}
printf("thread 2 exit code %ld", (long)tret);
exit(0);
}
结果是:只有线程2执行了线程清理函数。
原因是:在main中给线程函数传入参数(void* )1后,线程函数1使用return返回。而线程2调用了thread_exit。
ps:前面的cancel没有测试,可以改写线程函数2,给线程函数2传入线程1的ID,再调用pthread_cancel()函数,看线程1是否执行线程清理函数。
二、线程同步基础概念
首先,先需要了解是同步/异步
同步/异步 一般指的是方法,同步是等待方法返回后再执行下一步动作,异步则是不等待方法返回直接执行下一步动作。
那么线程同步便是当多个线程请求同一资源时,同时仅会有一个线程请求到资源,其他线程等待线程操作完毕,然后再去请求资源。
那么,为什么需要线程同步?
举一个例子:假设线程没有同步,线程A对一个全局变量进行写操作的同时线程B对该变量进行读操作,B可能读到的值时不确定的,可能是线程A改变之前的,也可能是A改变之后的。由于写时间是大于一个储存器访问周期的。读操作可能在写的到一半时开始,读操作也可能在写完成时开始。这时显然得到的结果不是我们想要的。
再举一个具象化的例子:如果线程A,线程B同时对同一个全局变量进行自增操作。假设变量初始值是0,那么结果可能是1,也可能是2。
原因自己想一想呗。
线程同步可以很好的解决这个问题啊,有了线程同步,上述第一个例子,执行会是这样的。
三、线程同步机制
3.1 互斥量
互斥量从本质上来说是一把锁,在线程访问公共资源前设置互斥量(加锁),在访问公共资源后释放互斥量(解锁)。
线程对该互斥量加锁之后,之后其他任何企图对该互斥量加锁的线程都会被阻塞在加锁处,直到持有锁的当前线程释放锁,其他线程才会变为可运行状态,第一个变为可运行状态的线程可对该互斥量加锁。
似乎有点迷,EM...往下走,不着急。先介绍下,前面操作的函数实现
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *retrict attr)
mutex:返回互斥量的指针
attr:定制属性,先不管
作用:初始化互斥量
注:也可以把互斥量初始化为PTHREAD_MUTEX_INTIALIZER(只适用于静态分配)
int pthread_mutex_destroy(pthread_mutex_t *restrict mutex)
mutex:返回互斥量的指针
作用:销毁互斥量
int pthread_mutex_lock(pthread_mutex_t mutex)
作用对mutex上锁
int pthread_mutex_trylock(pthread_mutex_t mutex)
作用:对mutex尝试上锁,与lock不同的是,当前mutex被锁住了,lock会等待,trylock返回失败。
int pthread_mutex_unlock(pthread_mutex_t mutex)
作用:对mutex解锁
好,我们接下来,应用上述操作,进一步学习互斥量是如何工作的,本例使用两个线程,都对count进行自减,减到零结束
#include <ptread.h>
#include <stdio.h>
typedef struct {
int count; //计数
pthread_mutex_t lock; //保护该结构体的计数的互斥量
}foo;
foo* fh;
void thr_fction1(void)
{
while(1)
{
if (fh->count == 0)
{
printf("thread 1 return");
return;
}
pthread_mutex_lock(fh->lock);
fh->count--;
printf("thread 1 count: %ld", fh->count);
pthread_mutex_unlock(fh->unlock);
}
}
void thr_fction2(void)
{
while(1)
{
if (fh->count == 0)
{
printf("thread 2 return");
return;
}
pthread_mutex_lock(fh->lock);
fh->count--;
printf("thread 2 count: %ld", fh->count);
pthread_mutex_unlock(fh->unlock);
}
}
int main(int argc, char** argv)
{
int err;
pthread_t tid1, tid2;
fh = malloc(sizeof(foo));
fh->count = 10;
if (pthread_mutex_init(&(fh->lock), NULL) != 0)
{
err_exit(err, "can't init thread mutex");
}
err = pthread_create(&tid1,NULL, thr_fction1, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 1");
}
err = pthread_create(&tid2, NULL, thr_fction2, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 2");
}
err = pthread_join(tid1);
if (err != 0)
{
err_exit(err, "can't join thread 1");
}
err = pthread_join(tid2);
if (err != 0)
{
err_exit(err, "can't join thread 2");
}
exit(0);
}
避免死锁
死锁产生:
(1)线程试图对同一个互斥量加锁两次
(2)多个互斥量存在时,访问顺序不确定
例如:同一资源,有两个互斥量保护,线程A持有锁1,试图获得锁2,线程B持有锁2,试图锁1。这两个线程会永远阻塞在这里,线程A在没有获得锁2,访问完资源前不会释放锁1,而线程B也是如此。互相持有对方需要的锁,但都不会进行释放自己获得的锁。
好的解决办法是,
(1)锁获得顺序的一致性,规定只能先获得锁1后获得锁2。
(2)可以先使用trylock,如果返回buzy,那么就释放自己所持有的锁,然后等待一段时间,在进行请求获得。
int pthread_mutex_timelock(pthread_mutex_t *restrict mutex,const struct timespec*retrict tsptr)
作用:当线程试图获取一个已加锁的互斥量时,该函数设置等待到tsptr后,如果该锁还不是可获得的,那么返回ETIMEDOUT,否则获得锁。
读写锁
与互斥量类似,不过读写锁并行性更高,读写锁有三种撞他,读模式加锁,写模式加锁,不加锁。一次只有一个线程可以占有写模式下的锁,而多个线程可以同时占有读模式下的读写锁。
其实也就是,如果有一个线程持有写模式下的锁,那么无论以何种模式获得该锁的其他线程都会被阻塞。而有一个或多个线程持有读模式下的锁,想要获得读模式锁的线程可以直接获得,而想要获得写模式锁的线程会被阻塞直到当前所有持有读模式锁的线程释放锁。
基本函数
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *retrict attr)
作用:init读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock)
作用:destroy读写锁
int pthread_rwlock_rdlock(pthread_rwlock_t *restrict rwlock)
作用:以读模式获得读写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *restrict rwlock)
作用:以写模式获得读写锁
int pthread_rwlock_unlock(pthread_rwlock_t *restrict rwlock)
作用:解锁
3.2 条件变量
条件变量是线程可用的另一种同步机制。条件变量与互斥量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。条件变量本身是互斥量保护的,线程在改变条件状态之前必须首先锁住互斥量。其他线程在获得互斥量之前不会察觉到这种改变。
实际上,条件变量本身的意义在于使得同步更加规范。举一个例子:
一个程序里有三个线程,线程A是生产者,线程B,C是消费者。线程A的工作是生产数据,每次生产两个数据。线程B,C将数据打印然后删除,消费者每次消费一个数据。
那么线程A生产完数据后,线程B消费完数据,然后线程C消费数据。线程C消费完数据后,那么此时线程A和线程B会抢占资源。可能是线程A获得锁,也可能是B获得锁。如果线程B获得锁,此时已经没有数据可供消费了。就会出错。
此时条件变量就是来规范该过程的,有了条件变量,在线程B,C消费完数据后,保证拿到锁的是线程A。
那么,接下来,看看实现该过程所需要的函数。
int pthread_cond_init(pthread_cond_t *restrict cond, const phthread_condattr_t *restrict attr)
cond:指向条件变量的指针
attr:定制属性,暂时不需要关注
作用:init条件变量
int pthread_cond_destroy(pthread_cond_t *cond)
作用:destroy条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *restrict mutex)
cond:条件变量 指针
mutex:条件变量关联的锁
作用:等待cond条件变量变为真,否则一直会阻塞在该函数处。一般和获取锁操作连用。
int pth_cond_signal(pthread_cond_t *cond)
作用:唤醒等待pthread_cond_wait的线程,随机唤醒一个
int pth_cond_broadcast(pthread_cond_t *cond)
作用:唤醒等待pthread_cond_wait的线程,唤醒全部
有了上述的函数,我们将详细讲述条件变量是如何实现该过程的。
#include <ptread.h>
#include <stdio.h>
typedef struct {
int count; //计数
pthread_mutex_t lock; //保护该结构体的计数的互斥量
}foo;
foo* fh;
pthread_cond_t qready = PTHREAD_COND_INITALIZER;
void thr_fction1(void)
{
while (1)
{
if (fh->count == 0)
{
printf("thread 1 exit");
thread_exit();
}
pthread_mutex_lock(fh->lock);
fh->count = 2;
printf("thread 1 count: %ld", fh->count);
pth_cond_broadcast(&qready);
pthread_mutex_lock(fh->unlock);
}
}
void thr_fction2(void)
{
while (1)
{
pthread_mutex_lock(fh->lock);
pthread_cond_wait(&qready,&(fh->lock));
fh->count--;
printf("thread 2 count: %ld", fh->count);
pthread_mutex_lock(fh->unlock);
}
}
void thr_fction3(void)
{
while (1)
{
pthread_mutex_lock(fh->lock);
pthread_cond_wait(&qready, &(fh->lock));
fh->count--;
printf("thread 3 count: %ld", fh->count);
pthread_mutex_lock(fh->unlock);
}
}
int main(int argc, char** argv)
{
int err;
pthread_t tid1, tid2, tid3;
fh = malloc(sizeof(foo));
fh->count = 10;
if (pthread_mutex_init(&(fh->lock), NULL) != 0)
{
err_exit(err, "can't init thread mutex");
}
err = pthread_create(&tid1,NULL, thr_fction1, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 1");
}
err = pthread_create(&tid2, NULL, thr_fction2, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 2");
}
err = pthread_create(&tid3, NULL, thr_fction3, NULL);
if (err != 0)
{
err_exit(err, "can't create thread 2");
}
err = pthread_join(tid1);
if (err != 0)
{
err_exit(err, "can't join thread 1");
}
err = pthread_join(tid2);
if (err != 0)
{
err_exit(err, "can't join thread 2");
}
err = pthread_join(tid3);
if (err != 0)
{
err_exit(err, "can't join thread 2");
}
exit(0);
}
碰到之前没有数据可供消费,但此时消费者 生产者争抢锁的问题,在上述程序得到很好的解决。
可以看到pthread_cond_wait函数放在消费者里,而信号函数pthread_cond_broadcast放在生产者里。
以前的过程变为,生产者产生数据后,释放锁,并调用pthread_cond_broadcast唤醒所有等待pthread_cond_wait的线程(并将条件变量置为真)。线程B,C相继消费,后由于线程B,C中pthread_cond_wait执行结束后,会将条件变量置为无效,线程B,C由于会由于条件变量置为无效状态,又会阻塞在pthread_cond_wait处。那么此时只有线程A可以获得锁,然后A再生产数据..... 循环往复。
现在条件变量的基本工作都介绍结束了。
但是依旧有问题。
在线程B,C消费之后,重新阻塞在了pthread_cond_wait处,但是在这之前BC已经获得锁了啊?A如何获得锁?
这个问题.....往后闪! 我要开始装逼了。
这都得益于pthread_cond_wait的机制,pthread_cond_wait会关联一个锁,在调用pthread_cond_wait之前lock该锁,是因为pthread_cond_wait会先执行unlock关联锁的操作,然后等待pthread_cond_wait的条件为真,它会自行执行获得锁的操作。因此,虽然执行到了pthread_cond_wait处,但是此时锁已经被释放了。这也是为什么线程B,C都会被阻塞在pthread_cond_wait处,但线程A依旧可以获得锁的原因。并且被pthread_cond_wait阻塞的线程会进入一个队列,pthread_cond_broadcast便依次唤醒队列里的线程。
关于条件变量,pthread_cond_wait函数比较关键,它的性质,我都通过上述例子,一点点的展示出来了,我就不一一罗列了,如果没有上述环境,该函数的性质是没有办法讲清楚的。
下面我还看到一些关于条件讲的比较好的
这个写的例子可以,我刚好,没有写signal的用法,大同小异。
https://blog.youkuaiyun.com/qq_39736982/article/details/82380689
这个是关于pthread_cond_wait的理解
https://www.2cto.com/kf/201708/665818.html