前言
本节我们将对线程id进行了解,线程私有栈等,学会线程控制,线程创建,线程终止,线程等待…
目录
1. 线程的id
- 线程创建和进程一样也得有线程id。注意: pthread_t tid,是个地址。
1.1 pthread_self:
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID.
pthread_t pthread_self(void);
注意:
- pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
1.2 线程独立栈结构:
libpthread. so是Linux系统中用于支持多线程编程的共享库,其中包含了与线程管理相关的函数和数据结构
- 线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。
Linux的线程库,虽然是原生的,在系统当中会内置的线程库,但是依旧是用户级线程库。
因为Linux没有真线程,就没办法提供真线程的调用接口。
共享库(Shared Library)是被所有线程共享的。
库可以创建多个线程,那么库要不要管理线程呢?
答案:是的。
- 要想管理就要,先描述,再组织
- 再库里面有一个struct thread_info结构体,是用来描述创建的线程的。
- 这个结构体中有线程的tid,还有一个重要的就是指向线程私有栈的栈顶指针。
- pthread_t对应的就是用户级线程的控制结构体的起始地址:
- 可以通过该地址找到线程相关属性。
- 用户的所有创建线程、等待线程、获取线程的各种属性,都是这个库帮我们去做的,控制进程内的轻量级进程来完成的。
- 每一个线程都有一个thread_info,而它的属性里面就有私有栈
- 主线程的独立栈结构,用的就是地址空间中的栈区,新线程用的栈结构,用的是库中提供的栈结构
备注:
用户级线程:栈是由库来维护,但是还是用户提供的
pthread_info不是在库里面直接创建的,而是由操作系统内核在创建线程时生成的线程控制块的信息
Linux中,线程库用户级线程库,和内核的LWP是1 : 1
1.3 线程的局部存储:
线程的局部存储,代表的是,我们的线程除了保存临时数据时有自己的线程栈,我们的ptread库还提供了一种能力:
- 如果我们定义了一个全局的变量,但是这个全局的变量想让每个线程各自私有,那么就可以使用线程局部存储这样的概念。
- 在全局变量前加上__thread即可。
- 可以理解成将这个全局变量都拷贝一份给对应的线程,所以每个线程都有自己的。
2. 线程互斥
-
临界资源:多线程执行流共享的资源就叫做临界资源
-
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
-
进程代码中,有大量的代码,只有一部分代码,会访问临界资源。
-
多个进程对临界资源做读写的代码,我们称之为临界区。
2.1 互斥性与原子性:
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完
成 - 互斥: 任何时刻,只允许一个进程/线程,访问临界资源。
互斥量mutex:
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。
- 多个线程并发的操作共享变量,会带来一些问题
2.2 线程安全:
线程安全是指在多线程环境下,对共享资源的访问不会导致数据不一致或者出现意料之外的结果。
当多个线程同时访问共享资源时,如果没有适当的同步机制或保护措施,可能会导致以下问题:
- 竞态条件(Race Condition):多个线程对同一资源进行读写操作,由于执行顺序不确定,可能导致结果的不确定性、错误的计算结果或数据丢失等问题。
- 数据竞争(Data Race):多个线程同时对同一数据进行读写操作,由于缺乏同步机制,可能导致数据的不一致性或错误的结果。
为了确保线程安全,需要采取合适的并发控制措施,如加锁机制、原子操作、信号量等。这些机制可以保证在任意时刻只有一个线程能够访问共享资源,避免数据竞争和竞态条件的发生。
补充知识点:
- 一个线程对全局变量的恶意修改 ,可能会影响其他线程安全。
- 一个线程有bug导致线程退出,导致其他线程也退出,也叫做线程退出。
- 绝大多数的系统自带的库(比如C++的STL库)都是不可重入的,并非所有容器都是线程安全的。
2.3 线程加锁与解锁:
- 加锁:
定义全局的互斥锁,所有线程能访问
初始化互斥量有两种方法:
方法一:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
方法二:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr); // 参数: // mutex:要初始化的互斥量 //
attr:NULL
- 解锁 / 销毁锁:
int pthread_mutex_destroy(pthread_mutex_t *mutex);
销毁互斥量需要注意:
- 使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁。
- 不要销毁一个已经加锁的互斥量。
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。
2.4 锁的加锁范围与锁的原子性讨论
-
加锁的范围:
-
只要对临界区加锁,而且加锁的粒度越细越好。
-
锁保护的是临界区,任何线程执行临界区代码访问临界资源,都必须先申请锁,前提是都必须先看到锁.
-
临界区中存在if条件时,需要在其前加锁
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;
}
}
- 锁的原子性讨论:
锁,本身不就也是临界资源吗,谁来给它加锁呢?锁的设计者早就想到了。
- pthread_mutex_lock竞争和申请锁的过程,就是原子的!
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
我们看一下pthread_mutex_lock和pthread_mutex_unlock的伪代码:
2.5 死锁
2.5.1 死锁的概念
- 两个线程拿着对方要的锁,自己又抱着一把锁。
- 线程1拿着A,线程2拿着B,但是这两个线程又都向对方要锁。
- 互相申请对方的锁,但是自己要的锁已经被对方拿走了
2.5.2 死锁的四个必要条件:
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
当以上四个条件同时满足时,就可能发生死锁。
避免死锁的条件:
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
2.6 Linux线程同步
互斥有可能会导致饥饿问题:
- 一个执行流,长时间得不到某种资源
- 一个线程的优先级很高,每次竞争锁时,都可以优先竞争到
- 就会导致其他线程一直竞争不到锁,造成饥饿问题。。
Linux中提供了完成同步的重要机制,叫做:条件变量。(最常用,没有之一,最常用的线程同步的策略)
2.6.1 条件变量
条件变量要和mutex互斥锁,一并使用!
- 初始化互斥量有两种方法:
方法一:全局的初始化方法
> pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
方法二:局部的初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict
attr);
参数:
cond:要初始化的条件变量
attr:NULL
- 条件变量的销毁:
int pthread_cond_destroy(pthread_cond_t *cond)
- pthread_cond_wait
-
其中timewait接口可以在条件变量下等,设置等待的时间(超时了就不等了)。
-
条件变量也是临界资源,所以这里需要一个mutex互斥锁来保证条件变量读写的原子性。
-
唤醒线程
broadcast是给在当前条件变量等待的所有线程发信号唤醒,而signal是发送信号只唤醒一个线程。
- 条件变量决定,什么时候叫醒一个线程,以前只要有锁,如果所有线程都被叫醒,大家都去参与竞争,谁抢到了算谁的,这个机制完全是由调度器决定的。
pthread_cond_wait内部已经帮我们想到了这一点,并做了相应的措施:(重点)
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex)
{
pthread_mutex_unlock(mutex);// 先解锁
// 避免因为该线程拿着锁去休眠了,导致其他线程无法申请该锁
//条件变量相关代码
pthread_mutex_lock(mutex);// 条件满足后,再加锁
}
条件变量经典错误:
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
pthread_cond_wait(&cond);// 解锁和加锁的操作,该函数会帮我们完成
pthread_mutex_lock(&mutex);// 二次申请同一把锁,出现死锁!
}
pthread_mutex_unlock(&mutex);
代码演示:
#include <iostream>
#include <vector>
#include <cstring>
#include <functional>
#include <unistd.h>
#include <pthread.h>
using namespace std;
// 定义一个条件变量
pthread_cond_t cond;
// 定义一个互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 当前不用,但是接口需要,所以我们需要留下来
// 定义全局退出变量
volatile bool quit = false;
// 三个线程都会调用这个函数
void *waitCommand(void *args)
{
string name = static_cast<char*>(args);
pthread_mutex_lock(&mutex);
// 如果不退出一直去运行
while (!quit)
{
// pthread_cond_wait内部先解锁
pthread_cond_wait(&cond, &mutex);
// 被唤醒出来之后锁已经加上了
cout << "thread id: " << pthread_self() << " running... " << name << endl;
}
pthread_mutex_unlock(&mutex);
cout << "thread quit..." << (char*)args << endl;
return nullptr;
}
int main()
{
// 初始化一个条件变量
pthread_cond_init(&cond, nullptr);
// 创建三个线程
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, waitCommand, (void*)"thread1");
pthread_create(&t2, nullptr, waitCommand, (void*)"thread2");
pthread_create(&t3, nullptr, waitCommand, (void*)"thread3");
// 主线程控制
while(true)
{
char n;
// cin和cout在交叉使用的时候,缓冲区会做强制刷新
cout << "请输入你的command: ";
cin >> n;
if(n == 'n')
{
// 唤醒在特定条件变量下等的线程
pthread_cond_signal(&cond);
}
else if (n == 'x')
{
pthread_cond_broadcast(&cond);
}
else
{
quit = true;
break;
}
usleep(100);
}
// 唤醒所有等待的条件变量
pthread_cond_broadcast(&cond);
cout << "main thread quit..." << endl;
// 释放条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
// 等待线程
int m = pthread_join(t1, nullptr);
cout << strerror(m) << endl;
m = pthread_join(t2, nullptr);
cout << strerror(m) << endl;
m = pthread_join(t3, nullptr);
cout << strerror(m) << endl;
return 0;
}
条件变量使用规范
- 等待条件代码
pthread_mutex_lock(&mutex);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
- 给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex)
尾声
看到这里,相信大家对这个Linux 有了解了。
如果你感觉这篇博客对你有帮助,不要忘了一键三连哦