十六、多线程(中)

news2025/1/13 3:06:40

文章目录

  • 一、线程互斥
    • (一)四个概念
      • 1.临界资源
      • 2.临界区
      • 3.互斥特性
      • 4.线程互斥
      • 5.原子性
  • 二、互斥
    • (一)在执行语句的任何地方,线程可能被切换走
    • (二)切换会保存上下文
    • (三)抢票场景中的问题
    • (四)解决方案
  • 三、互斥量(上锁)
    • (一)概念
    • (二)加锁小例子
    • (三)互斥量接口
      • 1.初始化互斥量
      • 2.销毁互斥量
      • 3.互斥量加锁和解锁
      • 4.改进上面的售票系统
    • (四)加锁方式
      • 1.基本的传锁(传不了线程名字)
      • 2.既传name又传锁
  • 二、加锁的原理探究
    • (一)上锁后的临界区内仍可以进程切换
    • (二)在我被切走的时候,绝对不会有线程进入临界区
    • (三)加锁是原子的
      • 1.xchgb 交换是原子的
      • 2.加锁原理
    • (四)C++ 加锁
  • 五、可重入&&线程安全
    • (一)线程安全
      • 1.概念
      • 2.常见的线程不安全的情况
      • 3.常见的线程安全的情况
      • 4.常见不可重入的情况
      • 5.常见可重入的情况
      • 6.可重入与线程安全联系
      • 7.可重入与线程安全区别

一、线程互斥

(一)四个概念

1.临界资源

临界资源:多个执行流都能看到并能访问的资源,临界资源。

2.临界区

临界区:多个执行流代码中有不同的代码,访问临界资源的代码,我们称之为临界区。

3.互斥特性

互斥特性:当我们访问某种资源的时候,任何时刻,都只有一个执行流在进行访问,这个就叫做:互斥特性!!!,访问临界资源,通常对临界资源起保护作用。

4.线程互斥

线程互斥:指的是在多个线程间对临界资源进行争抢访问时有可能会造成数据二义,因此通过保证同一时间只有一个线程能够访问临界资源的方式实现线程对临界资源的访问安全性

5.原子性

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

二、互斥

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,会带来一些问题。
    例子:模拟抢票

(一)在执行语句的任何地方,线程可能被切换走

int tickets;
tickets--

tickets->是由3条语句完成的:

– 操作并不是原子操作,而是对应三条汇编指令:

① load tickets to 寄存器
② 更新寄存器里面的值,执行-1操作;
③ 将新值,从寄存器写回共享变量ticket的内存地址

(二)切换会保存上下文

CPU内的寄存器是被所有的执行流共享的,但是寄存器里面的数据是属于当前执行流的上下文数据。线程被切换的时候,需要保存上下文;线程被换回的时候,需要恢复上下文。

(三)抢票场景中的问题

情况1:线程A先抢到一张票时,寄存器中tickets 10000——>9999, 还未写回内存,A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,直接抢了9950张,还剩50张,此时B的时间片到了,又切回线程A,又把9999写入内存,就错误了。

情况2:或者在抢最后一张时,线程A先抢最后一张票,if (tickets > 0)为真,进入if语句,此时A的时间片到了就被切走了,开始执行线程B了;线程B也抢票,此时显示票数仍是1,if (tickets > 0)为真,进入if语句,并执行tickets–;,tickets变为0,此时B的时间片到了,又切回线程A,线程A又继续执行tickets–;,此时直接把票数减到了负数,就出错了。

#include<iostream>
#include<unistd.h>
#include<string>
#include<cstdio>
#include<pthread.h>

using namespace std;

int tickets = 10000;

void *getTicket(void *args) {
    const char *name = static_cast<const char *>(args); 
    while (true) {
        if (tickets > 0) {
            usleep(1254);
            cout << name << " 抢到了票, 票的编号: " << tickets << endl;
            tickets--;
         //模拟其他业务逻辑的执行
        }
        else {
            cout << "票抢完了 >>>>> !!!!!" << endl;
            break;
        }
    }
    return nullptr;
}
int main() {
  
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, getTicket, (void *)"thread 1");
    pthread_create(&t2, nullptr, getTicket, (void *)"thread 2");
    pthread_create(&t3, nullptr, getTicket, (void *)"thread 3");
    pthread_create(&t4, nullptr, getTicket, (void *)"thread 4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    return 0;
}

在这里插入图片描述

(四)解决方案

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

原子性:一件事要么不做,要么全做完

把tickets–这个临界区设为原子的,使不想被打扰,进行加锁。

三、互斥量(上锁)

在这里插入图片描述

(一)概念

加锁范围:临界区,只要对临界区加锁,而且加锁的力度越细越好
加锁本质:加锁的本质是让线程执行临界区代码串行化
加锁是一套规范,通过临界区对临界资源进行访问的时候,要加就都要加

锁保护的是临界区, 任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁!
那这把锁,本身不就也是临界资源吗?锁的设计者早就想到了!

pthread_mutex_lock: 竞争和申请锁的过程,就是原子的!申请锁的过程不会中断,不会被打扰。

难度在加锁的临界区里面,就没有线程切换了吗????

mutex简单理解就是一个0/1的计数器,用于标记资源访问状态:

0表示已经有执行流加锁成功,资源处于不可访问,
1表示未加锁,资源可访问。

(二)加锁小例子

下列操作中,需要执行加锁的操作是()[多选]

A.x++;

B.x=y;

C.++x;

D.x=1;

答:D 常量的直接赋值是一个原子操作

ABC选项中涉及到了数据的运算,则涉及从内存加载数据到寄存器,在寄存器中运算,将寄存器中数据交还内存的过程,因此需要加锁保护的操作中,正确选项为:ABC

(三)互斥量接口

1.初始化互斥量

初始化互斥量有两种:
方法1 静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2 动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

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

2.销毁互斥量

注意:

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)

3.互斥量加锁和解锁

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

调用 pthread_ lock 时,可能会遇到以下情况:

  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

4.改进上面的售票系统

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h> 
using namespace std;
int tickets = 10000;

pthread_mutex_t mutex;

void *getTickets(void* args) {
    const char* name = static_cast<const char *>(args);

    while (true) {
        pthread_mutex_lock(&mutex);
        if (tickets > 0) {
        
        usleep(800);
        
        cout << name << " 抢到了票, 票的编号: " << tickets << endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
        }
        else {
            cout << name << "] 已经放弃抢票了,因为没有了..." << endl;
            pthread_mutex_unlock(&mutex);
            break;

        }
  
    }
      return nullptr;
}

int main() {
    pthread_mutex_init(&mutex,nullptr); // 上锁

    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    pthread_create(&tid1, nullptr, getTickets, (void *)"thread 1");
    pthread_create(&tid2, nullptr, getTickets, (void *)"thread 2");
    pthread_create(&tid3, nullptr, getTickets, (void *)"thread 3");
    pthread_create(&tid4, nullptr, getTickets, (void *)"thread 4");


    int n = pthread_join(tid1, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid2, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid3, nullptr);
    cout << n << ":" << strerror(n) << endl;
    n = pthread_join(tid4, nullptr);
    cout << n << ":" << strerror(n) << endl;
 
    pthread_mutex_destroy(&mutex);// 销毁锁

}

在这里插入图片描述

(四)加锁方式

1.基本的传锁(传不了线程名字)

#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

int tickets = 1000;

void* startRoutine(void * args) {
    pthread_mutex_t* mutex_p = static_cast<pthread_mutex_t*>(args);

    while (true) {
        pthread_mutex_lock(mutex_p);
        if (tickets > 0) {
            usleep(1000);
            cout << "thread: " << pthread_self() << "get a ticket: " << tickets << endl;
            tickets --;
            pthread_mutex_unlock(mutex_p);
            //做其他的事
            usleep(500);
        }
        else {
            pthread_mutex_unlock(mutex_p);
            break;
        }
    }
     return nullptr;
}
int main() {

   pthread_t t1, t2, t3, t4;
 
    static pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
    pthread_create(&t1, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t2, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t3, nullptr, startRoutine, (void *)&mutex);
    pthread_create(&t4, nullptr, startRoutine, (void *)&mutex);
 
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex);
    return 0;
}

在这里插入图片描述

2.既传name又传锁

二、加锁的原理探究

(一)上锁后的临界区内仍可以进程切换

我在临界资源对应的临界区中上锁了,临界区还是多行代码,是多行代码就可以被切换。加锁 不等于 不会被切换。加锁后仍然可以切换进程,因为线程执行的加锁解锁等对应的也是代码,线程在任意代码处都可以被切换,只是线程加锁是原子的——要么你拿到了锁,要么没有。

(二)在我被切走的时候,绝对不会有线程进入临界区

因为每个线程进入临界区都必须先申请锁!
假设当前的锁被A申请走了,即便当前的线程A没有被调度,因为它被切走的时候是抱着锁走的.
其他线程想进入临界区需要先申请锁,但是已经有线程A持有锁了,则其他线程在申请时会被阻塞。
即:一旦一个线程持有了锁,该线程根本就不担心任何的切换问题!对于其他线程而言,线程A访问临界区,只有没有进入和使用完毕两种状态,才对其他线程有意义!
总之:对于其他线程而言,线程A访问临界区具有一定的原子性
注意:尽量不要在临界区内做耗时的事情!因为只有持有锁的线程能访问,其他线程都会阻塞等待。

(三)加锁是原子的

  • 每一个CPU任何时刻只能有一个线程在跑

  • 单独的一条汇编代码是具有原子性的

1.xchgb 交换是原子的

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题 为了实现互斥锁操作,大多数体系结构(芯片体系结构)都提供了swap或exchange指令,该指令的作用是使用一条汇编代码把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

2.加锁原理

mutex中的值默认是1
%al :CPU中的寄存器( 凡是在寄存器中的数据,全部都是线程的内部上下文! !)
mutex :内存中的一个变量

在这里插入图片描述
加锁原理解释:线程A执行 movb $0,%al :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是1) 的值,交换后 寄存器%al中是1, 变量mutex中是0。
还未执行判断,此时突然进程切换,线程A会自动带走%al中的上下文数据1,线程B开始执行:线程B执行 movb $0,%al :把0放入寄存器%al中。然后执行xchgb %al, mutex:通过一条汇编代码交换 寄存器%al (值是0) 和 变量mutex (值是0) 的值,交换后 寄存器%al 和 变量mutex中都是0。再判断——>因为%al是0,不大于0就挂起。此时线程B挂起,该线程A继续执行,线程A会把自己上下文数据恢复到%al中,此时%al=1,该执行判断了——>因为%al是1,就返回。这样就成功做到:多个线程看起来同时在访问寄存器,但是互不影响

在这里插入图片描述

(四)C++ 加锁

五、可重入&&线程安全

(一)线程安全

线程安全指的是在多线程编程中,多个线程对临界资源进行争抢访问而不会造成数据二义或程序逻辑混乱的情况。

线程安全的实现,通过同步与互斥实现。

  • 具体互斥的实现可以通过互斥锁和信号量实现
  • 而同步可以通过条件变量与信号量实现。

线程是安全的,但是线程中调用的函数不一定是可重入函数,因为线程安全指的是当前线程中对各项操作时安全的,但不表示内部调用的函数是安全的,两个之间并没有必然关系

1.概念

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。(我们写的不加锁的抢票函数就是线程不安全函数,因为可能抢票抢到-1)

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

2.常见的线程不安全的情况

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

3.常见的线程安全的情况

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

4.常见不可重入的情况

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

5.常见可重入的情况

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

6.可重入与线程安全联系

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

7.可重入与线程安全区别

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/590041.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

用HTML、CSS和JavaScript实现鼠标可交互的3D太阳和月亮切换效果

部分数据来源&#xff1a;ChatGPT 引言 太阳和月亮对于我们来说是一种常见的对比&#xff0c;这篇文章将介绍一个使用HTML、CSS和JavaScript创建的网页场景&#xff0c;能够把太阳和月亮切换展示给用户。这个场景能够让用户使用鼠标和滚轮与场景互动&#xff0c;带来更多的趣…

解锁Qt QListWidget的全部潜力——用最佳实践和技巧赢得用户的喜爱和赞誉!

文章目录 前言一、属性和方法添加列表项获取当前选中的列表项删除列表项列表显示模式交替背景色 二、信号与槽选中的行数变化item被点击 三、解决icon图标模式下图标不对称的问题1、设置属性2、面向结果的手动换行 总结 前言 在现代的GUI应用程序中&#xff0c;列表框是必不可…

什么是千兆光模块和万兆光模块?它们有什么区别?

众所周知千兆光模块和万兆光模块的主区别在于它们的传输速率不一样&#xff0c;那你还知道千兆光模块和万兆光模块的其他区别吗&#xff1f;接下来海翎光电的小编将对千兆光模块和万兆光模块的区别进行详细解析。 什么是千兆光模块&#xff1f; 千兆光模块即传输速率为1000Mbps…

Java之路:构建坚实基础,系统学习Java技术的终极指南

无论是初学者还是有经验的专业人士&#xff0c;在学习一门新的IT技术时&#xff0c;都需要采取一种系统性的学习方法。作为一名Java技术er&#xff0c;下面我将介绍我是如何系统的学习Java技术的。 一、Java技术介绍 Java是一种广泛应用于软件开发的高级编程语言&#xff0c;…

数据链路层:点对点协议PPP

数据链路层&#xff1a;点对点协议PPP 笔记来源&#xff1a; 湖科大教书匠&#xff1a;点对点协议PPP 声明&#xff1a;该学习笔记来自湖科大教书匠&#xff0c;笔记仅做学习参考 数据链路层只负责直接相连的两个结点之间的通信 PPP是点对点数据链路层协议 用户通过ISP接入因特…

sklearn实现余弦相似度计算

from sklearn.metrics.pairwise import cosine_similarity cosine_similarity() 这个函数的输入是 n 个长度相同的 list 或 array 函数的处理是计算这 n 个 list 两两之间的余弦相似性 最后生成的是一个 n*n 的相似性矩阵s&#xff0c;s[i][j] 表示输入中第 i 个和第 j 个元…

如何在 Ubuntu Linux 上使用 SNAP 安装 Docker?

Docker 是一种开源的容器化平台&#xff0c;它允许开发人员将应用程序和其依赖项打包到一个可移植的容器中&#xff0c;以便在不同的环境中运行。在 Ubuntu Linux 上&#xff0c;我们可以使用 SNAP&#xff08;一种软件包管理系统&#xff09;来安装和管理 Docker。本文将详细介…

ProtoBuf 语法(一)

系列文章 ProtoBuf 语法&#xff08;二&#xff09; ProtoBuf 语法&#xff08;三&#xff09; 文章目录 前言一、字段规则二、消息类型的定义与使用2.1 定义2.2 使用 三、enum 类型3.1 定义规则3.2 注意事项 四、any 类型4.1 类型说明4.2 类型使用 五、oneof 类型六、map 类型…

HACKER KID: 1.0.1实战演练

文章目录 HACKER KID: 1.0.1实战演练一、前期准备1、相关信息 二、信息收集1、端口扫描2、访问网站3、扫描目录4、查看源码5、请求参数6、burpsuite批量请求7、编辑hosts文件8、DNS区域传输9、编辑hosts10、访问网站11、注册账号12、burpsuite抓包13、XML注入14、解密15、登录网…

供应链对于小程序、app等平台能带来什么好处?

供应链对于小程序、 app等平台的重要性不言而喻&#xff0c;这是一个企业生存的根本&#xff0c;只有保证了供应链&#xff0c;才能获得足够的产品和服务&#xff0c;保证企业能够长期稳定发展。因此很多企业都开始重视供应链&#xff0c;同时也在为之努力。 那么&#xff0c;…

gradle环境的spring boot搭建

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

tensorrt yolov7 推理

参考 源码修改如下&#xff1a;如果将源代码cpp/norm/yolo.hpp修改为自己训练的数据时修改如下&#xff1a; class YOLO{ const char* INPUT_BLOB_NAME "images"; const char* OUTPUT_BLOB_NAME "output"; }根据自己转换onnx模型采用netron打开查看 输入…

(2022,MoCA)Few-shot 图像生成的原型记忆(Prototype Memory)和注意力机制

Prototype Memory and Attention Mechanisms for Few Shot Image Generation 公众号&#xff1a;EDPJ 目录 0. 摘要 1. 简介 2. 相关工作 3. 方法 3.1 原型记忆学习 3.2 记忆概念注意力&#xff08;MEMORY CONCEPT ATTENTION&#xff0c;MoCA&#xff09; 3.3 空间上…

自平衡二叉树(AVL)及四种旋转方式详解

推荐可视化插入、删除节点的二叉树网站&#xff1a;AVL Tree Visualzation (usfca.edu) 1. 概述 AVL树是一种自平衡二叉搜索树&#xff0c;他是搜索二叉树(BST)的优化&#xff0c;它在每次插入或删除操作后&#xff0c;通过旋转节点来保持树的平衡性。AVL树的平衡条件是任意节…

代码随想录算法训练营第三十八天 | 力扣 509. 斐波那契数, 70. 爬楼梯, 746. 使用最小花费爬楼梯

509. 斐波那契数 题目 509. 斐波那契数 斐波那契数 &#xff08;通常用 F(n) 表示&#xff09;形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始&#xff0c;后面的每一项数字都是前面两项数字的和。也就是&#xff1a; F(0) 0&#xff0c;F(1) 1 F(n) F(n - 1) F(…

几种经典算法

1.分治法 分治法也叫做分而治之法。核心思想是将一个难以直接解决的大问题依照相同的概念分割成两个或者多个相同的小问题&#xff0c;以便各个击破。 如图所示&#xff1a; 2.递归法 递归法和分而治之法像一对孪生兄弟&#xff0c;都是将一个复杂的算法问题进行分解&#x…

【JAVAWEB】CSS

目录 1.CSS是什么 2.基本语法规范 3.引入方式 3.1内部样式表 3.2行内样式表 3.3外部样式 4.代码风格 4.1样式风格 4.2样式大小写 4.3空格规范 5.选择器 5.1选择器的功能 5.2选择器的种类 1.基础选择器 2.复合选择器 6.常用元素属性 6.1字体属性 设置字体font-…

配置静态ip

1.切换到root用户&#xff08;当前永久&#xff0c;不是5分钟权限失效那种&#xff09; su root #普通用户切换到root用户 2.cd到网络配置文件夹network-scripts目录下 cd /etc/sysconfig/network-scripts ls #ls查看文件目录 #找到ifcfg-exx这个格式的文件&#xff0c;我这…

IIC总线协议的死锁问题

目录 1. IIC的特性 2. IIC死锁问题分析 3. 常见的IIC死锁问题解决方法 1. IIC的特性 IIC协议是一个允许一主多从通信的协议&#xff0c;只能用于短距离通信&#xff0c;并且只需要两根信号线来交换信息。 IIC的两根信号是SCL和SDA&#xff0c;SCL是时钟信号线&#xff0c;S…

【Linux】多线程01 --- 理解线程 线程控制及封装

&#x1f34e;作者&#xff1a;阿润菜菜 &#x1f4d6;专栏&#xff1a;Linux系统编程 目录 一、线程概念 -- 理解线程与进程的区别和联系1. 再次理解用户级页表和进程地址空间2.理解Linux的轻量级进程3. 线程的属性4.线程的优点和缺点二、线程的控制 --- 学学接口使用 一、线程…