当前位置: 首页 > article >正文

多线程1:基础概念、接口介绍、锁

线程概念(了解就行

什么是线程

  1. 线程是“一个进程内部的控制序列”
  2. 线程在运行,本质是在进程地址空间内运行

线程和进程

在这里插入图片描述

线程接口

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;失败返回错误码

(感觉这个函数的使用很抽象啊,我们来看看怎么用吧)

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);
    }
}

主要就是对哪个线程进行操作(tid)、以及操作方法(rout)

线程终止

有三种方法:

  1. 线程函数中使用return,不能在主线程中使用
  2. 线程调用pthread_exit 终止自己
  3. 线程调用pthread_cancel 终止id的进程
功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

注意: value_ptr不能指向局部变量,因为这是野指针

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

等待线程(感觉比较重要

等待的目的
线程结束后,被结束的这个线程还有资源没有完全释放,并且这块空间新的线程用不到了 (就是没用了,留着就是浪费),所以要自己释放完之后,再使用其他的线程

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止

对于不同的终止情况,pthread_join获得的value_ptr指向的内容也不同(了解即可

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数 PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参 数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

分离线程

新线程默认是joinable(可等待的),结束线程之后,需要调用pthread_join
如果不需要等待,可以将线程分离。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

int pthread_detach(pthread_t thread);

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃 (线程崩溃,进程也崩溃)
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

互斥量mutex:锁

什么是锁,锁的作用

共享变量
变量在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

并发操作共享变量带来的问题
来看一段代码:

int ticket = 100;
void *route(void *arg)
{
    char *id = (char*)arg;
    while ( 1 ) {
        if ( ticket > 0 ) {
            usleep(1000);
            printf("%s sells ticket:%d\n", id, ticket);
            ticket--;
        } else {
            break;
    }      
}
}
int main( void )
{
    pthread_t t1, t2, t3, t4;
    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);

    return 0;
}

最后的运行结果:

thread 4 sells ticket:4
thread 1 sells ticket:3
thread 3 sells ticket:3
thread 2 sells ticket:1
thread 4 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2

诶?我们发现最后ticket为负数,是什么导致了ticket为临界值时的多次减小
有以下几种可能:

  1. 进入if语句后,切换到其他的线程
  2. usleep(1000);模拟的是执行业务的过程,可能在执行业务的过程中,有多个进程访问这段代码
  3. ticket–并不是原子的操作

如果看过前面的文章的话,能快速判断出:ticket–并不是原子的操作!!(即并不是要么已完成,要么还没执行)

减减这个操作对应着至少三条汇编代码

  1. 将ticket赋值给寄存器
  2. 寄存器-1
  3. 将寄存器赋值给ticket

避免的方法

  1. 代码必须互斥:单个线程在临界区执行代码时,其他线程无法进入临界区
  2. 多个线程访问临界区的时候,只允许一个线程访问临界区
  3. 线程没有访问临界区的时候,不能阻止其他进程访问临界区

那么锁就可以满足这三点
在这里插入图片描述

锁的接口

初始化锁

  1. 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//11中直接使用mutex创建锁即可
//std::mutex _mutex;
  1. 动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
//相当于
//pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));  
//pthread_mutex_init(mutex, NULL);

参数:
mutex:要初始化的互斥量
attr:NULL

销毁锁

  1. 静态分配的锁不用手动销毁
  2. 加锁的锁不能销毁
  3. 销毁后,要确保之后不会有线程再尝试上锁,即一个线程在未解锁之前只能上一次锁,不然程序会卡死

加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
//_mutex.lock();
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//_mutex.unlock();
返回值:成功返回0,失败返回错误号

改进代码
那么上面的ticket代码就可以改进为:
注意:加锁的目的就是将并行代码变为串行执行

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);
        } 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);
    return 0;
}

被锁保护起来的代码是临界区
被保护的共享资源是临界资源

补充:RAII风格的锁

std::lock_guard<std::mutex> lock(d->_mutex);
//在d类内创建锁
//利用给的lock_guard来加锁
//就可以做到临时对象随着循环创建和销毁了(RAII)

补充基础

数据在内存中的时候,是共享的,所有线程都可以访问,
在CPU内部寄存器的时候,数据就是这个线程私有的

锁的底层实现

我们知道++n和–n都是非原子的,所以不能用int类型记录锁。
体系结构中提供了swap和exchange指令,可以将寄存器和内存单元的数据直接交换,这就是原子的了

加锁解锁的伪代码大概是这样:
在这里插入图片描述加索逻辑

  1. 将寄存器和mutex交换(exchange)
  2. 判断寄存器是否大于0
  3. 不满足则一直等待指导可以加锁,回到最开始、重新执行加索

小结

暂时写这么多,下一篇可以准备多线程了~


http://www.kler.cn/a/412363.html

相关文章:

  • 如何选择最适合企业的ETL解决方案?
  • NIO三大组件
  • testImplementation和androidTestImplementation区别
  • hive的存储格式
  • 什么是MyBatis?
  • 警钟长鸣,防微杜渐,遨游防爆手机如何护航安全生产?
  • 通俗理解人工智能、机器学习和深度学习的关系
  • 【carla生成车辆时遇到的问题】carla显示的坐标和carlaworld中提取的坐标y值相反
  • 前后端中Json数据的简单处理
  • Javaweb 前端 HTML css 案例 总结
  • 开发一个基于MACOS M1/2芯片的Android 12的模拟器
  • 基于STM32的智能风扇控制系统
  • digit_eye开发记录(2): Python读取MNIST数据集
  • 渗透测试笔记—window基础
  • 蓝桥杯每日真题 - 第24天
  • 27加餐篇:gRPC框架的优势与不足之处
  • Apache Zeppelin:一个基于Web的大数据可视化分析平台
  • 前端 设置 div 标签内子多个子 div 内容,在一行展示,并且可以字段自动换行
  • Flink 实现超速监控:从 Kafka 读取卡口数据写入 MySQL
  • 浏览器开发工具
  • java——SpringBoot中常用注解及其底层原理
  • SSM之AOP与事务
  • 缓存雪崩、击穿、穿透深度解析与实战应对
  • 使用OpenCV实现视频背景减除与目标检测
  • 【QT】背景,安装和介绍
  • 【云计算网络安全】解析 Amazon 安全服务:构建纵深防御设计最佳实践