文章目录
- 一、线程是什么
-
- [1. 简介](#1. 简介)
- [2. 特点](#2. 特点)
- [3. 线程与进程对比](#3. 线程与进程对比)
- [4. 可重入函数](#4. 可重入函数)
- 二、线程控制
-
- [1. 创建线程](#1. 创建线程)
- [2. 线程终止](#2. 线程终止)
- [3. 线程等待](#3. 线程等待)
- [4. 线程分离](#4. 线程分离)
- [5. 线程id与线程地址空间的理解](#5. 线程id与线程地址空间的理解)
- [6. 简单的封装线程控制](#6. 简单的封装线程控制)
一、线程是什么
1. 简介
在一个程序里的一个执行流叫做线程,更准确的定义是"一个进程内部的控制序列"。
在CPU的视角内,没有进程,只有执行流(线程)!
之前我们学习的进程,本质只有一个线程,即只有一个执行流。实际上,一个进程可以有多个线程,至少有一个。
进程是资源分配的基本单位,而线程是操作系统调度和执行的基本单位!
进程 = 多个线程+虚拟地址空间+页表+代码和数据
既然一个进程可以有多个线程,那么势必要对线程进程描述组织,所以一定有相关的数据结构!Windows仿照进程控制块PCB,单独设计了一种线程控制块TCB;而Linux中,线程复用了进程的task_strcut数据结构进行描述!
所以,严谨来说,Windows才存在真正的线程;Linux中不存在真正意义上的线程,应该称之为"轻量级进程"!
Linux提供了轻量级进程相关的系统调用,可是我们用户只想用线程怎么办?于是有了pthread库------用户级线程库,为我们提供管理线程的接口和参数,向下调用Linux的轻量级进程系统调用。
pthread是一个第三方库,早期C库没有包含他,编译时需要我们显示链接-lpthread,现在新版本的C库可能已经包括他了。
为了方便叙述,本文中暂且认为:Linux轻量级进程==线程。
2. 特点
首先要知道的是:
一个进程的所有线程共享一份地址空间与页表!也就是共享进程资源!
由于这一点,创建新线程时不需要拷贝地址空间和页表,创建新进程。所以创建一个新线程的代价比创建一个新进程小的多 。
同时,因为进程切换需要丢弃cache,线程切换不需要切换cache。与进程切换相比,线程的切换需要操作系统做的工作少很多 。
在计算密集型应用中,为了能在多核多CPU机器上效率更高,可以将计算分解到多个线程中实现。
线程也有缺点:最主要的是缺乏保护,因在时间分配上的细微差别或者资源使用冲突的可能性是很大的。这就需要线程互斥和线程同步操作了。
当然,要记得所有线程都是属于同一个进程的。单独某个线程出现异常崩溃或发送信号等情况,影响的是整个进程所有线程,进程挂掉了所有线程也就都挂了。
3. 线程与进程对比
进程具有独立性,大部分资源是独占的。
一个进程的所有线程共享虚拟地址空间,也就共享大部分进程资源。
因为是同一个地址空间,所以代码段和全局数据段都是共享的。一个全局变量、全局函数,各进程都能访问。除此之外,各线程还共享:
- 文件描述符表
- 信号动作表
- 当前工作目录
- 进程pid和用户id
- 等等
但是,也有一些资源是每个线程单独拥有的:
- 线程tid
- 线程自己的上下文数据
- 函数栈帧
- 线程局部存储(后面讲)
- errno
- 信号屏蔽字
- 调度优先级
- 等等
4. 可重入函数
可重入函数是指可以被多个任务或执行流(如中断处理、多线程)同时安全调用的函数。
其核心在于:当函数正在执行时,另一个执行流(如中断或另一线程)同时再次进入该函数,不会产生数据错乱或逻辑错误。
这意味着,可重入函数,不能调用全局资源,如:
- 不能调用malloc或free
- 不能访问全局或静态变量
- 不能调用标准IO函数
这种函数就是不可重入函数。
反之,如果一个函数只访问自己的局部变量或参数,则称之为可重入函数。
在后续讲解线程互斥、线程同步时的线程安全问题,函数是否可重入是一个重要的问题。
二、线程控制
与线程有关的函数基本都属于pthread库,使用这些函数,需要包含头文件<pthread.h>,gcc编译时需要使用选项-lpthread
以下所有函数,返回值为int类型都表示:函数调用成功返回0,失败返回错误码。
命令ps -aL可以查到当前所有的轻量级进程。
1. 创建线程

参数:
- thread:输出型参数,记录创建的新线程id。
- attr:用来设置线程属性,我们用户不必关心,传递NULL即可。
- start_routine:是一个参数为
void*,返回值为void*的函数,是创建的新线程会去执行的函数。 - arg:传递给start_routine的参数。
2. 线程终止
如果要只终止一个线程而不终止整个进程,有三种方法:
-
线程函数内return
-
线程调用
pthread_exit终止自己

这个函数的参数相当于线程的返回值
-
线程调用
pthread_cancel可以终止同一进程中的任意一个进程(不建议使用)
需要注意的是,不论return还是pthread_exit,返回的指针指向的内存必须是全局的或是手动分配的。不能在线程函数的栈上分配,否则线程函数退出后这块内存会被自动释放了。
3. 线程等待
进程需要被等待,是因为他需要被父进程回收,防止内存泄露,获得退出信息,否则会导致子进程的僵尸问题。
线程等待的道理也是类似的,主线程需要回收子线程的空间,也可能需要获得新线程的执行结果。
除此之外,一个子线程的异常崩溃会杀死整个进程;而子进程的崩溃,父进程可以捕获并继续运行。
等待线程的函数:

参数:
- thread:等待的线程id
- retval:一个二级指针,它指向的指针会指向线程的返回值。如果不关心线程的终止信息,可以设为NULL
如果thread线程是被别的线程用pthread_cancel异常终止的,则retval指向的内容是常数PTHREAD_CANCELED,即-1。
4. 线程分离
默认情况下,新创建的线程是"joinable"的,这代表线程退出后必须对其进行线程等待操作,否则无法释放资源。
如果不关心线程的退出情况,不需要等待线程,我们可以将线程设置为"分离状态",让线程退出时系统自动回收释放线!
但是,一旦子线程被设置为分离状态,主线程就不能提前退出。
pthread_detach函数,用于分离一个线程:

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
5. 线程id与线程地址空间的理解
pthread库会给每个线程分配一个线程id,这个id是pthread库给每个线程定义的进程内唯一标识,此id的作用域是进程级而非内核级!
Linux系统中,pthread_t 类型的线程id,本质是一个地址,是一个进程地址空间上的地址!

如图,这是线程在进程地址空间的分布情况:
pthread动态库链接到内存共享区,内部会给每个线程开辟一部分区域,存放每个线程自己的数据。线程id,就是每个线程自己的内存区域的起始一个字节地址!
函数pthread_self,能返回当前线程的id:

如果一个全局变量用__thread修饰了,则各个线程会各自开辟一份空间,各自有一份·。互不干扰。这种就称之为线程局部存储!
要注意的是,只能用来局部存储内置类型数据。
除此之外,我们还可以在系统层面给线程自己设定名字,名字不能过长否则会设置失败

综合演示:
cpp
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
// 局部存储, 每个线程自己有独立的a变量
__thread int a = 10;
// 正常的全局变量,所有线程都能看到并共享
int b = 1;
void* routine(void* args)
{
pthread_t* id = static_cast<pthread_t*>(args);
std::string name = "thread" + std::to_string(b++);
pthread_setname_np(*id, name.c_str());
while (1)
{
std::cout << "新线程id: " << *id << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid1, tid2;
// 创建两个新线程,新线程调用routine函数
pthread_create(&tid1, nullptr, routine, (void*)&tid1);
pthread_create(&tid2, nullptr, routine, (void*)&tid2);
pthread_detach(tid2); // 分离tid2,系统会自动回收
pthread_join(tid1, nullptr); // 等待回收线程tid1
return 0;
}

打印线程信息中的LWP,可以认为是真正的"轻量级进程id",之前所说的pthread_t 类型的线程id本质就是虚拟空间中的一个地址。
有一个线程的LWP和进程pid相同,这个线程就是主线程。主线程的栈就在整个内存空间的栈上,其他子线程的栈在共享区。
6. 简单的封装线程控制
cpp
// Thread.hpp
#ifndef _THREAD_
#define _THREAD_
#include <iostream>
#include <pthread.h>
#include <unistd.h>
static int gnumber = 1;
// 想要让线程执行的任务类型,但是线程函数必须是void*(*)(void*)类型,
// 所以要在线程函数内再封装真正想要让线程完成的任务
using callback_t = void (*)();
class Thread
{
private:
// 类内普通成员函数会隐藏this参数,因此必须加static防止隐式传this
static void* Thread_Routine(void* args)
{
Thread* self = static_cast<Thread*>(args);
pthread_setname_np(self->_tid, self->_name.c_str());
self->_task(); // 执行任务
return nullptr;
}
public:
Thread(callback_t task) : _task(task), _tid(-1), _joinable(true), _result(nullptr)
{
_name = "NewThread-" + std::to_string(gnumber++);
}
void Start()
{
// 我们想要在Thread_Rontine函数类调用_task完成任务,可以把this自己传过去
pthread_create(&_tid, nullptr, Thread_Routine, this);
}
void Join()
{
if (_joinable)
{
pthread_join(_tid, &_result);
std::cout << "线程已回收" << std::endl;
}
else
{
std::cerr << "线程不可被等待" << std::endl;
}
}
void Detach()
{
if(_joinable)
{
pthread_detach(_tid);
_joinable = false;
}
else
{
std::cerr<<"线程已被分离" << std::endl;
}
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
callback_t _task;
void* _result;
bool _joinable;
};
#endif
cpp
#include "Thread.hpp"
void task1()
{
while (1)
{
std::cout << "这是task1" << std::endl;
sleep(1);
}
}
void task2()
{
while (1)
{
std::cout << "这是task2" << std::endl;
sleep(1);
}
}
int main()
{
Thread th1(task1);
Thread th2(task2);
th1.Start();
th2.Start();
th1.Join();
th2.Join();
return 0;
}

本篇完,感谢阅读。