【Linux】线程篇Ⅱ:

news/2024/10/20 3:16:38/

线程Ⅱ

  • 🔗接上篇【线程篇Ⅰ】
  • 五、线程库 和 线程 id
  • 六、互斥(加锁)
    • 1. 一些接口
      • 1.1 pthread_mutex_init 函数:锁的初始化
      • 1.2 pthread_mutex_destroy 函数:锁的销毁
      • 1.3 pthread_mutex_lock 函数:上锁
      • 1.4 pthread_mutex_unlock 函数:解锁
      • 1.5 使用案例 及 注意细节
    • 2. 原理


🔗接上篇【线程篇Ⅰ】

👉🔗【Linux】线程篇Ⅰ:线程和task_struct 执行流的理解、相关接口命令、线程异常、线程的私有和共享


五、线程库 和 线程 id

对于 Linux 目前实现的 NPTL 实现而言

pthread_t 类型的线程 ID,本质就是一个 进程地址空间 上的一个地址。

线程篇Ⅰ中涉及到的接口,主要是原生系统库的系统级解决方案,虽然在库中实现但是跟语言一样,比语言更靠近底层罢了。而 C++ 其实是对线程库做的封装!!

虽然原生接口效率更高,但是 语言接口 有跨平台性。不确定只在 Linux 下跑的还是推荐使用语言接口。

在这里插入图片描述

另:有如下 线程独立栈 的理解和应用
在这里插入图片描述

  • 通过更改 寄存器 ebp、esp 就能 切换 线程栈
  • 数据通过 ebp - 偏移量 进行访问或者开辟空间(ebp 是一个相对稳定的位置)
  • 首地址之所以是低地址,是因为栈的扩展方式 和 ebp 开辟空间的方式(如图)。
/*__thread*/ int g_val = 100;void *threadRoutine(void* args)
{string name = static_cast<const char*>(args);int cnt = 5;while(cnt){// 局部变量cout << name << " : " << cnt-- << " : " << hexAddr(pthread_self()) << " &cnt: " << &cnt << endl;// 全局变量cout << name << " g_val: " << g_val++ << ", &g_val: " << &g_val << endl;sleep(1);}return nullptr;
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1");pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2"); pthread_join(t1, nullptr);pthread_join(t2, nullptr);return 0
}
  • 线程函数中的 临时变量,储存在 进程地址空间 共享区的 线程库的 线程栈 中,线程各自使用互不影响。

  • 全局变量 储存在主线程的 已初始化数据段,其他新线程访问全局变量访问的是同一个,是并发访问。

  • 前面 声明 __thread (局部存储)字样的全局部变量 ,储存在已初始化数据段,并在产生新线程后,拷贝到 线程库的 线程局部存储段 中,供各自线程使用且互不影响。(由于地址空间的分布规则,全局数据被拷贝后的地址会比原来的地址大很多,如上图示)

      __thread 定义的全局变量 可以应用在:带出某一个函数 被 各个线程调用的次数
    
  • __thread 局部存储 与 static 静态变量 没有关系哦,静态变量被所有线程共享的,存在已初始化数据段。


六、互斥(加锁)

任何一个时刻,都只允许一个执行流在进行共享资源的访问,叫做 互斥,也叫 加锁

  • 我们把任何一个时刻,都只允许一个执行流在进行访问的 共享资源,叫做 临界资源
  • 任何一个线程,都有代码 访问临界资源 的叫做,临界区
  • 不访问临界资源 的区域叫做,非临界区
  • 控制进出临界区的手段(加锁)造就了临界资源。

加锁 可以保证一系列操作,要不做完 要不不做,这种特性叫做 原子性,临界资源是有原子性的。

1. 一些接口

pthread_mutex_t 是原生系统库给我们提供的一种数据类型,用来创建锁。

以下接口头文件相同:

 #include <pthread.h>

1.1 pthread_mutex_init 函数:锁的初始化

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

参数 restrict mutex:

  • 需要初始化的锁名称

参数 restrict attr:

  • 属性,保持默认设置为 nullptr

注意:静态或者全局的锁,可以用如下的宏直接对锁做初始化

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

1.2 pthread_mutex_destroy 函数:锁的销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数 mutex:

  • 需要销毁的锁的名称

1.3 pthread_mutex_lock 函数:上锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数 mutex:

  • 需要销毁的锁的名称
int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数 mutex:

  • 需要销毁的锁的名称

1.4 pthread_mutex_unlock 函数:解锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数mutex:

  • 需要解锁的锁的名称

1.5 使用案例 及 注意细节

🌰案例:实现多线程同时抢票

// 临界资源
int tickets = 1000;                               class TData
{
public:TData(const string &name, pthread_mutex_t *mutex):_name(name), _pmutex(mutex){}~TData(){}
public:string _name;pthread_mutex_t *_pmutex;
};void threadRoutine(void *args)
{TData *td = static_cast<TData *>(args);while (true){pthread_mutex_lock(td->_pmutex); if (tickets > 0){usleep(2000);cout << td->_name << " get a ticket: " << tickets-- << endl; // 临界区pthread_mutex_unlock(td->_pmutex);}else{pthread_mutex_unlock(td->_pmutex);break;}// 我们抢完一张票的时候,我们还要有后续的动作// usleep(13);}
}int main()
{pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);pthread_t tids[4];int n = sizeof(tids)/sizeof(tids[0]);for(int i = 0; i < n; i++){char name[64];snprintf(name, 64, "thread-%d", i+1);TData *td = new TData(name, &mutex);pthread_create(tids+i, nullptr, threadRoutine, td);}for(int i = 0; i < n; i++){pthread_join(tids[i], nullptr);}pthread_mutex_destroy(&mutex);return 0;
}

注意细节:

  1. 加锁本质就是给 临界区 加锁,凡是访问同一个 临界资源 的线程,都要进行加锁保护,而且 必须加同一把锁。加锁的粒度 要尽可能的细

  2. 由于所有线程都必须要先看到同一把锁,锁本身就是公共资源,不过 加锁和解锁本身就是原子的,可以保证自己的安全

  3. 临界区可以是一行代码,可以是一批代码,线程仍然可能在临界区任意位置被切换,加锁并不影响这一点。

    切换线程不会影响锁的安全性,比如该线程加锁后被切走了,由于这个整个临界区的原子性,没有被解锁的情况下,任何人都没有办法进入临界区。即 他人无法成功的申请到锁,因为锁被该线程拿走了。

  4. 这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁), 原子性就体现在这

2. 原理

  1. swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性

  2. 寄存器硬件只有一套,但是寄存器内部的数据是每一个线程都要有的。寄存器 != 寄存器的内容(执行流的上下文)

  3. 加锁解锁的代码怎么执行的?

  • 首先:在内存中创建锁对象 mutex,这是一个共享对象,里面置 1

    • 这个 1 很重要,一个锁对象只会生成一个 1,1 在哪个线程的手上就是哪个线程有权访问临界资源,这个 1 只会流转,不会新建!
  • 接着,线程调用接口 pthread_mutex_lock()

// 实现接口,编译的伪代码如下
lock:movb $0, %al	// al是放线程上下文的寄存器,这里调用了线程,向自己的上下文写入 0xchgb %al, mutex	// 将al寄存器和内存中mutex里的值做交换(本质是,线程将共享数据交换到自己私有的上下文中,因为这里只有一条代码,正是这一条代码才保证了加锁的原子性)if(al寄存器的内容 > 0)return 0;else挂起等待;goto lock;
  • pthread_mutex_unlock() 的伪代码
	movb $1, %al唤醒等待 Mutex 的线程;return 0;

http://www.ppmy.cn/news/1059165.html

相关文章

【Java面试】指令重排引发问题及解决方案

一 指令重排引发的问题 什么是指令重排&#xff1f; 指令重排是指在程序执行过程中&#xff0c;为了优化性能&#xff0c;编译器或处理器可能会重新安排代码指令的执行顺序&#xff0c;但要求不改变程序的最终结果。 在多线程环境中&#xff0c;指令重排可能会引发一些问题&a…

iconfont 图标在vue里的使用

刚好项目需要使用一个iconfont的图标&#xff0c;所以记录一下这个过程 1、iconfont-阿里巴巴矢量图标库 这个注册一个账号&#xff0c;以便后续使用下载代码时需要 2、寻找自己需要的图标 我主要是找两个图标 &#xff0c;一个加号&#xff0c;一个减号&#xff0c;分别加入到…

C 语言编程规范 -- 华为

1. 代码总体原则 1.1 清晰第一&#xff0c;清晰性是易于维护&#xff0c;易于重构的程序必须具备的特征 代码首先是给人读的&#xff0c;好的代码应当可以像文章一样发生朗诵出来&#xff0c;“程序必须为阅读它的人而编写&#xff0c;只是顺便用于机器执行” – Harold Abel…

微信小程序|步骤条

步骤条是现代用户界面设计中常见的元素之一,它能够引导用户按照预定顺序完成一系列任务或步骤。在小程序中,实现步骤条可以为用户提供更好的导航和引导,使用户体验更加流畅和直观。本文将介绍如何在小程序中实现步骤条,并逐步展示实现的过程和关键技巧 目录 步骤条的作用及…

【PHP】PHP文件操作详解

PHP是一种广泛使用的服务器端脚本语言&#xff0c;用于开发Web应用程序。在PHP中&#xff0c;文件操作是一项重要的功能&#xff0c;包括文件的读取、写入、删除和其他操作。本文将详细介绍PHP文件操作的各个方面&#xff0c;并通过示例代码进行说明。 一、文件读取 要读取一…

idea启动正常,打成jar包时,启动报错

背景 自己写了个小程序&#xff0c;在idea中启动正常&#xff0c;达成jar包发布时&#xff0c;启动报错。 Caused by: java.sql.SQLException: unknown jdbc driver : at com.alibaba.druid.util.JdbcUtils.getDriverClassName(JdbcUtils.java:517) at com.alibaba.druid.pool…

香港全新的虚拟资产服务商发牌制度

香港证监会2023年2月20日通告&#xff0c;原有虛擬資產交易平台如要符合資格參與當作為獲發牌的安排&#xff0c;必須在2023 年6 月1 日至2024 年2 月29 日期間(即由2023 年6 月1 日37起計九個月內)內&#xff0c;根據《打擊洗錢條例》下的虛擬資產服務提供者制度在網上提交完全…

Linux下的Shell基础——正则表达式入门(四)

前言&#xff1a; 正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。在很多文本编辑器里&#xff0c;正则表达式通常被用来检索、替换那些符合某个模式的文本。 在Linux 中&#xff0c;grep&#xff0c;sed&#xff0c;awk 等文本处理工具都支持…