线程Ⅱ
- 🔗接上篇【线程篇Ⅰ】
- 五、线程库 和 线程 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;
}
注意细节:
-
加锁本质就是给 临界区 加锁,凡是访问同一个 临界资源 的线程,都要进行加锁保护,而且 必须加同一把锁。加锁的粒度 要尽可能的细。
-
由于所有线程都必须要先看到同一把锁,锁本身就是公共资源,不过 加锁和解锁本身就是原子的,可以保证自己的安全
-
临界区可以是一行代码,可以是一批代码,线程仍然可能在临界区任意位置被切换,加锁并不影响这一点。
切换线程不会影响锁的安全性,比如该线程加锁后被切走了,由于这个整个临界区的原子性,没有被解锁的情况下,任何人都没有办法进入临界区。即 他人无法成功的申请到锁,因为锁被该线程拿走了。
-
这也正是体现互斥带来的串行化的表现,站在其他线程的角度,对其他线程有意义的状态就是:锁被我申请(持有锁),锁被我释放了(不持有锁), 原子性就体现在这
2. 原理
-
swap 或 exchange 指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性
-
寄存器硬件只有一套,但是寄存器内部的数据是每一个线程都要有的。寄存器 != 寄存器的内容(执行流的上下文)
-
加锁解锁的代码怎么执行的?
-
首先:在内存中创建锁对象 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;