文章目录
一、背景知识
1、地址空间
[!IMPORTANT]
- os进行内存管理,不是以字节为单位,而是以内存块为单位。(内存块默认大小4KB)
- 系统和磁盘进行IO的基本单位是4KB(8个连续的扇区)
2、物理内存
3、页表
4、文件缓冲区、虚拟地址
什么是文件缓冲区?
将物理内存的页框(struct page)和文件(struct file)关联起来,这个页框就是文件缓冲区。
什么是函数?
连续的代码地址构成的代码块。
函数有地址吗?
有,每行代码都有地址,而且对于同一个函数内部的代码语句,我们认为地址是连续的。
代码数据划分的本质是什么?
拆分页表。
虚拟地址的本质是什么?
虚拟地址本质是一种资源!拥有者可以通过页表进行访问。
二、多线程
1、线程的概念
[!IMPORTANT]
- 线程定义:在进程内部运行,是cpu调度的基本单位。
- 进程定义(内核观点):承担分配系统资源的基本实体。
可以粗略的理解为:同一个进程内,一个task_struct对应的执行流就是一个线程。
os怎么管理线程?
[!IMPORTANT]
先描述、再组织。
描述:struct tcb{//线程id,优先级,状态,上下文,连接属性…};
但是,在Linux中,线程是复用的进程的pcb。这样就不需要单独为线程单独设计数据结构和调度算法了。
Linux中,进程和线程的关系?
[!IMPORTANT]
在Linux中,没有线程的概念,只有轻量级进程的概念。
一个进程中,可能存在不止一个执行流,也就是说,可能存在多个pcb。以前学的进程是一个进程仅存在一个pcb,而现在是一对多的关系。
Linux中没有线程的概念,用户是怎么用线程的?
[!IMPORTANT]
在Linux底层中没有线程的概念,只有轻量级进程,为了上层能用线程,Linux把它进行了封装成了线程库。所以要用到线程的时候,需要连接一个线程库。
2、线程的操作
创建线程
想要创建线程,必须手动连接pthread库。
g++ $^ -o &@ -lpthread
// $^:是目标文件 --- main.exe->可执行程序
// $@:是依赖文件 --- main.cc->源文件
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 第三个参数,传一个函数指针,新线程所要执行的代码
// 第四个参数,作为第三个函数指针指向函数的参数。
// 主线程继续向下运行,新线程去执行其他的代码。
// 想要创建线程,必须手动连接pthread库
线程的唯一标识
对于同一个进程中的不同线程,两个线程的PID相同,但是LWP不同。
所以,唯一标识一个线程的是LWP(Light Weight Process:轻量级进程)。
LWP作为线程唯一标识,对单线程有影响吗?
没有影响,因为对于单进程而言,LWP 和 PID是一一对应的。
已经有多进程了,为什么还需要多线程?
[!IMPORTANT]
- 进程的创建,成本高;线程的创建,成本低。—启动
- 线程的调度成本低。—运行
- 删除一个线程的成本低。—删除
线程的优势:
线程的缺点:
对于一个进程中的多个线程,只要有一个线程出现了问题,多个线程都会崩溃。
对于不同系统实现线程的方式
[!IMPORTANT]
- 不同的系统对于进程和线程的实现方式都不一样,但是实现的原则都是一样的。
- 对于Linux:没有单独实现线程,只是复用pcb。
- 对于Windows:单独实现线程。
为什么线程调度的成本更低?
[!IMPORTANT]
cpu的寄存器只有一套。当调度另外一个线程时,cpu中,cache等寄存器的上下文数据和物理空间之间的拷贝的操作。
如果是两个进程的话,代码可能不一样,对与进程a来说cache里面存储的代码,在进程b用不上,需要重新进行加载。
不同线程之间的共享部分?
[!IMPORTANT]
大部分地址空间资源,多线程都是共享的!
不同线程之间的私有部分?
[!IMPORTANT]
- 一组寄存器:硬件上下文数据—线程可以动态运行。
- 栈:每个线程都要有自己的栈结构。线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈空间。
全面看待线程函数传参?
[!IMPORTANT]
参数是能传递进去的,只需要在线程函数中将参数强转成需要的类型。
我们可以传递任意类型,可以传递类对象地址(用于传递多个参数)。
传递线程函数参数是类对象的时候,建议从堆上空间开辟。
全面的看待线程函数返回?
[!IMPORTANT]
线程退出的时候,只需要考虑正常的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程。
创建多线程
std::vector<pthread_t> tids;
for (int i = 0; i < num; i ++ )
{
pthread_t tid;
char *name = new char[128];
snprintf(name, 128, "thread-%d", i+1);
pthread_create(&tid, nullptr, threadrun, name);
tids.emplace_back(tid);
}
for(auto tid:tids)
{
pthread_join(tid, nullptr);
}
线程等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
、
主线程和新线程哪一个先运行?
[!IMPORTANT]
不确定!
我们期望谁最后退出?怎么能保证呢?
[!IMPORTANT]
主线程应该最后退出。理由是要和子进程一样,退出信息要被父进程接收。
保证主线程最后退出:用pthread_join函数来进行保证,如果主线程先完成部分代码,会等待新线程。
tid是什么呢?(pthread_t tid)
[!IMPORTANT]
是一个地址(虚拟地址)。
给用户提供的的线程ID,不是内核中的lwp,而是pthread库维护的一个唯一值。
线程退出
[!IMPORTANT]
线程退出有三种情况:1、正常退出(线程函数返回 或者 主线程返回)2、通过pthread_exit退出 3、线程被取消(pthread_cancel)
#include <pthread.h>
void pthread_exit(void *retval); // 线程退出
#include <pthread.h>
int pthread_cancel(pthread_t thread); // 线程取消
// pthread_cancel是在主线程内部调用函数。
#include <pthread.h>
int pthread_detach(pthread_t thread); // 线程分离
// 可以在线程函数中调用线程分离函数,也可以在主线程中调用线程分离函数。
线程退出
线程如何终止?
[!IMPORTANT]
1、新线程:线程函数return。
2、主线程:main函数结束,表示进程结束。
可不可以不join线程,让他执行完就退出呢?
[!IMPORTANT]
可以!
a、一个线程被创建,默认是joinable的,必须要被join的。
b、如果一个线程被分离,线程的工作状态->分离状态,不需要被join,也不能被join。依旧属于线程内部,但是不需要被等待。
库的理解
[!IMPORTANT]
创建线程,前提就是把库加载到内存,映射到进程的地址空间!
- 每个线程都有自己独立的栈,本质上就是线程在自己的tcb中维护了一段大小合适的栈结构。
- Linux线程 = pthread中线程的属性集+LWP
在新线程内部获取该线程的线程id
#include <pthread.h>
pthread_t pthread_self(void);
// 在线程的内部,获取线程id -> 也就是tid
怎么保证pthread中线程和LWP一一对应?
os要提供一个LWP相关的系统调用。pthread库就是对其进行封装。
线程互斥(互斥锁)
[!IMPORTANT]
共享资源:多个线程能够看到的资源。
我们需要对共享资源进行保护。
锁的接口
// 互斥锁的类型:pthread_mutex_t
// 锁是全局的或者静态的,只需要initializer即可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
// 锁是动态的
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
对临界资源保护的理解
[!IMPORTANT]
所谓的对临界资源进行保护,本质是对临界区代码及进行保护!—> 我们对所有资源进行访问,本质都是通过代码进行访问的 —> 保护资源,本质就是把访问资源的代码进行保护
原理角度理解锁
[!IMPORTANT]
如何理解申请锁成功,允许你进入临界区。—申请锁成功,pthread_mutex_lock()函数会返回。
如何理解申请锁失败,不允许你进入临界区。—申请所失败,pthread_mutex_lock()函数不返回,线程就是阻塞。线程会在每次时间
片中检查,临界资源是否仍被加锁,如果没有,则申请加锁,成功,就继续执行。
实现角度理解锁
[!IMPORTANT]
大多数的体系结构中,都提供了swap或者exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,只有一条指令,保证了原子性。
1、cpu的寄存器只有一套,被所用的线程共享,但是寄存器里面的数据属于执行流的上下文,属于执行流私有的数据。
2、cpu在执行代码的时候,一定也要有对应的执行载体—线程&&进程
3、数据在内存中,被所有线程是共享的。
结论:把数据从内存移动到cpu寄存器中,本质是把数据从共享变成线程私有!
饥饿问题:某些线程长时间在等待队列中等待资源
[!IMPORTANT]
解决方案:要二次申请时,必须排队。—具有一定的顺序性(同步)
抢票小程序
// ticket.cc
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <vector>
#include <mutex>
#include <memory>
#include "LockGuard.hpp"
int g_ticket_num = 10000;
int g_cnt = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *func(void *args)
{
while (g_ticket_num > 0)
{
LockGuard lockguard(&mtx);
if (g_ticket_num > 0)
{
std::cout << g_ticket_num << "票" << std::endl;
g_ticket_num--;
}
}
return nullptr;
}
int main()
{
std::vector<pthread_t> tids(10);
for (int i = 0; i