深入理解Linux线程的基本的操作
引言
在程序的内部一个执行的路线叫做线程。更准确的定义叫做执行队列。用一个比较通俗易懂的理解,多进程就像是一个项目组有很多的人,每个人执行的分工。多线程就像是一个人同时做很多份工作。
- 一个进程至少含有一个线程。
- 透过进程虚拟地址空间,可以看到进程的⼤部分资源,将进程资源合理分配给每个执⾏流,就形
成了线程执⾏流
线程的优缺点分析
线程的优点
- 创建一个线程的代价比较小
- 与进程相比,线程切换的代价也比较小
- 线程的切换,虚拟空间仍然是相同的,进程的切换涉及到虚拟空间的切换,这两种本质的区别是,进程的切换涉及到大量的寄存器中内容的维护(寄存器中内容的换出和换入)。
- 还有一个上下文切换回扰乱处理器缓存,一个显著的区别就是切换进程改变虚拟内存的时候,页表缓冲TLB,全部失效,需要全部刷新,频繁的切换会导致效率严重的削弱。
- 能够充分利用多处理器可并行的数量
线程的缺点
-
资源消耗
- 每个线程都需要占用内存和CPU资源,线程过多可能导致系统资源耗尽。
-
复杂性
- 多线程编程容易引入竞争条件、死锁等问题,增加了调试和维护的难度。
-
同步开销
- 线程间共享数据时,需要使用锁、信号量等机制,增加了额外开销,可能降低性能。
-
调试困难
- 多线程程序的非确定性行为使得问题难以复现和调试。
-
可移植性差
- 不同操作系统对线程的支持和实现不同,可能导致跨平台问题。
-
上下文切换开销
- 线程切换需要保存和恢复上下文,频繁切换会降低效率。
-
线程安全问题
- 共享数据的线程可能因未正确同步而导致数据不一致。
-
难以扩展
- 随着线程数量增加,管理和协调的复杂性显著上升。
-
优先级反转
- 低优先级线程持有高优先级线程所需的资源时,可能导致高优先级线程被阻塞。
-
缺乏隔离性
- 一个线程的错误可能影响整个进程,导致程序崩溃。
线程异常
线程本身属于进程的一部分,当一个线程出现问题被操作系统kill掉了之后,整个程序也会崩溃,这可造成严重的后果。
Linux线程创建的规则
创建一个新的线程
- thread: 返回创建线程的线程ID
- attr: nullptr表示默认
- start_routine: 函数指针,表示创建的新的线程会执行这个函数
- args: start_routine的参数
获取线程ID
这个函数的作用就是获取我们当前执行流的所在的线程ID。
线程终止
这个函数的作用类似于return,或者说和return几乎一致。void* retval表示我们想要返回的参数,注意这里的这个参数必须是全局的,可以通过这样的设计模式,我们全局的定义一个vector的数据,其中就是保存我们线程的返回值。但是我个人认为多线程使用返回值传递参数只会徒增烦恼,我们可以直接在传递参数就传递返回参数的引用或者指针。
强制终止
这个函数的作用是强制性停止一个线程,但是这样做显然是不安全的,可能导致很多数据出现错误。
线程等待
由于操作系统是通过c语言进行编写的,很多时候很多的结构需要我们手动进行释放空间,就像进程的wait一样。
这里提供的pthread_join接口的作用就是帮助释放一个线程的空间,thread_t表示线程的唯一标识符tid,retval用来接收返回值。
基于Linux对实现std::thread的封装
#include <pthread.h>
#include <unistd.h>
#include <functional>
#include <utility>
#include <memory>
#include <tuple>
namespace ThreadModule {
enum class ThreadStatus {
Running,
Stopped,
Detached,
};
class pthread {
using func_t = std::function<void()>;
public:
template <class _Func, class... Args>
pthread(_Func&& func, Args&&... args)
: _status(ThreadStatus::Stopped),
_joinable(true),
_tid(::pthread_self()),
_pid(getpid()),
_task(std::make_unique<Task>(
std::bind(std::forward<_Func>(func), std::forward<Args>(args)...))) {
// 启动线程
if (::pthread_create(&_tid, nullptr, runtine, _task.get()) != 0) {
throw std::runtime_error("Failed to create thread");
}
_status = ThreadStatus::Running;
}
~pthread() {
if (_joinable) {
::pthread_detach(_tid);
}
}
// 禁用拷贝
pthread(const pthread&) = delete;
pthread& operator=(const pthread&) = delete;
// 启用移动
pthread(pthread&& other) noexcept
: _status(other._status),
_joinable(other._joinable),
_tid(other._tid),
_pid(other._pid),
_task(std::move(other._task)) {
other._joinable = false;
}
pthread& operator=(pthread&& other) noexcept {
if (this != &other) {
if (_joinable) {
::pthread_detach(_tid);
}
_status = other._status;
_joinable = other._joinable;
_tid = other._tid;
_pid = other._pid;
_task = std::move(other._task);
other._joinable = false;
}
return *this;
}
void join() {
if (_joinable) {
::pthread_join(_tid, nullptr);
_joinable = false;
_status = ThreadStatus::Stopped;
}
}
bool is_join() { return _joinable; }
void detach() {
if (_joinable) {
::pthread_detach(_tid);
_joinable = false;
_status = ThreadStatus::Detached;
}
}
bool is_joinable() const { return _joinable; }
private:
struct Task {
explicit Task(func_t func) : _func(std::move(func)) {}
void run() { _func(); }
func_t _func;
};
static void* runtine(void* args) {
Task* task = static_cast<Task*>(args);
task->run();
return nullptr;
}
ThreadStatus _status;
bool _joinable;
pthread_t _tid;
pid_t _pid;
std::unique_ptr<Task> _task;
};
}
这里实现的方式和c++11线程库实现的机理类似,我们在构造的时候,就会启动线程,并且提供了join的接口。
目前没有完善的点 : 这里我封装的thread没有实现的是返回值的问题,但是可以接受任何参数函数的传递。但是我的理解是在线程中,其实没有必要利用返回值,反而会提高代码的难度。
线程互斥
引言
为了理解线程互斥的问题,我们首先应该认识一下临界区的概念年
临界区
临界区是指程序中访问共享资源(如共享设备或共享存储器)的那段代码。共享
资源一次只能被一个进程使用,因此当一个进程进入临界区时,其他进程必须待
这是一个什么样的概念呢,就是说一个项目我们很多人同时去做,但是有很多人都需要使用打印机,但是只有一个打印机,这里的人就是多个线程,打印机就是临界区,我们必须控制好线程对临界区的访问,不然就会出现意想不到的问题。
互斥
互斥保持的就是任何的时候只能有一个线程进行访问
锁的概念
既然我们访问临界区的时候只能同时允许一个线程进行访问,这个时候就需要锁,所得作用就是一个线程在访问临界区的时候将临界区锁起来不让其他的线程进行访问
锁的原子性
锁只有两种状态,被一个执行流(线程)获取,没有被一个执行流获取,换句话说锁没有中间态,这保证了多线程访问时候的安全性,因为临界区的不安全性就是来源于多个执行流访问中间态出现问题。
Linux中的锁的实现
lock:
movb $0, %al //把al清零
xchgb %al, mutex //交换al和mutex的值
if (al寄存器的内容 > 0)
return 0;
else
挂起等待
goto lock;
unlock:
movb $1, mutex
唤醒等待mutex的线程;
return 0;
从中我们可以发现加锁和解锁是原子的,即不同的执行流同时进行访问的时候,之后lock success 和 lock fail两种情况。
Linux中的锁
这里提供了Linux中锁的相关的接口
c++对锁(pthread_mutex_t)进行封装
#pragma once
#include <iostream>
#include <pthread.h>
namespace MutexModule
{
class Mutex{
public:
Mutex() { ::pthread_mutex_init(&_mtx,nullptr); }
void Lock() { ::pthread_mutex_lock(&_mtx); }
void Unlock() { ::pthread_mutex_unlock(&_mtx); }
~Mutex() { ::pthread_mutex_destroy(&_mtx); }
pthread_mutex_t * MutexPtr() { return &_mtx; }
private:
pthread_mutex_t _mtx;
};
class LockGuard{
public:
LockGuard(Mutex& lock):_lock(lock){
_lock.Lock();
}
~LockGuard(){
_lock.Unlock();
}
private:
Mutex& _lock;
};
}
namespace StandardModule
{
using mutex = MutexModule::Mutex;
}
这里我还是解释一下LockGuard的作用,LockGuard是利用的RAII的思想(标准库中叫做std::lockguard,std::unique_lock),这种思想在智能指针中引用广泛,或者说这就是智能指针的核心思想
RAII的思想
RAII(Resource Acquisition Is Initialization,资源获取即初始化)是C++中的一种编程思想,主要用于管理资源的生命周期,确保资源在不再需要时能够被正确释放。RAII的核心思想是将资源的获取与对象的初始化绑定在一起,利用对象的生命周期来自动管理资源。