线程详解
线程概念
什么是线程:是进程内的一个执行分支,是一个新的执行流。线程的执行粒度要比进程要细。
-
linux中线程如何理解
linux实现方案,在Linux中,线程在进程”内部”执行,线程在进程的地址空间内运行
任何执行流都要有资源
cpu只有调度执行流的概念不关心是进程还是线程
linux没有真正意义上的线程,而是用“进程”的数据结构模拟的线程
pcb也就是一个执行流
linux中的执行流,是一个轻量级进程。
-
重新定义线程和进程
线程是操作系统调度的基本单位
进程是承担系统资源的基本实体
进程内部包含线程,线程是进程内部的执行流资源
-
重谈地址空间
4kb,4*2的10次方。
线程目前分配资源,本质就是分配地址空间范围
虚拟地址是如何转换到物理地址的?
32位的操作系统是32位的虚拟地址。
虚拟地址到物理地址的转换:
页表:
二级页表大部分都是不全的。
在32位的虚拟地址中,地址通常被分为三个部分:前10位,中间10位,和后12位这三个部分的作用如下:
前10位:这部分被称为页目录索引(Page Directory Index)。它用于在页目录表中定位一个页目录项(Page Directory Entry,PDE)。页目录项中包含了页表的物理地址
中间10位:这部分被称为页表索引(Page Table Index)。它用于在页表中定位一个页表项(Page Table Entry,PTE)。页表项中包含了物理页框的地址
后12位:这部分被称为页内偏移(Offset)。它表示在物理页框中的具体位置
这种地址划分方式是为了实现虚拟内存到物理内存的映射。当一个进程需要访问其虚拟内存空间中的某个地址时,操作系统会首先通过查找页目录表,使用前10位找到对应的页目录项,然后从该页目录项中读取出页表的物理地址。接着,操作系统会使用中间10位在页表中找到对应的页表项,从该页表项中读取出物理页框的地址。最后,操作系统会使用后12位在物理页框中找到具体的位置,从而完成地址的转换。这就是所谓的虚拟内存技术,它使得每个进程都认为自己独占了全部的内存资源,极大地简化了程序的编写和执行这就是32位虚拟地址中前10位,中间10位,和后12位的作用。
线程切换不需要重新cache数据,cache是缓存的热数据。
起始地址 + 类型 = 起始地址 + 偏移量
线程相对于进程的优势:
创建和释放更加轻量化
切换更加轻量化
线程切换的页表和地址空间都不需要切换。
线程在执行就是进程在执行。
pthread库
内核中没有线程的概念, 只有轻量级进程,不会直接给我们提供轻量级进程的系统调用
所以有了用户层的第三方库,pthread线程库,在应用层对轻量级进程接口进行封装。为用户直接提供线程的接口
Linux中编写多线程代码需要使用第三方库 pthread
1.pthread_create函数
pthread_create
函数是 POSIX 线程 (pthreads) 库的一部分,通常用于在类 Unix 操作系统环境中创建和管理线程。线程允许程序在同一进程内并发地执行多个任务。
以下是 pthread_create
的工作原理、参数以及一个示例:
函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
参数
-
*pthread_t thread: 指向
pthread_t
类型变量的指针,该变量将在成功创建线程后存储线程 ID。 -
*const pthread_attr_t attr: 指向
pthread_attr_t
结构的指针,该结构指定新线程的属性。如果此参数为NULL
,则使用默认属性。大部分情况不考虑。 -
void *(*start_routine)(void *): 指向新线程将要执行的函数的指针。该函数应接受一个
void *
类型的参数并返回一个void *
类型的值。 -
*void arg: 传递给启动例程的单个参数。如果不需要参数,可以传递
NULL
。
返回值
- 成功时,
pthread_create
返回 0。 - 失败时,返回一个错误号码,指示错误原因。
示例:
以下是一个简单的示例,演示如何使用 pthread_create
:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
// 定义线程要运行的函数
void *print_message(void *ptr) {
char *message = (char *)ptr;
printf("%s\n", message);
return NULL;
}
int main() {
pthread_t thread1, thread2;
const char *message1 = "来自线程1的问候";
const char *message2 = "来自线程2的问候";
// 创建第一个线程
if (pthread_create(&thread1, NULL, print_message, (void *)message1)) {
fprintf(stderr, "创建线程1时出错\n");
return 1;
}
// 创建第二个线程
if (pthread_create(&thread2, NULL, print_message, (void *)message2)) {
fprintf(stderr, "创建线程2时出错\n");
return 1;
}
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return 0;
}
在这个示例中,我们定义了一个名为 print_message
的函数,线程在创建后将运行这个函数。我们创建了两个线程,每个线程打印一条消息。pthread_join
用于等待线程完成,以确保主程序在所有线程完成之前不会退出。
线程的标识符LWP
ps -aL查看所有的轻量级进程
LWP轻量级进程的标识符。
PID = LWP是主线程
LWP是调度的基本单位
任何一个线程被杀掉进程都会被杀掉
当一个线程崩溃了,进程也就退出了。
一旦一个线程把自己的函数执行完了就退出了
线程的tid:线程tId是在用户层维护的,LWP是操作系统层面,每一个线程库级别的tcb的起始地址叫做线程的tid
栈在线程中私有的
堆空间也是被所有线程共享的
线程的概念是线程库给我们维护的
线程栈:
每一个线程在运行的时候一定要有自己的栈结构
主线程用主结构的栈结构即可
线程和线程之间没有秘密,线程上的栈上的数据,也是可以被其他线程看到并且访问的
___thread定义一个单独属于自己的线程全局变量,这是一个编译选项,编译的时候会给每个线程的局部存储开辟一份。
线程的局部存储只能够用来定义内置类型,不能用来修饰自定义类型
除了主线程,所有其他线程的独立栈,都在共享区,具体来讲实在pthread库中,tid指向用户tcb中。
线程栈中的数据其他线程是可以看到的
全局变量是被所有线程同时看到并且访问的
假如我们想要给每一个线程开辟一个全局变量,我们需要用到线程的局部存储,线程局部存储在每个线程的独立栈结构中。
线程的局部存储:
__thread int num = 100;
//局部存储只能定义内置变量
线程控制
线程等待
一个线程被创建出来了,哪个线程先运行?我们不清楚
但是最后退出的就是主线程。防止新线程造成内存泄露。创建新线程的退出结果。
main thread等待的时候,默认是阻塞等待的。
pthread_join()函数:
1. **等待其他线程结束**:当调用 `pthread_join()` 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行¹. 这对于管理多个线程的资源使用和确保代码的正确执行顺序非常重要².
2. **对线程的资源进行回收**:如果一个线程是非分离的(默认情况下创建的线程都是非分离),并且没有对该线程使用 `pthread_join()` 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”¹.
因此,`pthread_join()` 既用于等待其他线程的终止,也用于确保线程资源的回收。如果想获取某个线程执行结束时返回的数据,也可以使用 `pthread_join()` 来实现³。希望这对你有帮助!
保证新线程先退出,主线程后退出。
线程不用考虑异常,线程一旦异常了,进程就直接退出了。
线程分离:
int pthread_detach(pthread_t thread)
线程分离是一种设置,当线程被标记为分离状态后,线程结束时,其资源会被系统自动回收,而不再需要在其他线程中使用 pthread_join()
这样,你无需显式等待线程结束,系统会自动处理资源释放。如果你不关心线程的返回值,或者不想让线程继续执行,可以选择将线程设置为分离状态。
#include <stdio.h>
#include <pthread.h>
void* ThreadEntry(void* arg) {
// 线程执行的代码
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, ThreadEntry, NULL);
pthread_detach(tid); // 将线程设置为分离线程
// 主线程的其他操作
return 0;
}
终止线程:
pthread_exit(void*(100));
return (void*)100
这两种写法是等价的,都可以让进程退出。
线程取消:
int pthread_cancel(pthread_t thread)
如果一个线程是被取消的,那么这个线程的函数的返回值就是-1,就是PTHREAD_CANCELE
线程的互斥
对一个全局变量进行多线程并发–/++操作是否安全?
我们的并发操作对100操作不是原子的。
怎么解决这个问题:
对于共享数据的访问,要保证任何时候只有一个执行流访问——互斥
锁的使用:
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
void * getTicke(void* args)
{
while(true)
{
pthread_mutex_lock(td->lock);//申请锁成功才能往后执行,不成功,阻塞等待
if(tickets > 0)
{
......
pthread_mutex_unlock(td->lock);
}
else
{
pthread_mutex_unlock(td->lock);
break;
}
}
return nullptr;
}
加锁的本质是用时间来换空间
加锁的表现:线程对于临界区代码串行执行
加锁的原则:尽量的要保证临界区代码越少越好。
纯互斥环境,如果锁分配不够合理,容易导致其他线程的饥饿。不是说只要有互斥,必有饥饿
适合纯互斥的场景, 就用互斥
锁本身就是共享资源:所以申请锁和释放锁的过程本身就是原子的
线程在任何时候都可以被切换,就算再临界区内也可以线程切换,
只有我是持有锁被切换的,其他线程也不能进入临界区访问临界资源
对于其他线程来讲,一个线程要么没有锁,要么释放锁 。当前进程访问临界区的过程,对于其他线程是原子的。
同步:
让所有的线程获取锁,按照一定的顺序。按照一定的顺序获取资源就是同步。 (顺序性)
锁的原理:
原子:一条汇编就是原子的
pthread_mutex_lock() 函数的原理
lock:
movb $b,%al
xchgb %al,mutex
if(al寄存器内容 > 0){
return 0;
}else
挂起等待;
goto lock;
unlock:
movb $1,mutex
唤醒等待mutex进程;
return 0;
死锁
死锁是指一组进程中各个进程均占有不会释放的资源,但因为互相申请被其他进程所占用不会释放资源而处于一种永久等待的状态。
死锁需要同时满足这4个条件:
互斥条件:一个资源每次只能被一个执行流申请使用
请求与保持条件:一个执行流因为请求资源而阻塞时,对已经获得的资源保持不放
不剥夺条件:一个执行流对已经获得的资源,在没有使用完之前,不能强行剥夺
循环等待条件:若干个执行流之间形成一种头尾相接的循环等待资源的关系。
条件变量:
当一个线程互斥访问某个变量时,他可能发现在其他线程改变其状态前,它什么也做不了。
例如:一个线程访问队列时,发现队列为空,他只能等待,直到其他线程将一个节点添加到队列当中。这种情况就需要用到条件变量。
pthread_cond_init()
:
是用于初始化条件变量的函数,条件变量是多线程编程中的一种同步机制,允许线程在等待某个条件满足时进入休眠状态,并在条件满足时被唤醒。条件变量通常与互斥锁一起使用,以防止竞争条件。
函数原型:
#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
参数
cond
: 指向要初始化的条件变量的指针。attr
: 指向条件变量属性对象的指针。如果传递NULL
,则使用默认属性。
返回值
- 成功时返回
0
。 - 失败时返回一个错误码。
pthread_cond_wait()
是 POSIX 线程库中用于条件变量等待的函数。它使线程进入等待状态,直到某个条件满足。在此过程中,pthread_cond_wait()
会自动释放与条件变量关联的互斥锁,并在条件变量被唤醒时重新获取该互斥锁。这种行为可以防止等待条件的线程占用互斥锁,使得其他线程可以修改条件并唤醒等待线程。
函数原型
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
cond
: 指向条件变量的指针。mutex
: 指向与条件变量关联的互斥锁的指针。
返回值
- 返回 0 表示成功。
- 返回错误码表示失败(例如:
EINVAL
表示无效的条件变量或互斥锁,EPERM
表示互斥锁未被调用线程持有)。
cp问题
生产者 VS 生产者 : 竞争关系,互斥关系
生产者 VS 消费者 :互斥关系,原子性。同步。
消费者 VS 消费者 : 互斥关系
3种关系,2种角色–生产者消费者,1个消费场所–特定结构的内存空间
优点:支持忙闲不均。生产与消费进行解耦。