文章目录
一、Linux多线程
一级页表和二级页表都是key/val模型
,一级页表的key是第一份的10个比特位,则有210个key值,其val是第二份的10个比特位。第二份的10个比特位也就是二级页表的key,将一级页表和二级页表的key值组合起来就能找到要访问数据所在页的起始地址。第三份的12个比特位就是相对于页的起始地址的偏移量 (212 = 4096 刚好 是一页所占的字节数)
采取上面的方法来映射的话,只需要维护页表之间的关系即可,大概需要20多M的空间。
1. 线程概念
在Linux下,没有为线程创建新的内核数据结构,因为线程的很多操作是和进程相似的。但是在Windows下系统为线程创建了独立的内核数据结构,有真正意义上的线程。
每一个task_struct,可以被称之为线程,线程是在进程内部执行,也就是在进程的地址空间内运行。是操作系统调度的基本单位。对于CPU来说,不需要关心执行流是线程还是进程,他只关心PCB。创建一个线程后,不在创建地址空间、页表,只创建task_struct,指向父进程的地址空间,通过一定的手段,将当前进程的资源以一定的方式划分给不同的task_struct。
- 线程是在进程内部执行的,也就是说线程是在进程的地址空间内运行的,其是操作系统调度的基本单位。进程等于内核数据结构加上该进程对应的代码和数据,内核数据结构可能不止一个 PCB,进程是承担分配系统资源的基本实体,将资源分配给线程!
- 我们之前学习的是只有一个执行流的进程,而今天学习的是具有多个执行流的进程(task_struct 是进程内部的一个执行流),所以这两者是不冲突的。
- 在运行队列中排队的都是 task_struct,CPU 只能看到 task_struct,CPU 根本不关系当前调度的是进程还是线程,只关心 task_struct。所以,CPU 调度的基本单位是”线程”。Linux 下的线程是轻量级进程,没有真正意义上的线程结构,没有为线程专门设计内核数据结构,而是通过 PCB 来模拟实现出线程的。
- Linux 并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口!在用户层实现了一套多进程方案,以库的方式提供给用户进行使用,这个库就是 pthread 线程库(原生线程库)。
2. 线程创建
pthread_create
——创建线程
makefile
thread:thread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f thread
这里我们一定要加-lpthread
,告诉编译器我们要链接的原生线程库,否则就会产生链接错误。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* thread1_run(void* args)
{
while(1)
{
printf("我是线程1, 我正在运行\n");
sleep(1);
}
}
void* thread2_run(void* args)
{
while(1)
{
printf("我是线程2, 我正在运行\n");
sleep(1);
}
}
void* thread3_run(void* args)
{
while(1)
{
printf("我是线程3, 我正在运行\n");
sleep(1);
}
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, thread1_run, nullptr);
pthread_create(&t1, nullptr, thread2_run, nullptr);
pthread_create(&t1, nullptr, thread3_run, nullptr);
while(true)
{
printf("我是主线程, 我正在运行\n");
sleep(1);
}
return 0;
}
这里我们可以看到主线程的LWP
和进程的PID
是一样的。因为线程的资源都来自于进程,所以当我们将进程杀掉后,全部的执行流都会终止。因为杀掉进程就要回收进程的资源,所以线程也会全部终止。
被线程共享的进程资源:
线程独自占用的资源:
3. 线程和进程
线程的调度切换的成本是比进程调度切换的成本更低的。这是因为线程在进行切换时,地址空间和页表是不用换的。而进程进行切换时,需要将进程的上下文,进程地址空间、页表、PCB 等都要切换。CPU 内部是有 L1 ~ L3 的 Cache,CPU 执行指令时,会更具局部性原理将内存中的代码和数据预读到 CPU 的缓存中。如果是多线程,CPU 预读的代码和数据很大可能就会被所有的线程共享,那么进行线程切换时,下一个线程所需要的代码和数据很有可能已经被预读了,这样线程切换的成本就会更低!而进程具有独立性,进行进程切换时,CPU 的 Cache 缓存的代码和数据就会立即失效,需要将新进程的代码和数据重新加载到 Cache 中,所以进程切换的成本是更高的。
进程和线程的关系如下图:
4. 线程的优缺点
注:线程不是创建越多越好,因为线程切换也是有成本的,并不是不需要成本。创建线程太多了,线程切换的成本有可能就是最大的成本了。
线程的缺点
线程异常:
线程用途:
二、线程控制
clone函数
——创建线程或子进程
1. 线程创建
创建一批线程。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;
void* thread_run(void* args)
{
char* name = (char*)args;
while(true)
{
printf("new thread running, my thread name is: %s\n", name);
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tids[10];
for(int i = 0; i < 10; i++)
{
char tname[64];
snprintf(tname, 64, "thread-%d", i + 1);
pthread_create(tids + i, nullptr, thread_run, (void*)tname);
}
while (true)
{
cout << "main thread running" << endl;
sleep(1);
}
return 0;
}
这里为什么打印出来的线程名称都是10而不是从1到10呢?因为我们传的第四个参数是缓冲区的首元素的地址。因此每次传的都是相同的,所以为了解决这个问题,我们需要给每个线程单独开一个缓冲区。
3秒之后退掉主线程:
运行程序后我们可以发现,10个进程运行了3秒之后自动退出了。说明了主线程退出进程就会退出。进程退出后所有的资源都会释放掉,所以线程也就会退出。但是新的线程也会存在僵尸进程的问题,所以我们需要让主线程去等待并回收退出的线程。
2. 线程终止
如果我们需要终止某个线程,那么可以有一下三种方法:
注:在多线程场景下,不要使用 exit 函数,exit 函数是终止整个进程的!
pthread_exit
函数
void* thread_run(void* args)
{
char* name = (char*)args;
while(true)
{
printf("new thread running, my thread name is: %s\n", name);
sleep(1);
break;
}
delete name;
pthread_exit((void*)1);
}
int main()
{
pthread_t tids[10];
for(int i = 0; i < 10; i++)
{
// char tname[64];
char* tname = new char[64];
snprintf(tname, 64, "thread-%d", i + 1);
pthread_create(tids + i, nullptr, thread_run, (void*)tname);
}
void* ret = nullptr;
for(int i = 0; i < 10l; i++){
int n = pthread_join(tids[i], &ret);
if(n != 0) cerr << "pthread_join error" << endl;
cout << "thread quit: " << (uint64_t)ret << endl;
}
cout << "all thread quit" << endl;
return 0;
}
pthread_cancel
函数
void* thread_run(void* args)
{
char* name = (char*)args;
while(true)
{
printf("new thread running, my thread name is: %s\n", name);
sleep(1);
}
pthread_exit((void*)1);
}
int main()
{
pthread_t tid;
// 线程控制
pthread_create(&tid, nullptr, thread_run, (void*)"thread 1");
sleep(3);
pthread_cancel(tid); // 取消线程
void* ret = nullptr;
pthread_join(tid, &ret);
cout << "new thread exit: " << (int64_t)ret << endl;
return 0;
}
当一个线程被取消时,线程的退出结果是-1,(PTHREAD_CANCELED)。使用pthread_cancel函数的前提是线程已经跑起来了才能够取消,所以一个线程不能被创建后立马取消。一般情况下,都是用主线程来取消新线程的。如果使用新线程来取消主线程回影响整个进程。
3. 线程等待
线程在创建并执行的时候,也是需要被等待的。如果不等待线程会引起类似于进程的僵尸进程问题,进而导致内存泄漏。已经推出的线程其空间没有被释放,仍然在进程的地址空间内。创建的新线程不会复用刚才退出的线程的地址空间。
pthread_join
函数
- 如果thread线程的通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_cancel异常终止,retval所指向的单元里存放的是常数PTHREAD_CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,retval所指向的单元存放的是传给pthread_exit的参数。
- 如果不想得到线程的终止状态,可以传nullptr给retval参数。
thread线程的函数的返回值不会考虑异常的情况,如果线程出现了异常,那么整个进程都会奔溃掉。注意:状态寄存器是所有线程共享的。
线程库的引出
我们目前用的不是Linux自带的创建线程的接口,用的是pthread库中的接口!因为用户需要的是线程,但是Linux操作系统只能提供轻量级进程,无法完全表示线程,所以在用户和操作系统之间加了个软件层pthread库
。操作系统承担轻量级进程的调度和内核数据结构的管理,而线程库要给用户提供线程相关的属性字段。包括线程ID、栈的大小等等。
主线程使用的栈是进程地址空间的栈,而其余线程使用的栈就是都是在共享区中开辟的。
pthread_self
函数
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);
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
void *ret = nullptr;
pthread_join(tid, &ret);
cout << " new thread exit : " << (int64_t)ret << "quit thread: " << tid << endl;
return 0;
}
4. 线程分离
- 默认情况下,新创建的线程是
joinable
的,线程退出后,需要对其进行pthread_join
操作,否则无法释放资源,从而造成系统泄漏。 - 如果不关心线程的返回值,join 是一种负担。这个时候,我们可以告诉系统:当线程退出时,自动释放线程资源,这就是线程分离。
- 一般主线程时不退出的,当用户有个任务要处理,主线程就可以创建新线程来执行用户的任务,但主线程不关心任务处理的结果,那么就可以将该线程分离出去。
pthread_detach
函数
void *threadRun(void* args)
{
string name = (char*)args;
int cnt = 5;
while(cnt)
{
cout << name << " is running: " << cnt-- << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, threadRun, (void*)"thread 1");
pthread_detach(tid); // 线程分离
int n = pthread_join(tid, nullptr);
if(n != 0)
{
cout << "error : " << n << " : " << strerror(n) << endl;
}
sleep(5);
return 0;
}
如果我们在线程的自定义函数中将自己分离。发现并没有报错。这是因为当我们刚创建县线程时,有可能新的线程并没有跑,而是主线程继续向下执行,进入join后,新线程才把自己分离。所以在join后才进行分离线程就不会出现问题。
5. 线程局部存储
局部变量在每个线程中都是私有的。
void *threadRoutine(void* args)
{
string name = static_cast<const char*>(args);
int cnt = 5;
while(cnt)
{
cout << name << " cnt: " << cnt-- << ", &cnt: " << &cnt << endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3;
pthread_create(&t1, nullptr, threadRoutine, (void*)"thread 1"); // 线程被创建的时候,谁先执行不确定!
pthread_create(&t2, nullptr, threadRoutine, (void*)"thread 2");
pthread_create(&t3, nullptr, threadRoutine, (void*)"thread 3");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
return 0;
}
这里我们可以看到三个线程的cnt分别是不同的。说明局部变量被开辟到了不同的线程独立栈中了。
全局变量是所有线程共享的
当有多个线程对全局变量做修改时,地址是相同的。说明线程共享全局变量。
局部存储
全局变量前面加__thread
后,这里我们可以看到每个线程对应的地址是不一样的。说明了全局变量在每个线程中各自存在一份。修改后的全局变量在线程的局部存储当中,将原来的全局变量给了主线程以及新线程对应的线程局部存储都拷贝了一份。
三、线程封装
创建10个线程,分别从1加到指定数字:
enum{ OK=0, ERROR };
class ThreadDate
{
public:
ThreadDate(const string& name, int id, time_t createTime, int top)
:_name(name), _id(id), _createTime(createTime), _status(OK), _top(top), _result(0)
{}
~ThreadDate()
{}
// 输入
string _name;
int _id;
uint64_t _createTime;
// 返回
int _status;
int _top;
int _result;
};
void* thread_run(void* args)
{
ThreadDate* td = static_cast<ThreadDate*>(args);
for(int i = 1; i <= td->_top; i++)
{
td->_result += i;
}
cout << td->_name << " cal done!" << endl;
return td;
}
int main()
{
pthread_t tids[10];
for(int i = 0; i < 10; i++)
{
char tname[64];
snprintf(tname, 64, "thread-%d", i + 1);
ThreadDate* td = new ThreadDate(tname, i + 1, time(nullptr), 100 + 5*i);
pthread_create(tids + i, nullptr, thread_run, td);
sleep(1);
}
void* ret = nullptr;
for(int i = 0; i < 10; i++)
{
int n = pthread_join(tids[i], &ret);
if(n != 0) cerr << "pthread_join error" << endl;
ThreadDate* td = static_cast<ThreadDate*>(ret);
if(td->_status == OK)
{
cout << td->_name << " 计算的结果是: " << td->_result << " (它要计算的是[1, " << td->_top << "])" <<endl;
}
delete td;
}
return 0;
}