linux-多线程

linux-多线程

1.linux 线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

线程概念

1.先说进程:传统操作系统中使用pcb来描述一个程序的运行pcb就是进程
2. linux下pcb用来模拟实现线程,因此linux下的pcb实际是一个轻量级进程LWP 线程
3.这个轻量级进程因为共用大部分进程资源,相较于传统进程更加轻量化

2.linux 进程VS线程

进程和线程

进程是资源分配的基本单位—因为程序运行时资源是分配给整个线程组(进程)的
线程是cpu调度的基本单位—因为linux下pcb是线程

线程之间资源的独有与共享:

独有:
  • 栈,
  • 寄存器,
  • 信号屏蔽字,
  • errno,
  • 线程ID,
  • 调度优先级
共享:
  • 共享虚拟地址空间,
  • 文件描述符表,
  • 信号处理方式,
  • 当前工作路径,
  • 用户id/组id

任务的执行既可以使用多进程也可以使用多线程,各有优缺,视场景而定

多线程任务处理的优缺点:
多线程共用进程大部分资源

  • 1.线程间通信处理进程间的方式之外还有更简单的就是全局数据/传参-线程间通信更加方便
  • 2.创建/销毁-个线程的成本相较于进程要更低
  • 3.线程间的调度相较于进程要更低
    线程之间缺乏访问控制,有些系统调用/异常针对的是整个进程;稳定性相较于进程更低

使用场景: shell这种对主程序稳定安全性要求更高的程序就需要使用多进程,让子进程处理任务–背锅

3.linux 线程控制

线程控制:线程创建/终止/等待/分离
  linux下操作系统并没有提供线程的控制系统调用接口;因此大佬们封装了一个套线程控制接口库
  使用库函数实现创建的线程我们称之为用户态线程,这个用户态线程在内核中使用一个轻量级进程实现调度
linux下的线程:用户态线程+轻量级进程

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程

功能:创建一个新的线程
原型
  int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(start_routine)(void), void *arg);
参数
  thread:返回线程ID
  attr:设置线程的属性,attr为NULL表示使用默认属性
   start_routine:是个函数地址,线程启动后要执行的函数
  arg:传给线程启动函数的参数
  返回值:成功返回0;失败返回错误码

错误检查:

  • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值来判定,因为读取返回值要比读取线程内的errno变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>
void *rout(void *arg) {
    int i;
    for( ; ; ) {
        printf("I'am thread 1\n");
        sleep(1);
   }
}
int main( void )
{
    pthread_t tid;
    int ret;
    if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ) {
        fprintf(stderr, "pthread_create : %s\n", strerror(ret));
        exit(EXIT_FAILURE);
   }
    int i;
    for(; ; ) {
        printf("I'am main thread\n");
        sleep(1);
   }
}

线程中id的讨论:

  • tid 线程地址空间首地址
  • pcb -> pid 轻量级进程id
  • pcb -> tgid 进程(线程组id),默认等于首线程id

线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数:退出线程自身,谁调用谁退出

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:线程的退出返回值,value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

int_pthread_cancel(pthread_t thread);取消其它线程;让其它线程退出

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:要取消的线程ID
返回值:成功返回0;失败返回错误码

线程退出之后,默认不会自动释放资源,(保存自己的退出结果在线程独有的地址空间中) ;因此也会造成资源泄漏。
主线程退出,其它线程依然可以正常运行

线程等待

  • 等待线程退出,获取退出线程的返回结果,释放退出线程资源;
    一个线程创建出来,默认有一个属性joinable;
  • 处于joinable属性的线程退出后为了保存返回值,因此不会自动释放资源;
  • 需要被其它线程等待,才能释放资源;
  • 处于joinable属性的线程必须被等待,否则造成资源泄漏
    一个线程也只有处于joinable状态的时候,才需要被等待

pthread_join函数:
功能:等待线程结束–>阻塞等待指定线程退出;通过retval获取返回值
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

线程分离

线程分离就是将线程joinable属性修改为detach属性线程若处于detach属性,则线程退出后将自动回收资源;
并且这个线程不需要被等待,等待是毫无意义的,因为线程退出返回值占用的空间已经被回收了

		pthread_detach (pthread_t tid)
  • 线程分离的适用场景:对线程的返回值不关心
  • 线程分离可以在任意线程中实现
  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
	pthread_detach(pthread_self());
  • joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

4.linux 线程安全

线程安全:多个线程同时对临界资源进行访问而不会造成数据二义
如何实现线程安全:同步+互斥
同步: 对临界资源访问的时序合理性
互斥:对临界资源同一时间访问的唯一性

可重入VS线程安全

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

  • 函数是可重入的,那就是线程安全的
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

线程间互斥的实现:互斥锁

1.定义互斥锁变量

	pthread mutex_t

2.对互斥锁变量进行初始化

	pthread_mutex_it(&mutex, &att)

3.对临界资源操作之前先加锁

	pthread_mutex_lock(&mutex)

4.对临界资源操作完毕后进行解锁

	pthread_ mutex_ unlock(&mutex);
  1. 销毁互斥锁
	pthread mutex destroy(&mutex);
//售票系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.h>
int ticket = 100;
pthread_mutex_t mutex;
void *route(void *arg)
{
    char *id = (char*)arg;
    while ( 1 ) {
        pthread_mutex_lock(&mutex);
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
            // sched_yield(); 放弃CPU
       } else {
            pthread_mutex_unlock(&mutex);
            break;
       }
   }
}
int main( void )
{
    pthread_t t1, t2, t3, t4;
    pthread_mutex_init(&mutex, NULL);
    pthread_create(&t1, NULL, route, "thread 1");
    pthread_create(&t2, NULL, route, "thread 2");
    pthread_create(&t3, NULL, route, "thread 3");
    pthread_create(&t4, NULL, route, "thread 4");
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);
    pthread_mutex_destroy(&mutex);
}

线程间同步的实现:等待 +唤醒

操作条件不满足则等待,别人促使条件满足后唤醒等待

条件变量

条件变量实现同步:

线程在对临界资源访问之前,先判断是否能够操作;若可以操作则线程直接操作;
否则若不能操作;则条件变量提供等待功能;让pcb等待在队列上
其它线程促使操作条件满足,然后唤醒条件变量等待队列上的线程

1.定义条件变量

	pthread _cond_t cond

2.条件变量初始化

	pthread_cond_init(&cond, &attr);

3.用户在判断条件不满足的情况下提供等待功能

	pthread_cond_wait(&cond, &mutex); 

4.用户在促使条件满足后,唤醒等待

	pthread_cond_signal(&cond) /pthread_cond_broadcast(&cond)

5.销毁条件变量

	pthread_cond_destroy (&cond)

条件变量的条件判断应该是一个循环判断:
多个顾客线程若同时被唤醒,只有一个顾客可以加锁,其它的顾客线程将阻塞在加锁上(而不是条件变量的等待)
第一个加锁的顾客开始吃面,吃碗面后进行解锁,这时候,获取到锁的线程有可能是一个顾客线程,因为再次判断有没有面,因此直接吃面,但是面已经被第一个顾客线程吃掉了, 因此逻辑错误应该加锁之后重新再次判断是否有面
不同的角色应该等待在不同的条件交量上:
在有多个厨师线程和顾客线程的时候,若是顾客和厨师线程都等待在同一个条件变量的等待队列中,会导致厨师做了一碗面, 本应该唤醒顾客线程吃面,但是这时候有可能唤醒的是一个厨师线程,而厨师线程因为循环判断有没有面,因为已经有面而陷入等待(而顾客线程因为没有被唤醒而无法吃面)

死锁

死锁:多个线程对锁资源进行竞争访问,但是因为推进顺序不当,导致相互等待,使程序无法往下运行

死锁产生的四个必要条件:

  • 1.互斥条件:一个锁只有一个线程可以获取
  • 2.不可剥夺条件: 我加的锁别人不能解
  • 3.请求与保持条件:拿着A锁,去请求B锁,但是获取不到B锁,也不释放A锁
  • 4.环路等待条件:我拿着A锁请求B锁,对方拿着B锁清秋A锁

死锁预防:

  • 破坏四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁避免:

  • 死锁检测算法
  • 银行家算法
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值