[Linux]多线程

本文介绍了Linux系统中的线程概念,线程与进程的区别,以及多线程在资源管理和调度上的优缺点。详细讲解了线程的创建、退出、等待和分离,并讨论了线程安全,包括互斥锁和条件变量的使用,以及防止死锁的方法。最后,通过顾客厨师问题展示了线程同步的实际应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

线程概念:

线程概念:线程是进程中的一条执行流,负责进程中一段代码的运行调度,一个进程中可以有一个或多个线程。
在Linux下,线程是通过pcb实现的,一个进程中可以有多个pcb,这些pcb共享进程中的大部分资源,因此也被称作轻量级进程。

在这里插入图片描述

线程就是进程中的一条执行流,而在Linux下执行流通过pcb来实现,一个进程中可以有多个执行流pcb,这些pcb共享进程中的大部分资源,相较于传统pcb较为轻量化,因此也被称作轻量级进程。

线程与进程的关键性区别:

​ 进程是系统进行资源分配的基本单元

​ 线程是系统进行运行调度的基本单元

线程间的独有与共享:

​ 独有:标识符、栈、寄存器、信号屏蔽字、errno…

​ 共享:虚拟地址空间、IO信息、信号处理方式、工作路径…

多进程与多线程进行多任务处理的优缺点:

线程优点:

​ 1、线程间通信更加灵活,包含进程间通信在内,还可以使用全局变量以及函数传参实现通信

​ 2、线程的创建与销毁成本更低

​ 3、线程间的调度成本低

进程的优点:稳定,健壮。 应用场景:shell、网络服务器…

线程不是越多越好,cpu资源是固定的,线程给多了反而会增加线程的切换调度成本

cpu密集型程序:程序中进行大量的数据运算,对cpu资源要求高

IO密集型程序:程序中进行大量的IO操作,对cpu资源要求并不高

线程控制

线程控制包括四个方面:创建、退出、等待、分离

创建

操作系统在底层并不会区分轻量级进程与线程,所以操作系统实际上并没有提供用于创建线程的接口,后来的大牛们通过封装实现了线程控制对应所需的库函数,用于对线程操作。

int pthread_creat(pthread_t *tid, pthread_attr_t *attr,
					void*(*routine)(void*), void *arg);
					

pthread_t *tid:(输出型参数) 用于接收线程ID----tid
pthread_attr_t *attr: 设置线程属性----通常置NULL
void*(*routine)(void*): 线程入口函数,入口函数运行完毕后,退出线程
void *arg:传递给线程的数据,就是传递给上面这个入口函数的形参
返回值:
成功返回0;失败返回非0—错误编号。

代码示例:
注意:线程的操作都是通过库函数来实现的,所以在编译和运行相关代码时,需要链接pthread动态库
命令操作:gcc creat.c -o creat -lpthread

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
void *thr_entry(void* arg){
  while(1){
    printf("normal thread--%s\n",(void*)arg);
    sleep(1);
  }
  return NULL;
}
int main(){
  //pthread_create(tid, 线程属性, 入口函数, 参数 )
  pthread_t tid;
  char *ptr="好好学习~";
  int ret=pthread_create(&tid,NULL,thr_entry,(void*)ptr);
  if(ret!=0){
    printf("thread_create error!");
    return -1;
  }
  while(1){
    printf("天气真好\n");
    sleep(1);
  }
  return 0;
}

在这里插入图片描述

退出

1、在线程入口函数中return;线程入口函数运行完毕,线程就会被销毁
注意:main函数中return会直接销毁进程,退出所有的线程

2、调用函数接口退出:

void pthread_exit(void* retval);  //退出调用该接口的线程
int pthread_cancel(pthread_t id); //取消指定的线程--非正常退出

注意:在主线程中调用pthread_exit()接口并不会导致所有线程退出,而是仅仅退出主线程。

等待

概述:等待指定的线程退出,获取退出线程的返回值,回收资源

一个处于joinable状态(线程的分离属性默认是joinable状态)的线程在退出时,操作系统不会自动释放其资源,所以需要线程等待。

int pthread_join(pthread_t tid,void** retval);

tid:指定要等待的线程
tval:用于获取线程返回值

分离

将线程的分离属性设置为detach状态;
处于detach状态的线程退出时会自动释放资源,不需要被等待。

int pthread_detach(pthread_t tid);

线程安全

表示线程中对临界资源的访问是安全的
临界资源:公共资源,每个线程都能访问到的资源
线程安全的实现:同步与互斥
互斥:同一时间只能有一个线程访问资源,保证资源访问的安全性;

同步:通过条件判断,让线程对临界资源的访问更加合理有序

互斥:互斥锁

互斥锁
互斥锁:通过互斥锁保护线程对临界资源的访问不被打断
本质:只有0或1两种状态的计数器
原理:标记临界资源的两种状态,可访问和不可访问;在线程访问资源之前先进行加锁操作(判断是否可访问,可访问则返回;不可访问则阻塞),加锁操作之后会将资源置为不可访问状态。
当线程访问完毕后进行解锁操作,将资源置为可访问状态。

操作步骤:

1、定义一个互斥锁变量:pthread_mutex_t mutex;
2、初始化互斥锁:

①、定义互斥锁时进行初始化: pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
②、接口操作:int pthread_mutex_init (pthread_mutx_t * restrict mutex, const pthread _mutexattr_t * restrict attr);

3、在访问临界资源之前进行加锁:
int pthread_mutex_lock(pthread_mutex_t *mutex);–阻塞操作
int pthread_mutex_trylock(pthread_mutex_t *mutex);–非阻塞操作
4、在访问临界资源完毕之后解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
5、不在使用互斥锁则销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

代码示例:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

int tickets=100;
void* scalpers(void* arg){
  pthread_mutex_t* mutex=(pthread_mutex_t*)arg;
  pthread_t tid=pthread_self();
  while(1){
    pthread_mutex_lock(mutex);    //加锁
    if(tickets>0){
      usleep(1);
      printf("I got a ticket!-%p-%d\n",tid,tickets);
      tickets--;
      pthread_mutex_unlock(mutex);  //解锁
    }else{
      pthread_mutex_unlock(mutex);  //在任意有可能造成线程退出的地方都要进行解锁
      pthread_exit(NULL);
    }
  }
  return NULL;
}

int main(){
  pthread_mutex_t mutex;
  pthread_t tid[4]={0};
  int ret=0;

  pthread_mutex_init(&mutex,NULL);  //互斥锁的初始化必须在线程创建之前
  for(int i=0;i<4;i++)
  {
    ret=pthread_create(&tid[i],NULL,scalpers,&mutex);
    if(ret!=0){
      printf("pthread_create error\n");
      return -1;
    }
  }
  
  for(int i=0;i<4;i++){
    pthread_join(tid[i],NULL);
  }

  pthread_mutex_destroy(&mutex);  //销毁互斥锁
  
  return 0;
}

在这里插入图片描述

死锁

描述的是程序因为流程无法继续推进而卡死的情况

死锁产生的原因:
1、互斥:同一时间只有一个线程能够访问资源
2、不可剥夺条件:一个线程加锁后,只能由自己来解锁,其他线程不能解
3、请求与保持条件:得到A锁后请求B锁,请求不到B锁,但是不释放A锁
4、环路等待条件:a线程拿到1锁,请求2锁;b线程拿到2锁,请求1锁

如何预防死锁:
1、保证加锁和解锁的顺序正确合理
2、破坏请求与保持条件:使用非阻塞加锁

如何解决死锁:银行家算法死锁检测算法

同步:条件变量

同步的实现:通过条件判断资源获取是否合理----条件变量
条件变量:一个pcb等待队列+使线程阻塞和唤醒的接口
原理:当资源获取不合理时,调用阻塞接口,使线程阻塞,将pcb挂到等待队列;等到资源访问合理时,通过唤醒接口唤醒阻塞的等待队列上的线程。

注意:条件变量本身只提供阻塞和唤醒功能,而资源获取是否合理需要程序员另外做判断

接口介绍
1、定义条件变量:pthread_cond_t cond;
2、初始化条件变量:pthread_cond_t cond=PTHREAD_COND_INITALIZER;
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
3、获取资源不合理时调用阻塞接口:
int pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t* mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
4、其他线程促使资源获取合理后,唤醒等待队列上的线程:
int pthread_cond_signal(pthread_cond_t *cond);----唤醒一个或者多个线程,至少一个
int pthread_cond_broadcast(pthread_cond_t *cond);----唤醒所有
5、销毁条件变量:
int pthread_cond_destroy(pthread_cond_t *cond)

实际应用:顾客厨师问题

编程解决以下问题:顾客来吃饭,首先判断柜台是否有饭,没饭就等待,有饭就吃饭;
厨师做饭,判断柜台是否有饭,有饭就等待,没饭就做饭。

在这里插入图片描述
注意:
1、条件的判断应该使用while循环判断,因为可能会有多名顾客同时来吃饭的情况
2、在具有多种角色的情况下,应该使用多个条件变量,否则会出现唤醒错误

代码:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>

pthread_cond_t cond_customer;
pthread_cond_t cond_cooker;
pthread_mutex_t mutex;

int counter=0;  //柜台   等于0表示柜台上没有饭

void *cooker(void* arg){
  while(1){
    //加锁
    pthread_mutex_lock(&mutex);
    while(counter==1){
      //表示柜台上有饭,厨师不需要做饭,cooker阻塞
      pthread_cond_wait(&cond_cooker,&mutex);
    }
    //做饭
    printf("做了一碗饭!\n");
    counter++;
    //唤醒顾客
    pthread_cond_signal(&cond_customer);
    //解锁
    pthread_mutex_unlock(&mutex);
  }
  return NULL;
}
void *customer(void* arg){
  while(1){
    //加锁
    pthread_mutex_lock(&mutex);
    while(counter==0){
      //阻塞
      pthread_cond_wait(&cond_customer,&mutex);
    }
    //吃饭
    printf("吃完了,好吃!\n");
    counter--;
    //唤醒厨师
    pthread_cond_signal(&cond_cooker);
    //解锁
    pthread_mutex_unlock(&mutex);

  }
  return NULL;
}


int main(){
  pthread_t tid1,tid2;


  pthread_cond_init(&cond_customer,NULL);    //初始化条件变量
  pthread_cond_init(&cond_cooker,NULL);    //初始化条件变量
  pthread_mutex_init(&mutex,NULL);  //初始化互斥锁

  int ret=0; 
  for(int i=0;i<3;i++){
    ret=pthread_create(&tid1,NULL,cooker,NULL); //创建cooker线程
    if(ret!=0){
     printf("thread create error!\n");
     return -1;
    }
  }
  for(int i=0;i<3;i++){
    ret=pthread_create(&tid2,NULL,customer,NULL); //创建customer线程
    if(ret!=0){
     printf("thread create error!\n");
     return -1;
    }
  }
  pthread_join(tid1,NULL);
  pthread_join(tid2,NULL);
  pthread_cond_destroy(&cond_customer);    //销毁条件变量
  pthread_cond_destroy(&cond_cooker);    //销毁条件变量
  pthread_mutex_destroy(&mutex);  //销毁互斥锁
  return 0;
}

结果:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值