线程是什么
概念
线程是一个执行分支,执行粒度比进程更细,调度成本更低(不用再对cache进行切换)。线程是进程内部的一个执行流。
线程是CPU调度的基本单位,进程是承担分配系统资源的基本实体。
理解
Linux操作系统的角度
OS有系统调用也是创建”子进程”,但是只会创建进程的PCB并不会创建地址空间和页表,也不在磁盘加载自己的代码和数据,这里创建的PCB就是线程,他们会指向父进程的地址空间。
在编写代码时可以划分为不同区域,让每个PCB执行同一个代码区中不同的代码
站在CPU调度的角度,CPU无法分辨所调度的是进程还是线程。CPU不需要区分,只需要负责给他什么就执行什么。使用线程后,CPU的调度成本会很低,因为从进程切换到当前进程的其他线程不需要切换进程的地址空间和页表,如果是进程切换到其他进程,不仅要切换地址空间和页表,还要切换CPU中cache的内容。切换的工作由OS做,CPU执行OS的代码,完成切换。
线程在进程的地址空间中运行,也就是这个线程属于进程。
那么什么叫做进程呢?
进程是承担分配系统资源的基本实体。 进程一定要包含很多执行流、地址空间、页表、进程的代码和数据。 PCB可以理解为执行流,而不是进程。
如何理解以前所学的进程?
内部只有一个task_struct/执行流/线程的进程
不是所有操作系统都会采用线程的方案。
OS要管理线程,还是那句话,先描述,再组织。
TCB:线程控制块,属于进程PCB
Windows就是怎么做的 —— 内核有真线程
Linux的线程直接复用进程的结构体,用PCB模拟线程的TCB,所以Linux没有真正意义上的线程,是用进程方案模拟的线程。
Linux复用代码和机构意味着实现更简单,好维护,效率更高,更安全,Linux可以不间断的运行。
OS使用最频繁的功能,除了OS本身,就是进程。
Linux所有调度执行流都被叫做轻量级进程,轻量级进程可能是一个独立的进程,也可能是一个线程。
补充知识
寄存器
可见的 不可见的
CPU内部
运算器、控制器、寄存器、MMU、硬件cache L1 L2 L3
局部性原理
现代计算机预加载数据的理论基础。允许我们提前加载正在访问数据的相邻或者附近的数据。
物理地址的寻址方式
物理内存和磁盘直接的读写是高频的工作,过多的IO,磁盘注定会有过多的寻址,就有过多的机械运动,意味着效率非常低下。OS再和磁盘这样的设备进行IO交互的时候,绝对不是按照字节为单位的而是要按照**块(4KB,8个扇区)**为单位,如果只想更改一个比特位,也必须IO 4KB
- 文件系统+编译器:文件(可执行程序和动态库)在磁盘的时候,就是以块4KB为单位存储的。
- 操作系统+内存:内存实际在进行内存管理的时候,也要以4KB为单位
内存管理的本质:将磁盘中的特定的4KB块(数据内容)放入到某一个物理内存的4KB的空间(数据保存的空间)。
内存中的的块被称为页,内存的空间被称为页框。
磁盘中的数据块被称为页帧。
通过预先加载要访问数据的附近的数据来减少未来的IO次数。
为什么是4KB呢
1、IO的基本单位(内核内存 + 文件系统)都要提供支持。 2、通过局部性原理,预测未来的命中情况,提高效率。
页表是软件,也要占据内存 。
页表是如何从虚拟地址到物理地址的?
虚拟地址不是整体被使用的,虚拟地址共32个比特位。虚拟地址是按10 + 10 + 12比特划分的。
先查找虚拟地址的前10个比特位,索引页目录找到指定的页表项,再从虚拟地址的后10个比特,索引页表项找到物理内存页框的起始位置,到页框以后再从虚拟地址的后12位找到页内起始地址,剩下的根据类型,从起始地址处再继续向后找
定位任意一个内存字节位置:页框 + 页内偏移 —— 基地址 + 偏移量
实际在进行malloc申请内存的时候,OS在虚拟内存上申请内存就行了,真正访问内存时,OS才会自动给你申请或填充页表和申请具体的物理内存(缺页中断)。
char *s = "hello world!";
*s = 'H'; //报错
我们都知道这样写会报错,为什么呢?
因为字符常量区是不允许被修改,只允许读取。指针指向的是一个字符串的虚拟起始地址,*s寻址的时候必定会伴随虚拟地址到物理地址的转换,利用MMU+查页表的方式,查询到物理地址后,会对操作进行权限的审查,发现虽然能找到,但是权限是只读R,进行的操作是写操作W,是非法的,MMU就会发生异常,OS识别到异常,将异常转换成信号,发送给目标进程,在从内核转换成用户态时,进行信号处理——终止进程。
线程特点
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现(加密解密,文件的压缩和解压等与算法有关的——CPU资源)
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。(下载、上传,IO主要消耗IO资源,磁盘的IO、网络带宽等)
线程的缺点
-
性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
-
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
-
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
-
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多。
多线程程序中,任何一个线程崩溃了,最后会导致进程崩溃,为什么呢?
系统角度:线程是进程的执行分支,线程崩了,就是进程崩了! 信号角度:页表转换的时候,MMU识别写入权限时,没有验证通过,MMU异常,OS识别异常,给进程发信号——信号是以进程为主的。
因为执行流看到的资源是通过地址空间看到的,多个LWP(线程)看到的是同一个地址空间,所以所有的线程可能会共享进程的大部分资源。
Linux进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID 一组寄存器 (重要) 栈 errno 信号屏蔽字 调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数) 当前工作目录 用户id和组id
线程操作
ps -aL #查看线程
用户视角:只认进程
用户级线程库:对下将Linux轻量级进程的相关接口进行封装,对上给用户提供进行线程控制的接口——pthread库,任何系统都要自带这样的库,Linux中称为原生线程库。
操作系统视角:Linux下没有真正意义上的线程,而是用进程模拟的线程(LWP)所以,Linux不会提供直接创建线程的系统调用,他会给我们最多提供创建轻量级进程的接口。
线程创建
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr
,void *(*start_routine) (void *), void *arg);
//pthread_t 线程id
//void *(*start_routine) (void *) 回调函数
//void *arg 要传给回调函数的数据
pthread_t tid;
pthread_create(&tid, nullptr, thread_run, con);
主线程退出程序就自动终止。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* Thread_Run(void* args)
{
const char* name = static_cast<const char*>(args);
while(true)
{
cout << "我是一个子线程,我的名字是 " << name << ", 我的线程id是 " << pthread_self() << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Thread_Run, (void*)"thread-1");
while(true)
{
cout << "我是主线程,新线程id是 " << tid << endl;
sleep(2);
}
return 0;
}
主线程的PID和LWP相等。
线程等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
//void **retval 输出型参数
//例
void* ret;
int n = pthread_join(tid, &ret);
一个新的进程被创建出来,需要被主线程等待,如果不进行等待,会出现类似僵尸进程的现象。
linux指针8字节,强转int
会丢失精度。强转成int64_t
就没有问题了。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* Thread_Run(void* args)
{
const char* name = static_cast<const char*>(args);
cout << "我是一个子线程,我的名字是 " << name << ", 我的线程id是 " << pthread_self() << endl;
sleep(1);
return (void*)1;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, Thread_Run, (void*)"thread-1");
void* res = nullptr;
int n = pthread_join(tid, &res);
if(n != 0)
{
cerr << "thread join error" << endl;
exit(2);
}
cout << "thread-1的返回值:" << (int64_t)res << endl;
return 0;
}
线程终止
-
线程函数执行完毕
exit()
是进程退出,不是线程退出,只要有任何一个线程调用exit()
,整个进程(所有线程) 会全部退出 。 -
pthread_exit(void*)
#include <pthread.h>
void pthread_exit(void *retval);
//void *retval 传给主线程的数据
线程创建、等待、终止的应用
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 5
enum STATE
{
OK,
ERROR
};
class Thread_content
{
public:
Thread_content(const int n, string str, time_t ti)
:_n(n)
,_name(str)
,_t(ti)
,_state(OK)
{}
public:
int _n;
string _name;
time_t _t;
STATE _state;
};
void* thread_run(void* args)
{
Thread_content* tc = static_cast<Thread_content*>(args);
cout << "i am a thread, my thread name is " << tc->_name << ", i am No." << tc->_n << ", create time: " << tc->_t << endl;
pthread_exit(tc);
}
int main()
{
pthread_t tid[NUM];
for(int i = 0; i < NUM; i++)
{
char str[64];
snprintf(str, 64, "thread-%d", i);
Thread_content* con = new Thread_content(i, str, time(nullptr));
pthread_create(tid + i, nullptr, thread_run, con);
sleep(1);
}
void* ret;
for(int i = 0; i < NUM; i++)
{
int n = pthread_join(tid[i], &ret);
if(n != 0)
cerr << "thread join error" << endl;
Thread_content* con = static_cast<Thread_content*> (ret);
if(con->_state == OK)
cout << con->_name << " finished!" << endl;;
delete con;
}
cout << "all thread quit..." << endl;
return 0;
}
//让每个进程分别计算一个数
#include <iostream>
#include <string>
#include <ctime>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define NUM 5
enum STATE
{
OK,
ERROR
};
class Thread_content
{
public:
Thread_content(const int n, string str, time_t ti, int top)
:_n(n)
,_name(str)
,_t(ti)
,_state(OK)
,_top(top)
,_result(0)
{}
public:
//输入的数据
int _n;
int _top;
string _name;
time_t _t;
//输出的数据
STATE _state;
int _result;
};
void* thread_run(void* args)
{
Thread_content* tc = static_cast<Thread_content*>(args);
for(int i = 1; i <= tc->_top; i++)
{
tc->_result += i;
}
cout << tc->_name <<" calculate done!" << endl;
pthread_exit(tc);
}
int main()
{
pthread_t tid[NUM];
//创建线程
for(int i = 0; i < NUM; i++)
{
char str[64];
snprintf(str, 64, "thread-%d", i);
Thread_content* con = new Thread_content(i, str, time(nullptr), 60 + i * 12);
pthread_create(tid + i, nullptr, thread_run, con);
sleep(1);
}
//线程等待
void* ret; //用于获取线程返回的数据
for(int i = 0; i < NUM; i++)
{
int n = pthread_join(tid[i], &ret);
if(n != 0)
cerr << "thread join error" << endl;
Thread_content* con = static_cast<Thread_content*> (ret);
if(con->_state == OK)
cout << "my thread name is " << con->_name << ", i am No." << con->_n << ", create time: " << con->_t << ", my calculate result is :" << con->_result << ",计算范围是:1-" << con->_top << endl;
delete con;
}
cout << "all thread quit..." << endl;
return 0;
}
线程取消
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//pthread_t thread 要取消线程的线程id
取消后的进程会返回-1(PTHREAD_CANCELED
)。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <ctime>
using namespace std;
void *threadRun(void* args)
{
const char*name = static_cast<const char *>(args);
int cnt = 5;
while(cnt)
{
cout << name << " is running: " << cnt-- << " obtain self id: " << pthread_self() << endl;
sleep(1);
}
pthread_exit((void*)11);
// PTHREAD_CANCELED; #define PTHREAD_CANCELED ((void *) -1)
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
sleep(3);
pthread_cancel(tid);
void *ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit : " << (int64_t)ret << " quit thread: " << tid << endl;
return 0;
}
获取自己的线程id
#include <pthread.h>
pthread_t pthread_self(void);
线程分离
一个线程如果被分离,就无法再被join,如果join函数就会报错
在join时会检查对应线程的属性是否是joinable,如果先join再分离就无法检测到。
#include <pthread.h>
int pthread_detach(pthread_t thread);
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;
void* ThreadRoutine(void* args)
{
const char* name = static_cast<const char*>(args);
int count = 5;
while(count)
{
cout << "我是" << name << ", 我还有" << count-- << "秒!" << endl;
sleep(1);
}
return (void*) 1;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
pthread_detach(tid);
int n = pthread_join(tid, nullptr);
if(n != 0)
{
cerr << "errno: " << n << " errstr: " << strerror(n) << endl;
}
sleep(10);
return 0;
}
分离后join直接报错
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;
//可重入函数
void* ThreadRoutine(void* args)
{
pthread_detach(pthread_self());
const char* name = static_cast<const char*>(args);
int count = 5;
while(count)
{
cout << "我是" << name << ", 我还有" << count-- << "秒!" << endl;
sleep(1);
}
return (void*) 1;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
int n = pthread_join(tid, nullptr);
if(n != 0)
{
cerr << "errno: " << n << " errstr: " << strerror(n) << endl;
}
sleep(8);
return 0;
}
如果让线程自己分离自己,可以发现并没有报错,这是因为线程运行顺序是不确定的,主线程可能在子线程运行之前就已经完成join了,这时还没有运行到分离。
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread-1");
sleep(1);
int n = pthread_join(tid, nullptr);
if(n != 0)
{
cerr << "errno: " << n << " errstr: " << strerror(n) << endl;
}
sleep(8);
return 0;
}
在main()函数中加一句sleep(1)就可以正常报错了。
理解线程库
进程中的线程,可以随时访问库中的代码和数据!
线程id:pthread_t就是一个地址数据,用来标识线程相关属性集合的起始地址。
所有线程都要有自己独立的栈结构,主线程用的是进程系统栈,新线程用的是库中提供的栈
封装Thread类
//Thread.h
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
class Thread
{
typedef void (*FUNC_t)(void *);
enum Thread_STATUS
{
NEW,
RUNNING,
EXITED
};
public:
Thread(int num, FUNC_t func, void* args)
:_args(args)
,_status(NEW)
,_func(func)
{
char name[64];
snprintf(name, 64, "Thread-%d", num);
_name = name;
}
~Thread()
{}
pthread_t threadid()
{
if(_status == RUNNING)
return _tid;
else
return 0;
}
std::string threadname()
{
return _name;
}
Thread_STATUS status()
{
return _status;
}
static void* FuncHelper(void* args)
{
Thread* td = static_cast<Thread*>(args);
td->_func(td->_args);
//(*td) ();
}
void operator() ()
{
_func(_args);
}
void run()
{
int n = pthread_create(&_tid, nullptr, FuncHelper, this);
if(n != 0)
{
exit(1);
}
_status = RUNNING;
}
void join()
{
int n = pthread_join(_tid, nullptr);
if(n != 0)
{
std::cerr << "thread join error" << std::endl;
return;
}
_status = EXITED;
}
private:
pthread_t _tid;
std::string _name;
void* _args;
Thread_STATUS _status;
FUNC_t _func;
};
测试
#include "Thread.h"
#include <iostream>
#include <string>
#include <unistd.h>
using namespace std;
void ThreadRoutine(void* args)
{
string message = static_cast<const char*>(args);
int count = 5;
while(count)
{
cout << message << " , " << count-- << endl;
sleep(1);
}
}
int main()
{
Thread td1(1, ThreadRoutine, (void*)"我是一个线程1");
Thread td2(2, ThreadRoutine, (void*)"我是一个线程2");
td1.run();
td2.run();
cout << "name: " << td1.threadname() << ", tid: " << td1.threadid() << ", status: " << td1.status() << endl;
cout << "name: " << td2.threadname() << ", tid: " << td2.threadid() << ", status: " << td2.status() << endl;
td1.join();
td2.join();
return 0;
}