Linux——线程
文章目录
一、线程的概念
课本中对于线程的定义:线程是比进程更加轻量化的一种执行流 / 线程是在进程内部执行的一种执行流
我们对于线程的定义:线程是CPU调度的基本单位 / 进程是承担系统资源的基本实体
可以得出以下结论:
线程是进程内的一个执行流,我们之前学习的进程,其实是只有一个执行流的进程
进程其实可以存在有多个执行流,每一个执行流就是一个线程
线程也需要管理起来,每个执行流都有一个线程控制块,它们共同指向同一份进程地址空间,共用一份页表
不同的执行流拿着进程地址空间中代码区划分好的代码进行执行
二、线程的理解
线程的实现方法有很多种,Linux与Windows下实现的方法是不同的
一个进程可以拥有多个线程,而系统中会存在很多个进程,所以线程肯定也是需要被管理起来的
先描述,再组织
在Windows下:不仅有PCB(进程控制块),还单独设计了一套描述线程的结构体进行管理——TCB(线程控制块)
在Linux下:由于进程和线程的大部分的属性相同,而CPU在调度的时候不管是进程还是线程,都只看程序控制块执行指令,所以在Linux下不分进程与线程的概念,Linux下只有轻量级进程的概念,又因为学习者在课本中学习了进程与线程的概念,是区分进程与线程的,所以Linux提供了原生线程库,对轻量级进程进行了封装,提供了线程的一系列调用方法,实际的线程在用户空间中实现,但在内核中的实现只有轻量级进程,也就是进程与线程共用一套方法
线程的特点
1. 线程创建更简单,只需要建立线程控制块即可,进程地址空间与页表用进程创建好的
2. 线程在进程的地址空间中运行,其本质是执行进程地址空间的一部分代码,进程获得一个时间片,时间片也要被内部的线程进程瓜分,线程自己是不会获得时间片的,OS只会将时间片分给各个进程,线程分进程获得的时间片
线程切换为什么效率高?
1. 切换的寄存器少
2. 不需要重新更新cache
cache会将当前代码附近的代码进行加载,如果是同一个进程的线程,切换的时候就不用更新(局部性原理)
如何证明Linux下只有轻量级进程?
轻量级进程:LWP(Light Weight Processes)
三、进程地址空间的重新理解
虚拟地址空间的大小为4GB,有2^32个地址,按道理页表也应该有 2^32个条目,假设一个地址映射到物理地址,页表中一条需要10个字节,页表中不止有虚拟地址,物理地址,还有对应的权限与命中信息等等,那么将4GB的地址全部映射完需要40GB的大小,所以我们之前理解的页表肯定是浅显的,也是有问题的
那么实际上的页表长什么样呢?
进程地址空间的一个地址我们称为虚拟地址,有32个比特位
前面的学习我们知道,文件系统lO的基本单位大小为4KB—page size(也称为页帧)
为了适配磁盘中文件系统的设计,物理内存实际上也划分成了一个一个的数据页,称为页框
所以从磁盘加载到内存是以4KB为单位加载的
虚拟地址的32个比特位并不是以一个整体转化的,而是分成10、10、12三块二进制构成
页表也不止一张,分为页目录(指向页表的数组)、页表(指向对应页框起始地址的数组)
先拿着虚拟地址的高十位去查页目录,比如如果是0000000001就是第二个位置,映射到指定的页表,再通过中间10个比特位在页表中拿到物理内存中页框的起始地址,而一个页框的大小是4KB,有2^12字节,刚好对应虚拟地址的低12位比特位,就可以作为页内偏移量找到对应的位置
这样我们在使用的时候有可能只使用了几个页表,那么其他的页表就不会加载到内存,只有需要的时候才会创建,由此解决了内存不足的问题
四、线程控制
上文中提到,Linux下是没有线程与进程的概念的,只有轻量级进程的概念
所以同样也不会有创建线程的接口,但因为学习者是有线程与进程的概念的,Linux提供了线程的原生库
用户只关注线程,但是OS不提供线程的接口,只提供创建轻量级进程的接口,所以在用户和OS之间加了一个用户级线程库, 向上提供各种线程接口,向下把对线程的各种操作转化为对轻量级进程的各种操作
这个库在任何linux操作系统都默认存在
pthread_self()
——获取调用线程的tid
在线程内部,线程可以通过pthread_self函数来获取自己的tid来进行分离,取消,终止操作
4.1 线程创建
原生库提供创建线程函数:pthread_create()
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.// 链接的时候必须加上-lpthread
RETURN VALUE
On success, pthread_create() returns 0;
on error, it returns an error number, and the contents of *thread are undefined.
功能:创建一个新的线程
原型:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (start_routine)(void), void arg);
参数说明:
thread:线程id,返回型参数,返回创建线程的tid
attr:线程属性,直接设为nullptr
start_routine:函数指针,指向线程需要执行的函数
arg:这个参数会传递进start_routine的void参数中
注意:在链接的时候要注意link到系统给的原生线程库-lpthread
对于错误的检查:像我们以前的函数基本都是设置进全局变量errno来指示错误,而pthreads函数出错时不会设置全局变量errno,因为全部变量会被多个线程共享,它会将错误代码通过返回值返回
我们可以创建一个线程帮我们执行任务,但如果线程异常退出会怎么办?
可以发现,只要有一个线程异常退出了,整个进程都会被结束!
查看轻量级线程:
使用指令:
ps -aL
可以发现,这两个执行流的PID都相同,说明是同一个进程,而LWP不同,说明是不同的轻量级进程
细节:主线程的PID和LWP一样
在Linux下CPU在调度的时候用的就是LWP来作为标识符表示特定的执行流
当只有一个单进程的时候PID和LWP是等价的
tid又是什么呢?
可以发现,tid好像是一串地址?——下文中解释
通过线程的原理,不难发现同一个进程的所有执行流之间指向的虚拟地址空间都一样,意味着每个执行流都能看到相同的一份资源,无论全局变量还是函数,都是共享的
验证如下:
由此可见,线程之间的资源共享是很方便的,但这会引发资源安全相关的问题
4.2 线程等待
跟进程一样线程也是要等待的,如果不等待,就会造成内存泄漏(类似僵尸进程)
等待的目的:
获取新线程的退出信息,与返回值
回收线程对应的PCB等内核资源,防止内存泄漏
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_join() returns 0;
on error, it returns an error number.
功能:等待线程结束
原型:int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:要等待线程的tid
retval:线程执行函数的返回值,创建时传入需要执行的函数是一个参数为void*
返回值为void*
的函数,而retval是一个viod**
类型的二级指针,指向的就是返回值void*
可以看到,线程也是需要等待的
4.3 线程取消
取消线程的前提是线程在运行
#include <pthread.h>
int pthread_cancel(pthread_t thread);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_cancel() returns 0;
on error, it returns a nonzero error number.
功能:取消一个执行中的线程
原型:int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0,失败返回错误码
可以看到,线程是可以被取消的,但取消后需不需要进行等待呢?
可以发现,线程被取消后,依然需要等待,而等待到的返回值是-1
线程如果是被取消的,那么它的退出码是-1,它其实是一个宏:PTHREAD_CANCELED
4.4 线程终止
如果需要只终止某个线程,而不终止整个进程,可以有三种方法:
1. 从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit
2. 线程可以调用pthread_ exit来终止自己
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
线程函数return就代表该线程终止了
原生库中提供了pthread_ exit函数让线程终止自己
#include <pthread.h>
void pthread_exit(void *retval);
Compile and link with -pthread.
功能:线程终止
原型:void pthread_exit(void *value_ptr);
参数:value_ptr:value_ptr不要指向一个局部变量
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身),所以参数value_ptr不能指向一个局部变量,必须通过一个全局的数据来拿返回值
可以看到,线程被终止,仍然需要被等待
4.5 线程分离
默认情况下,新创建的线程是joinable的,线程退出后需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,需要join是一种负担,因为主线程需要一直阻塞等待,这个时候我们可以告诉系统,当线程退出时自动释放线程资源
#include <pthread.h>
int pthread_detach(pthread_t thread);
Compile and link with -pthread.
RETURN VALUE
On success, pthread_detach() returns 0;
on error, it returns an error number.
功能:线程分离
原型:int pthread_detach(pthread_t thread);
参数:thread:需要分离的线程的tid
返回值:成功返回0,失败返回错误码
可以看到,线程被分离后,是不能被join的,那能不能被取消呢?
线程如果是被分离的,该线程可以被取消,但是不能被join等待!
正常情况下,除了线程被分离,否则都需要join等待
五、对pthread原生库的深度理解
由于Linux下只有轻量级进程概念,以上关于线程的接口全部都不是系统直接提供的接口,而是原生线程库——pthread
提供的接口,从而向用户提供线程概念
5.1 系统调用问题
5.2 如何理解pthread库管理线程
5.3 站在语言角度理解pthread
C++11内部的多线程,本质就是对原生线程库的封装
线程中可以进行fork吗?
可以,也就是创建了一个新的子进程
线程中可以进行exec*程序替换吗?如何理解?
可以进行程序替换,但不推荐
一旦进行程序替换,该线程所属的原进程则整个被替换,这样做没有任何意义
不如线程中创建一个子进程,然后进行替换,让子进程去跑替换的内容
5.4 简单实现线程库封装
#pragma once
#include <iostream>
#include <pthread.h>
#include <functional>
using namespace std;
template <class T>
using func_t = function<void(T)>;
template <class T>
class Thread
{
// 传入该线程对应的Thread
static void *ThreadRoutine(void *args)
{
Thread *ts = static_cast<Thread *>(args);
//cout << "threadnamed: " << ts->_threadname << " " << "tid: " << ts->_tid << endl;
ts->_func(ts->_data);
return nullptr;
}
public:
Thread(string threadname, func_t<T> func, T data)
: _threadname(threadname), _func(func), _isrunning(false), _data(data)
{
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
if (n == 0)
{
_isrunning = true;
return true;
}
else
{
return false;
}
}
bool Join()
{
if (!_isrunning)
{
return true;
}
int n = pthread_join(_tid, nullptr);
if (n == 0)
{
_isrunning = false;
return true;
}
else
{
return false;
}
}
string ThreadName()
{
return _threadname;
}
bool IsRunning()
{
return _isrunning;
}
~Thread()
{
}
private:
pthread_t _tid;
string _threadname;
func_t<T> _func;
T _data;
bool _isrunning;
};