线程
线程概念
线程是一个进程内部的控制序列,一切进程至少都有一个线程,线程是运行在进程内部的,进程中的线程共享进程的虚拟地址空间
进程和线程
Linux下的线程是以进程的PCB来模拟的,一个进程中至少有一个线程,所以此时的进程就可以理解为一个线程组,在进程的CPB中,有一个单独的成员pid来标示这个进程,但是现在有多个线程所以就需要多个pid。
既然现在的进程是线程组,所以在每个task_struct中都会有一个标示来说明某个线程是哪个线程组的线程,这个成员就是tid即线程组id。
这个线程组id(tgid)是等于进程中的主线程的pid,使用ps命令显示的进程信息是首线程的信息。产看进程中的所有线程信息可以使用命令ps -efL。
CPU是以PCB来调度的,在Linux下线程是以PCB模拟的,所以Linux下线程是CPU的基本调度单位,进程是操作系统分配资源的基本单位。
线程的优缺点
线程的优点:
在等待慢速IO时程序可以做其他计算任务
线程间通信极为方便
创建/销毁线程相较于进程来说成本更低
线程的调度切换相对进程来说较低
线程缺点:
线程间数据访问变得更加简单,但是数据访问安全问题更加突出,编码难度增加
一些系统调用和异常是针对进程来说的,所以一个线程异常,整个进程都会崩溃
数据的共享和独有:
线程之间共享文件描述符表和信号的处理方式以及工作目录以及用户id
每个线程的栈区,上下文数据和errno,信号屏蔽字是独有的
线程控制
线程创建
线程是没有系统调用的,与线程有关的函数被封装成了一个线程库,要使用这些库需要用头文件引入
链接时要使用-lpthread
创建接口
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
创建一个线程
void *thr_start(void *arg)
{
int num = (int)arg;
while(1){
printf("I am simple pthread!\n");
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void *)555);
if(ret != 0){
perror("pthread_create");
return -1;
}
while(1){
printf("I am main pthread!\n");
sleep(1);
}
}
可以看到一个进程内部是有两个线程的,线程组内的第一个线程在用户态被称为主线程,在内核态被称为group_leader。内核在创建第一个线程时会将线程组id的值设置为第一个线程的线程id。所以线程组内存在一个线程id等于进程id,这个线程就是线程组的主线程。
其他线程的id是由内核进行分配的,但是线程组id和主线程的线程组id是一样的。线程和进程不同,进程有父进程的概念,但是在线程组中所有的线程都是对等关系。
线程id和地址空间
pthread_create会创建一个线程,会产生一个线程id放在第一个参数指向的地址中,这个本质上是一个进程地址空间上的一个地址。
这个线程id和在用户态展现的线程id是不同的,用户态的线程id是操作系统调度时需要标示的唯一一个值,但是pthread_create返回的线程id是为了线程库的后序操作的标示。
线程终止
线程函数return,主线程使用return相当于调用exit来退出进程
线程调用pthread_exit终止自己
线程调用pthread_cancel终止同一进程中的另一个线程
线程等待与分离
线程等待
与进程等待相类似,一个线程被创建出来之后我们希望得到线程退出的状态,这就需要线程等待
一个线程退出并不会释放其对应资源,线程空间仍旧在进程地址空间内
创建一个新的线程并不会复用刚才退出的线程的地址空间
int pthread_join(pthread_t thread, void **retval);
// retval:指向线程的返回值
调用join函数的线程将会挂起等待直到id为thread的线程终止。
thread线程以return返回,retval指向的单元里面是thread线程函数的返回值
thread线程自己调用pthread_exit终止,retval所指向的单元是传给pthread_exit的参数
thread线程被别的线程调用pthread_cancel终止,retval存放 PTHREAD_CANCELED
对thread线程的终止状态不感兴趣,可以传NULL给retval
线程分离
与进程无关,一个线程别创建出来是具有属性的,其默认属性是joinable,代表线程退出后需要被别人等待,而等待就是为了获取线程的返回值并且释放线程资源。但是在开发过程中有些线程是不需要被等待的,不关心线程的返回值,等待是毫无意义的,所有就有一个线程分离属性
int pthread_detach(pthread_t thread);
这个属性需要自行设置,告诉操作系统这个线程的返回值并不关心,如果线程退出就释放资源。
线程的derach和joinable属性相对应,也互相冲突,两者不能同时存在,如果一个线程是属性detach,那么调用join会直接报错。
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void *)555);
if(ret != 0){
perror("pthread_create");
return -1;
}
pthread_detach(tid);
}
线程安全概念
线程的同步与互斥
再说线程的同步与互斥之前我们可以先来了解一个模型:生产者与消费者模型
这个模型可以简单理解成三句话: 一个场所,两个角色,三种关系
一个场所与两个角色不难理解,三种关系:
生产者与生产者之间的关系:互斥
生产者与消费者之间的关系:同步+互斥
消费者与消费这之间的关系:互斥
进程中的线程共享了进程的虚拟地址空间,因此线程间的通信变得极为简单,但是如果缺乏数据的访问控制更容易造成混乱。所以线程之间应该有一种机制来确保数据的安全访问性。这就是线程的同步与互斥。
同步就是时序的制约访问,互斥是保证了公共资源在同一时间的唯一访问性。
同步可以用条件变量和posix信号量来实现,互斥可以使用互斥锁来实现。
互斥锁
我们可以模拟一个售票系统
int ticket = 400;
void *pth_start(void *arg)
{
char *name = (char*)arg;
while(1){
if(ticket > 0){
usleep(1000);
printf("%s sell ticket:%d\n", name, ticket);
ticket--;
}else{
break;
}
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, pth_start, "thread 1");
pthread_create(&t2, NULL, pth_start, "thread 2");
pthread_create(&t3, NULL, pth_start, "thread 3");
pthread_create(&t4, NULL, pth_start, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
这样直接运行我们会发现到最后票数出现负数,这显然是我们不希望出现的,那这是什么原因呢?
在if判断语句之后,usleep模拟漫长的业务过程,在这个过程中,可能会有很多个线程进入代码段执行下面的操作,由于ticket并不受保护,多个线程无限制访问造成数据出错。
那么如何解决这个问题?
代码需要互斥,即一个线程进入临界区,不允许其他线程进入
多个线程要执行临界区的代码,并且临界区没有线程执行,那么一次只允许一个线程进入该临界区
这些操作的本质可以理解为给临界资源上锁,当一个线程执行就加锁,其他线程不能执行,执行完毕解锁,其他线程可以执行。Linux上这把锁就是互斥锁。
互斥锁的相关接口:
//初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
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_trylock(pthread_mutex_t *mutex);//非阻塞,获取失败返回
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict abs_timeout); //限时阻塞加锁
//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
基于互斥锁,我们可以将上面的代码做出如下修改
int ticket = 400;
pthread_mutex_t mutex;
void *pth_start(void *arg)
{
char *name = (char*)arg;
while(1){
pthread_mutex_lock(&mutex);
if(ticket > 0){
usleep(1000);
printf("%s sell ticket:%d\n", name, ticket);
ticket--;
}else{
pthread_mutex_unlock(&mutex);
//在加锁后任意有可能退出的地方都要进行解锁,
//否则会导致其他线程阻塞
break;
}
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t t1, t2, t3, t4;
int ret = pthread_mutex_init(&mutex, NULL); //初始化互斥锁
if(ret < 0){
perror("pthread_mutex_init");
return -1;
}
pthread_create(&t1, NULL, pth_start, "thread 1");
pthread_create(&t2, NULL, pth_start, "thread 2");
pthread_create(&t3, NULL, pth_start, "thread 3");
pthread_create(&t4, NULL, pth_start, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex); //销毁互斥锁
}
对与互斥锁来说,一旦有加锁操作就一定要有解锁操作,否则会造成其他线程锁死。这就引申出死锁的概念。
死锁:由于线程一直获取不到锁资源而造成的锁死情况
造成死锁的必要条件:
互斥条件—只能有一个线程获取
不可剥夺条件—自己获取锁别人不可释放
请求与保持条件—在获取第一个锁之后又去获取第二个,没有获取到锁二,不会释放锁1
环路等待条件—A拿锁1去请求锁2,B拿锁2请求锁1
如何避免死锁:只要破坏死锁产生的必要条件即可
条件变量
条件变量是线程的另外一种同步机制,这些同步对象为线程提供了会合的场所,理解起来就是两个(或者多个)线程需要进行交互,我们指定在条件变量这个地方发生,一个线程用于修改这个变量使其满足其它线程继续往下执行的条件,其它线程则接收条件已经发生改变的信号。
基于条件变量我们可以实现一个简单的生产者与消费者模型
pthread_cond_t cond;
pthread_mutex_t mutex;
int basket = 0;
void *prodect(void *arg)
{
while(1){
sleep(1);
pthread_mutex_lock(mutex);
if(basket == 0){
printf("prodect some");
basket = 1;
//唤醒第一个等待在条件变量上的线程
pthread_cond_signal(&cond);
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
void consume(void *arg)
{
while(1){
pthread_mutex_lock(&mutex);//加锁保护
if(basket == 0){
//条件变量和互斥锁搭配使用
//wait先是对互斥锁进行判断是否加锁,加锁就先解锁
//然后陷入休眠等待-----原子操作
//防止情况:没有资源但是消费者速度比较快先拿到锁,
//这样生产者拿不到锁,造成双方死锁
//这里如果消费者先获取到锁先解锁
pthread_cond_wait(&cond, &mutex);//消费者先等待,等待生产者唤醒
}else{
printf("use prodect");
basket = 0;
}
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
pthread_cond_init(&cond, NULL);
pthread_mutex_init(&mutex, NULL);
pthread_create(&tid1, NULL, prodect, NULL);
pthread_create(&tid2, NULL, consume, NULL);
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}
这里使用条件变量和互斥锁搭配使用,如果只有条件变量正常情况下生产者生产资源通知消费者,消费者等待资源,但是线程之间访问临界资源并没有准确的限制,如果消费者先与生产者访问临界资源,造成消费者阻塞等待,生产者无法访问临界资源,双方都在阻塞,无法完成既定任务。
所以搭配互斥锁,当消费者先于生产者访问临界资源,wait会先判断是否有锁,如果有锁就先释放锁再等待生产者唤醒,这样生产者就可以访问临界资源,双方正常工作。