Linux系统收官篇:线程学习的一些心得总结

目录

一、线程

1、概念

(1)页表

(2)页目录

(3)缺页异常

2、特点

3、异常

4、线程控制

(1)POSIX线程库

(2)创建线程

(3)线程终止

(4)线程等待

(5)线程分离

5、线程ID

6、线程封装

(1)构造函数

(2)线程运行

(3)线程终止

(4)线程等待

(5)线程标志位

(6)线程入口函数

(7)线程分离

(8)测试

7、线程互斥

(1)相关概念

(2)互斥量

(3)互斥锁封装

[<1> 构造函数](#<1> 构造函数)

[<2> lock](#<2> lock)

[<3> unlock](#<3> unlock)

[<4> 析构函数](#<4> 析构函数)

[<5> lockguard](#<5> lockguard)

8、线程同步

(1)同步与竞态

(2)条件变量

[<1> pthread_cond_init](#<1> pthread_cond_init)

[<2> pthread_cond_destroy](#<2> pthread_cond_destroy)

[<3> pthread_cond_wait](#<3> pthread_cond_wait)

[<4> 唤醒等待](#<4> 唤醒等待)

[<5> 条件变量封装](#<5> 条件变量封装)

(3)生产者消费者模型

9、POSIX信号量

10、信号量封装

11、基于环形队列的生产消费模型

12、日志

13、线程池

(1)概念

(2)应用场景

​编辑

(3)单例模式

[<1> 饿汉实现方式](#<1> 饿汉实现方式)

[<2> 懒汉实现方式](#<2> 懒汉实现方式)

(4)实现

Perface:

线程是Linux系统编程的一个核心模块,多线程允许多个执行流并发执行,以此来显著提升程序的吞吐量与响应速度,这中间涉及到线程的创建、同步及销毁等诸多底层细节,线程作为进程的轻量级替代版本,具有更低的上下文切换开销和更高效的通信效率,使其成为高并发应用开发不可或缺的组成部分,当然线程在提供强大并发能力的同时,也引入了同步、数据竞争等需要谨慎应对的问题。本文将以理解线程概念为出发点,在此基础上进一步实现对线程的控制、同步和互斥等相关操作,线程处于Linux系统编程中性能与复杂性的交汇点,也是Linux系统学习路线的最后一个重要的模块,值得深入学习一番。

一、线程

1、概念

想要真正理解好线程这一概念,需要先从Linux内核的分页式存储谈起。

Linux内核将物理内存按照一个固定长度的页框进行分割,也叫物理页,每个页框包含一个物理页。一个页的大小等于页框的大小。大多数32位的体系结构支持4KB的页,64位体系结构一般支持8KB的页,需要注意的是,页框是一个存储区域,而页是一个数据块,可以存放在任何页框或磁盘中。

有了这种机制,CPU便并非是直接访问物理内存地址,而是通过虚拟地址空间来间接访问物理内存地址。虚拟地址空间是操作系统为每一个正在执行的进程分配的一个逻辑地址,在32位上,其范围为0~4G-1。

操作系统通过页表将虚拟地址空间和物理内存地址之间建立映射关系,页表上记录了每一对页和页框的映射关系,能让CPU间接访问物理内存地址。

总结一下,其思想就是将虚拟内存下的逻辑地址空间分为若干页,将物理内存空间分为若干页框,通过页表便能把连续的虚拟内存,映射到若干个不连续的物理内存页,这样就解决了使用连续的物理内存造成的碎片问题。

(1)页表

页表中的每一个表项,都指向一个物理页的开始地址,在32位系统中,虚拟内存的最大空间是4GB

这是每一个用户程序都拥有的虚拟内存空间。既然需要让4GB的虚拟内存空间全部可用,那么页表中就需要能够表示这所有的4GB空间,一共需要4GB/4KB=1048576个表项。

把页表看成普通的文件,对其进行离散分配,即对页表再分页,由此形成多级页表的思想。

(2)页目录

每一个页框都被一个页表中的一个表项指向,这1024个页表也需要被管理起来,管理页表的表称

之为页目录表,形成二级页表,如下图所示:

所有页表的物理地址被页目录表项指向。

页目录的物理地址被CR3寄存器指向,这个寄存器中,保存了当前正在执行任务的页目录地址。

因此操作系统在加载用户程序时,不仅仅需要为程序内容来分配物理内存,还需要为用来保存程序的页目录和页表分配物理内存。

MMU进行两次页表查询确定物理地址,在确认了权限等问题后,MMU再将这个物理地址发送到总线,内存收到之后开始读取对应地址的数据并返回。可知当页表变为N级时,就变成了N次检索+1次读写。可见,页表级数越多查询的步骤越多,对于CPU来说等待时间越长,效率越低。

单级页表对连续内存要求高,于是引入了多级页表,但多级页表也是一把双刃剑,在减少连续存储要求且减少存储空间的同时也降低了查询效率。

为了提高多级页表的查询效率,MMU引入了快表TLB,也就是缓存,当CPU给MMU传新虚拟地址之后,MMU先去问TLB那边有没有,如果有就直接拿到物理地址发到总线给内存。但TLB容量比较小,难免发生Cache Miss,这时MMU就在页表中查询,找到之后MMU除了把地址发到总线传给内存,还把这条映射关系给到TLB,让它记录一下刷新缓存。

(3)缺页异常

若CPU给MMU的虚拟地址,在TLB和页表都没有找到对应的物理页,这就是所谓的缺页异常,该异常是一个由硬件中断触发的可以由软件逻辑纠正的错误。

如果目标内存页在物理内存中没有对应的物理页或者存在但无对应权限,CPU就无法获取数据,这时CPU就会报告一个缺页错误。

由于CPU没有数据就无法进行计算,CPU罢工了用户进程也就出现了缺页中断,进程会从用户态切换到内核态,并将缺页中断交给交给内核的Page Fault Handler处理。

2、特点

特点:

进程间具有独立性,线程间具有共享性,线程共享地址空间,也就共享进程资源。

进程是资源分配的基本单位,线程是调度的基本单位。

线程共享进程数据,但也拥有自己独立的一部分数据:

线程ID,一组寄存器、线程的上下文数据、栈、errno、信号屏蔽字、调度优先级

优点:

创建一个新线程的代价要比创建一个新进程小得多。

与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少。

最主要的区别是线程的切换虚拟内存空间依然是相同的,但进程切换是不同的,这两种上下文切换的处理都是通过操作系统内核来完成的,内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。

线程占用的资源要比进程少

能充分利用多处理器的可并行数量

在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

缺点:

性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器,如果计算密集型线程的数量比可用处理器多,那么可能会有较大的性能损失。

健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

进程的多个线程共享:

同一地址空间,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中也都可以访问到,除此之外,各线程还共享以下进程资源和环境:

文件描述符表

各种信号的处理方式

当前工作目录

用户id和组id

3、异常

如果单个线程出现除零、野指针问题导致线程崩溃,进程也会随着崩溃。

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,进程终止,该进程内的所有线程也就随即退出。

4、线程控制

(1)POSIX线程库

与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头的。

要使用这些函数库,需要引入头文件<pthread.h>。

链接这些线程函数库时需要使用编译器命令的"-lpthread"选项。

(2)创建线程

pthread_create用于创建一个新的线程

参数:

thread:为输出型参数,返回线程ID

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:为函数地址,线程启动后要执行的函数

arg:传给线程启动函数的参数

返回值:成功返回0,失败返回错误码。

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<pthread.h>
#include<thread>
using namespace std;
void* pthreadfunc(void* args)
{
    string name=(const char*)args;
    while(true)
    {
        sleep(1);
        cout<<"我是新线程:name:"<<name<<",pid:"<<getpid()<<endl;
    }
    return nullptr;
}
int main()
{
    pthread_t thread;
    int ret=pthread_create(&thread,nullptr,pthreadfunc,(void*)"thread-1");
    if(ret!=0)
    {
        cerr<<"线程创建失败"<<endl;
        return 1;
    }
    while(1)
    {
        cout<<"我是主线程 "<<"pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

pthread_create(&thread,nullptr,pthreadfunc,(void*)"thread-1"),通过pthread_create创建一个新线程,通过while循环每秒输出一次线程名和进程ID,主线程同样通过while循环每秒输出一次主线程标识和进程ID,两个线程并发运行,都会持续输出信息,运行结果如下所示:

pthread_self用于获取线程ID,pthread_self返回一个pthread_t类型的变量,指定的是调用pthread_self函数的线程的ID,这个ID是pthread库给每个线程定义的进程内唯一标识,是pthread

库维持的。

由于每个进程有自己独立的内存空间,故此ID的作用域是进程级而非系统级。

pthread库也是通过内核提供的系统调用来创建线程的,而内核会为每个线程创建系统全局唯一的ID来唯一标识这个线程。

可使用ps命令查看线程信息

bash 复制代码
ps -aL | head -1 && ps -aL | grep pthread

结果如下所示:

LWP得到的是真正的线程ID,pthread_self得到的这个数实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。

可以看到,在ps -aL得到的线程ID,有一个线程ID 905012和进程ID 905012相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈则是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供,而pthread库在共享区,所以除了主线程之外的其他线程的栈都在共享区。

(3)线程终止

如果需要只终止某个线程而不终止整个进程,有以下三种方法:

1、从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

2、线程可以调用pthread_exit终止自己。

3、一个线程可以调用pthread_cancel终止同一进程中的另一个线程。

pthread_exit用于线程的终止,参数retval不能指向一个局部变量,pthread_exit无返回值,跟进程一样,线程结束时无法返回到它的调用者(自身)。需要注意,pthread_exit或return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。

pthread_cancel用于取消一个执行中的线程,thread为线程ID,调用成功返回0,失败返回相应的错误码。

(4)线程等待

为什么需要线程等待?

已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。

创建新的线程不会复用刚才退出线程的地址空间。

pthread_join用于等待线程结束,thread为线程ID,retval指向一个指针,后者指向线程的返回值,调用成功返回0,失败返回相应的错误码。

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的。

(5)线程分离

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。

如果不关心线程的返回值,join是一种负担,这个时候,可以告诉系统,当线程退出时,自动释放线程资源。

可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:

cpp 复制代码
pthread_detach(pthread_self());

joinable和分离是冲突的,一个线程不能既是joinable又是分离的。

cpp 复制代码
#include<iostream>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<pthread.h>
#include<thread>
using namespace std;
void* threadrun(void*args)
{
    cout<<(char*)args<<endl;
    return nullptr;
}
int main()
{
    pthread_t t;
    const char* msg="hello thread";
    pthread_create(&t,nullptr,threadrun,(void*)msg);
    int n=pthread_join(t,nullptr);
    if(n==0)
    {
        cout<<"wait success"<<endl;
    }
    else
    {
        cout<<"wait failed"<<endl;
    }
    return 0;
}

pthread_create(&t,nullptr,threadrun,(void*)msg),调用pthread_create创建一个新线程,将字符串msg作为参数传递给线程执行函数threadfun,线程函数收到参数后,将其转换为char指针类型并输出,主线程通过pthread_join等待子线程执行完毕,运行结果如下所示:

5、线程ID

pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,pthread_self函数可以用于获取线程自身的ID。

cpp 复制代码
pthread_t pthread_self(void);

pthread_t类型的线程ID,在Linux中,本质是一个进程地址空间上的一个地址,如下图所示:

6、线程封装

下面简单实现一个C++线程封装类,类内部封装了POSIX线程库的pthread_t线程ID,并提供了启动、分离、等待和停止线程的成员函数,使用模板参数K来支持传递任意类型的参数给线程函数。

(1)构造函数

cpp 复制代码
#ifndef _THREAD_H_
#define _THREAD_H_
#include<iostream>
#include<string>
#include<pthread.h>
#include<cstdio>
#include<cstring>
#include<functional>
#include<cstdint>
#include<cerrno>
using namespace std;
namespace threadmodule
{
    static uint32_t num=1;
    template<class K>
    class thread
    {
    using func_t=function<void(K)>;
    private:
    public:
        thread(func_t fun,K data)
        :_id(0)
        ,_detach(false)
        ,_running(false)
        ,_res(nullptr)
        ,_fun(fun)
        ,_data(data)
        {
            _name="thread-"+to_string(num++);
        }
    private:
        pthread_t _id;
        string _name;
        bool _detach;
        bool _running;
        void* _res;
        func_t _fun;
        K _data;
    };
}
#endif

构造函数thread用于初始化线程成员对象,_id用于存储系统线程ID,_detach用于标记线程是否已被分离,_running用于标记线程是否正在运行,_res用于存储线程的返回值,_fun用于存储用户提供的线程执行函数,_data用于存储传递给线程函数的参数,_name用于存储线程名,需要注意的是,构造函数thread并未真正创建系统线程,只是对线程对象进行了初始化。

(2)线程运行

cpp 复制代码
bool start()
{
   if(_running) return false;
   int n=pthread_create(&_id,nullptr,func,this);
   if(n!=0)
   {
      cerr<<strerror(n)<<endl;
      return false;
   }
   else
   {
      cout<<_name<<"创建成功"<<endl;
      return true;
   }
}

start函数用于创建并启动线程,先对_running标志位进行判断,如果线程已在运行则返回false启动失败,调用pthread_create系统函数创建线程,传入静态函数func作为线程入口,并将this指针作为参数传递给该静态函数,从而完成线程的启动。

(3)线程终止

cpp 复制代码
bool stop()
{
  if(_running)
  {
    int n=pthread_cancel(_id);
    if(n!=0)
    {
      cerr<<strerror(n)<<endl;
      return false;
    }
    else
    {
      _running=false;
      cout<<_name<<"stop"<<endl;
      return true;
    }
  }
  return false;
}

step函数用于终止正在运行的线程,首先对_running标志位进行判断,如果线程正在运行,则调用pthread_cancel函数向该线程发送取消请求,取消成功则将_running标志位设为false,return true。

(4)线程等待

cpp 复制代码
void join()
{
  if(_detach) {return;}
  int n=pthread_join(_id,&_res);
  if(n!=0)
  {
    cerr<<strerror(n)<<endl;
  }
}

join函数用于等待线程结束并回收其资源,函数首先检查_detach标志,如果线程已被分离,则直接返回,若线程未分离,则调用pthread_join系统函数,传入线程ID _id和返回值指针&_res,阻塞等待该线程退出,pthread_join会将线程的返回值存储到_res中,需要注意的是,join只能对未分离且正在运行的线程调用,且一个线程只能被join一次。

(5)线程标志位

cpp 复制代码
void enabledetach()
{
  _detach=true;
}
void enablerunning()
{
  _running=true;
}

enabledetach、enablerunning都是私有成员函数,设置为私有函数是为了防止外部直接修改线程内部状态,enabledetach、enablerunning用于修改线程对象的状态标志,enabledetach函数将_detach标志设置为true,表示线程已被标记为分离状态,enablerunning函数将_running标志设置为true,表示线程已开始运行,当线程真正启动并开始执行用户函数时,通过这个函数来更新状态标志。

(6)线程入口函数

cpp 复制代码
static void* func(void*args)
{
  thread* pt=static_cast<thread*>(args);
  pt->enablerunning();
  if(pt->_detach) pt->detach();
  pthread_setname_np(pt->_id,pt->_name.c_str());
  pt->_fun(pt->_data);
  return nullptr;
}

func为静态私有成员函数,作为线程的系统入口点,接收void指针参数,首先将其强制转换为thread对象指针,接着调用enablerunning将线程的运行状态设为true,随后检查分离状态,若_detach为true,则调用detach执行系统级线程分离,通过pthread_setname_np设置线程名称,最后调用_fun并传入数据_data,执行用户定义的逻辑,线程结束时返回nullptr,返回值将被pthread_join捕获并存入_res中。

(7)线程分离

cpp 复制代码
void detach()
{
  if(_detach) return;
  if(_running)
  pthread_detach(_id);
  enabledetach();
}

detach函数用于将线程设置为分离状态,使其退出时自动释放资源而无需其他线程等待。函数首先检查_detach标志,如果已经处于分离状态则直接返回,接着判断_running标志,如果线程已经启动运行,则立即调用系统函数pthread_detach传入线程ID _id,执行系统级线程分离,如果线程尚未启动,则暂不调用系统函数,只通过后面的enabledetach设置标志位,最后调用enabledetach将_detach标志设为true,记录分离状态。

(8)测试

线程thread类的总体实现如下:

thread.hpp

cpp 复制代码
#ifndef _THREAD_H_
#define _THREAD_H_
#include<iostream>
#include<string>
#include<pthread.h>
#include<cstdio>
#include<cstring>
#include<functional>
#include<cstdint>
#include<cerrno>
using namespace std;
namespace threadmodule
{
    static uint32_t num=1;
    template<class K>
    class thread
    {
        using func_t=function<void(K)>;
    private:
        void enabledetach()
        {
            _detach=true;
        }
        void enablerunning()
        {
            _running=true;
        }
        static void* func(void*args)
        {
            thread* pt=static_cast<thread*>(args);
            pt->enablerunning();
            if(pt->_detach) pt->detach();
            pthread_setname_np(pt->_id,pt->_name.c_str());
            pt->_fun(pt->_data);
            return nullptr;
        }
    public:
        thread(func_t fun,K data)
        :_id(0)
        ,_detach(false)
        ,_running(false)
        ,_res(nullptr)
        ,_fun(fun)
        ,_data(data)
        {
            _name="thread-"+to_string(num++);
        }
        void detach()
        {
            if(_detach) return;
            if(_running)
            pthread_detach(_id);
            enabledetach();
        }
        bool start()
        {
            if(_running) return false;
            int n=pthread_create(&_id,nullptr,func,this);
            if(n!=0)
            {
                cerr<<strerror(n)<<endl;
                return false;
            }
            else
            {
                cout<<_name<<"创建成功"<<endl;
                return true;
            }
        }
        bool stop()
        {
            if(_running)
            {
                int n=pthread_cancel(_id);
                if(n!=0)
                {
                    cerr<<strerror(n)<<endl;
                    return false;
                }
                else
                {
                  _running=false;
                  cout<<_name<<"stop"<<endl;
                  return true;
                }
            }
            return false;
        }
        void join()
        {
            if(_detach) {return;}
            int n=pthread_join(_id,&_res);
            if(n!=0)
            {
                cerr<<strerror(n)<<endl;
            }
        }
        ~thread()
        {}
    private:
        pthread_t _id;
        string _name;
        bool _detach;
        bool _running;
        void* _res;
        func_t _fun;
        K _data;
    };
}
#endif

Makefile

cpp 复制代码
thread:main.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -rf thread

g++ -o @ ^ -std=c++11 -lpthread,-std=c++11,Makefile一键化编译需要启用C++11标准,-lpthread链接POSIX线程库。

Main.cc

cpp 复制代码
#include"thread.hpp"
#include<unistd.h>
#include<vector>
#include<iostream>
using namespace std;
using namespace threadmodule;
void count(int id)
{
    cout<<"我是一个新线程,id:"<<id<<endl;
    sleep(1);
    cout<<"线程"<<id<<"退出"<<endl;
}
int main()
{
    vector<thread<int>> pv;
    for(int i=0;i<10;i++)
    {
        pv.emplace_back(count,i);
    }
    for(auto& thread:pv)
    {
        thread.start();
    }
    for(auto& thread:pv)
    {
        thread.join();
    }
    return 0;
}

main.cc对thread.hpp文件实现的线程thread类进行测试,vector<thread<int>> pv,pv用于创建多个线程对象,pv.emplace_back(count,i),通过for循环创建10个线程,每个线程执行count函数,输出自己的ID编号,休眠1秒后退出,通过范围for遍历容器pv,依次调用每个线程的start函数,10个线程并发执行,最后范围for遍历容器,依次调用每个线程的join函数,主线程阻塞直到所有线程退出。由于10个线程并发执行,输出会交错出现,具体哪个线程先执行取决于操作系统的调度,运行结果如下所示:

7、线程互斥

(1)相关概念

共享资源

临界资源:多线程执行流被保护的共享的资源就叫做临界资源。

临界区:每个线程内部,访问临界资源的代码,就叫做临界区。

互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

(2)互斥量

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题。

操作共享变量会有问题的售票系统:

cpp 复制代码
#include<stdlib.h> 
#include<string.h> 
#include<unistd.h> 
#include<pthread.h>
#include<cstdio>
int ticket = 100; 
void *route(void *arg)
{  
    char *id = (char*)arg;    
    while(1)
    {        
        if(ticket>0)
        {            
            usleep(1000);            
            printf("%s sells ticket:%d\n", id, ticket);                        
            ticket--;        
        } 
        else 
        {           
            break;        
        }    
    } 
    return nullptr;
} 
int main(void) 
{    
    pthread_t t1, t2, t3, t4;    
    pthread_create(&t1, NULL, route, (void*)"thread 1");    
    pthread_create(&t2, NULL, route, (void*)"thread 2");    
    pthread_create(&t3, NULL, route, (void*)"thread 3");    
    pthread_create(&t4, NULL, route, (void*)"thread 4");    
    pthread_join(t1, NULL);    
    pthread_join(t2, NULL);    
    pthread_join(t3, NULL);    
    pthread_join(t4, NULL); 
    return 0;
}

程序创建了4个线程模拟4个售票窗口,共同销售100张票,每个线程在票数大于0时卖出一张票,并将票数减一,直到票卖完为止,运行结果如下所示:

可以看到票数减到了负数,原因在于if语句判断条件为真之后,代码可以并发的切换到其他线程,usleep这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。

--ticket操作本身就不是一个原子操作

--操作并不是原子操作,而对应三条汇编指令:

load:将共享变量ticket从内存加载到寄存器中

update:更新寄存器里面的值,执行-1操作

store:将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题,需要做到三点:

代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁,Linux上称为互斥量。

初始化互斥量:

静态分配

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

动态分配

mutex为要初始化的互斥量,attr为NULL。

销毁互斥量:

销毁互斥量需要注意:

使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁

不要销毁一个已经加锁的互斥量

已经销毁的互斥量,要确保后面不会有线程再尝试加锁

互斥量加锁和解锁

成功返回0,失败返回错误号,调用pthread_lock时,可能会遇到以下情况:

互斥锁处于未锁状态,该函数会将互斥量锁定,同时返回成功。

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞,执行流被挂起,等待互斥量解锁。

改进以后的售票系统:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
#include<sched.h>
int ticket = 100; 
pthread_mutex_t mutex;
void *route(void *arg) 
{
 char *id = (char*)arg;
 while(1)
 {
  pthread_mutex_lock(&mutex);
  if(ticket > 0)
  {
   usleep(1000);
   printf("%s sells ticket:%d\n", id, ticket);
   ticket--;
   pthread_mutex_unlock(&mutex);
  }
  else
  {
   pthread_mutex_unlock(&mutex);
   break;
  }
 }
 return nullptr;
}
int main(void)
{
 pthread_t t1, t2, t3, t4;
 pthread_mutex_init(&mutex, NULL);
 pthread_create(&t1, NULL, route, (void*)"thread 1");
 pthread_create(&t2, NULL, route, (void*)"thread 2");
 pthread_create(&t3, NULL, route, (void*)"thread 3");
 pthread_create(&t4, NULL, route, (void*)"thread 4");
 pthread_join(t1, NULL);
 pthread_join(t2, NULL);
 pthread_join(t3, NULL);
 pthread_join(t4, NULL);
 pthread_mutex_destroy(&mutex);
 return 0;
}

pthread_mutex_lock(&mutex),每个线程在执行时先加锁,然后检查票数是否大于0,pthread_mutex_unlock(&mutex),线程执行完毕后解锁。主函数负责初始化互斥锁,创建4个线程,pthread_mutex_destroy(&mutex),等待所有线程执行完毕后销毁互斥锁,运行结果如下所示:

可以看到,使用互斥锁成功解决了并发访问共享数据的安全问题,程序不会出现票数为负的情况。

(3)互斥锁封装

mutex.hpp

<1> 构造函数
cpp 复制代码
#pragma once
#include<iostream>
#include<pthread.h>
using namespace std;
namespace mutexmodule
{
    class mutex
    {
    public:
        mutex()
        {
            pthread_mutex_init(&_mutex,nullptr);
        }
    private:
        pthread_mutex_t _mutex;
    };
}

pthread_mutex_init(&_mutex,nullptr),构造函数mutex通过调用pthread_mutex_init来初始化互斥锁对象。

<2> lock
cpp 复制代码
void lock()
{
   int n=pthread_mutex_lock(&_mutex);
   (void)n;
}

int n=pthread_mutex_lock(&_mutex),lock函数调用pthread_mutex_lock进行加锁,如果锁已被其他线程持有则阻塞等待。

<3> unlock
cpp 复制代码
void unlock()
{
  int n=pthread_mutex_unlock(&_mutex);
  (void)n;
}

int n=pthread_mutex_unlock(&_mutex),unlock函数调用pthread_mutex_unlock进行解锁,唤醒等待该锁的其他线程。

<4> 析构函数
cpp 复制代码
~mutex()
{
   pthread_mutex_destroy(&_mutex);
}

pthread_mutex_destroy(&_mutex),析构函数~mutex调用pthread_mutex_destroy进行互斥锁的销毁,释放系统资源。

<5> lockguard
cpp 复制代码
class lockguard
{
public:
    lockguard(mutex& mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~lockguard()
    {
        _mutex.unlock();
    }
private:
        mutex& _mutex;
};

lockguard构造函数构造时接收一个mutex对象,将传入的互斥锁引用保存到成员变量_mutex,_mutex.lock(),调用lock在构造时进行加锁。析构函数~lockguard调用保存的互斥锁对象的unlock在析构时进行解锁。

8、线程同步

(1)同步与竞态

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,称为同步。

竞态:因为时序问题,而导致程序异常,称为竞态。

(2)条件变量

当一个线程互斥地访问某个变量时,可能发现在其他线程改变状态之前,什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,直到其他线程将一个节点添加到队列中,这种情况就需要用到条件变量。

<1> pthread_cond_init

pthread_cond_init用于初始化条件变量,cond为指向要初始化条件变量对象的指针,attr为条件变量属性,通常设为NULL,使用默认属性,调用成功返回0,失败返回相应错误码。

<2> pthread_cond_destroy

pthread_cond_destroy用于销毁条件变量,释放条件变量占用的资源,cond指向要销毁条件变量的指针,销毁成功返回0,失败返回相应的错误码。

<3> pthread_cond_wait

pthread_cond_wait用于等待条件变量,它会原子性地解锁互斥锁并阻塞线程,直到被条件变量唤醒。cond为条件变量指针,mutex为互斥锁指针,调用成功返回0,失败返回相应的错误码。

<4> 唤醒等待

pthread_cond_broadcast、pthread_cond_signal都是用于唤醒等待条件变量的线程的两个函数,pthread_cond_broadcast用于唤醒所有线程,pthread_cond_signal用于唤醒一个线程。cond指向相应等待的条件变量,调用成功返回0,失败返回相应的错误码。

下面演示一个通过条件变量来控制多线程协作计数:

cpp 复制代码
#include<iostream>
#include<vector>
#include<string>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 5
int cnt=1000;
pthread_mutex_t glock=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond=PTHREAD_COND_INITIALIZER;
void* func(void*args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&glock);
        pthread_cond_wait(&gcond,&glock);
        if(cnt>2000) 
        {
            pthread_mutex_unlock(&glock);
            break;
        }
        cout<<name<<":"<<cnt<<endl;
        cnt++;
        pthread_mutex_unlock(&glock);
    }
    delete[] (char*)args;
    return nullptr;
}
int main()
{
    vector<pthread_t> pv;
    for(int i=0;i<NUM;i++)
    {
        pthread_t id;
        char *name=new char[64];
        snprintf(name,64,"thread-%d",i);
        int n=pthread_create(&id,nullptr,func,name);
        if(n!=0) continue;
        pv.push_back(id);
    }
    sleep(3);
    for(int i=0;i<1500;i++)
    {
        pthread_cond_signal(&gcond);
        usleep(500);
    }
    for(auto& id:pv)
    {
        int n=pthread_join(id,nullptr);
        (void)n;
    }
    return 0;
}

vector<pthread_t> pv,int n=pthread_create(&id,nullptr,func,name),主线程通过for循环创建5个子线程,pthread_cond_signal(&gcond),所有子线程启动后会等待条件变量gcond,处于阻塞状态,通过for循环发送信号,逐个唤醒等待的子线程,被唤醒的子线程会先加锁,然后让计数器cnt自增一,最后解锁,当cnt大于2000时,线程会解锁并退出循环,主线程发送完所有信号后,会等待每个子线程结束,然后程序正常退出,运行结果如下所示:

<5> 条件变量封装

cond.hpp

构造函数

cpp 复制代码
#pragma once
#include<pthread.h>
#include"mutex.hpp"
using namespace mutexmodule;
namespace condmodule
{
    class cond
    {
    public:
        cond()
        {
            pthread_cond_init(&_con,nullptr);
        }
    private:
        pthread_cond_t _con;
    };
}

pthread_cond_init(&_con,nullptr),cond构造函数通过调用pthread_cond_init来初始化条件变量。

wait

cpp 复制代码
void wait(mutex& mutex)
{
    int n=pthread_cond_wait(&_con,mutex.get());
    (void)n;
}

wait成员函数原子性地释放互斥锁并阻塞等待条件变量,int n=pthread_cond_wait(&_con,mutex.get()),通过mutex.get()获取底层的pthread_mutex_t*

cpp 复制代码
pthread_mutex_t* get()
{
    return &_mutex;
}

指针,调用pthread_cond_wait进入等待状态。

broadcast

cpp 复制代码
void broadcast()
{
    int n=pthread_cond_broadcast(&_con);
    (void)n;
}

int n=pthread_cond_broadcast(&_con),broadcast通过调用pthread_cond_broadcast唤醒所有当前阻塞在_con条件变量下的线程。

signal

cpp 复制代码
void signal()
{
    int n=pthread_cond_signal(&_con);
    (void)n;
}

int n=pthread_cond_signal(&_con),signal通过调用pthread_cond_signal唤醒一个当前阻塞在_con条件变量上的线程。

析构函数

cpp 复制代码
~cond()
{
    pthread_cond_destroy(&_con);
}

pthread_cond_destroy(&_con),析构函数通过调用pthread_cond_destroy销毁_con条件变量对象。

(3)生产者消费者模型

生产者消费者模型支持线程解耦、并发、忙闲不均:

生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题,生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接输入到阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

基于blockingqueue的生产者消费者模型

在多线程编程中阻塞队列(blockingqueue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素,当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。

下面通过queue来模拟实现阻塞队列的生产消费模型:

blockqueue.hpp

构造函数

cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>
#include"mutex.hpp"
#include"cond.hpp"
using namespace std;
const int capacity=10;
using namespace mutexmodule;
using namespace condmodule;
template<class K>
class blockqueue
{
public:
    blockqueue(int cap=capacity)
            :_capacity(cap)
            ,_csleep_num(0)
            ,_psleep_num(0)
    {
    }
private:
    queue<K> _q;
    int _capacity;
    mutex _mutex;
    cond _empty;
    cond _full;
    int _csleep_num;
    int _psleep_num;
};

_q用来模拟阻塞队列,_capacity为阻塞队列的最大容量限制,_mutex使用上面我们封装的mutex类,_empty、_full使用上面我们封装的cond类,它们分别为阻塞队列为空或满时的条件变量,_csleep_num用来记录当前正在等待的消费者线程数量,_psleep_num用来记录当前正在等待的生产者线程数量。构造函数blockqueue用于初始化阻塞队列各成员变量。

私有函数

cpp 复制代码
bool isfull(){ return _q.size()>=_capacity;}
bool isempty() { return _q.empty();}

isfull、isempty用来判断队列的满、空状态,当队列当前大小_q.size大于等于容量_capacity时isfull返回true,当队列为空时isempty返回true。

生产者

cpp 复制代码
void Equeue(const K& in)
{
    {
        lockguard guard(_mutex);
        while(isfull())
        {
            _psleep_num++;
            _full.wait(_mutex);
            _psleep_num--;
        }
        _q.push(in);
        if(_csleep_num>0)
        {
          _empty.signal();
          cout<<"唤醒消费者"<<endl;
        }
    }
}

生产者函数Equeue用于向队列中添加数据,函数首先使用lockguard自动加锁,然后通过while循环判断队列是否已满,如果队列已满,生产者会递增等待计数,_full.wait(_mutex),在条件变量_full上等待,被唤醒后递减等待计数,当队列不满时,将数据推入队列,如果当前有消费者在等待,就通过条件变量_empty唤醒其中一个消费者,_empty.signal(),函数结束时lockguard自动解锁。

消费者

cpp 复制代码
K pop()
{
    K data;
    {
        lockguard guard(_mutex);
        while(isempty())
        {
            _csleep_num++;
            _empty.wait(_mutex);
            _csleep_num--;
        }
        data=_q.front();
        _q.pop();
        if(_psleep_num>0)
        {
          _full.signal();
          cout<<"唤醒生产者"<<endl;
        }
    }
    return data;
}

消费者函数pop用于从队列中取出数据,临时变量data用于存储返回值,lockguard guard(_mutex),使用lockguard自动加锁,通过while循环判断队列是否为空,若队列为空,则消费者递增等待计数,_empty.wait(_mutex),在条件变量_empty上等待,被唤醒后递减等待计数。当队列非空时,取出队首元素保存到data中,并将该元素从队列弹出,如果当前有生产者在等待,就通过条件变量_full唤醒其中一个生产者,离开作用域时lockguard自动解锁,最后返回取出的数据。

9、POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX可以用于线程间同步。

初始化信号量

sem_init函数用于初始化未命名信号量,sem为指向信号量对象的指针,pshared为共享标志,0表示线程间共享,非0表示进程间共享,value为信号量的初始值,初始化成功返回0,失败返回-1。

销毁信号量

sem_destroy用于销毁未命名信号量,sem为指向要销毁的信号量对象的指针,调用成功返回0,失败返回-1。

等待信号量

sem_wait用于等待信号量,执行P操作,等待信号量,会将信号量的值-1,sem为指向信号量对象的指针,调用成功返回0,失败返回-1。

发布信号量

sem_post用于信号量的释放,执行V操作,发布信号量,表示资源使用完毕,可以归还资源了,将信号量值+1,sem为指向信号量对象的指针,调用成功返回0,失败返回-1。

10、信号量封装

sem.hpp

构造函数

cpp 复制代码
#include<iostream>
#include<pthread.h>
#include<semaphore.h>
using namespace std;
namespace semmodule
{
    const int num=1;
    class sem
    {
    public:
        sem(unsigned int val=num)
        {
            sem_init(&_sem,0,val);
        }
    private:
        sem_t _sem;
    };
}

sem_init(&_sem,0,val),构造函数sem通过调用sem_init来初始化信号量,val为信号量的初始值,默认为1,第二个参数0表示线程间共享。

P操作

cpp 复制代码
void p()
{
    int n=sem_wait(&_sem);
    (void)n;
}

int n=sem_wait(&_sem),p函数通过sem_wait等待信号量,执行P操作,将信号量值减1,当信号量为0时将阻塞。

V操作

cpp 复制代码
void v()
{
    int n=sem_post(&_sem);
    (void)n;
}

int n=sem_post(&_sem),v函数通过sem_post释放信号量,执行V操作,将信号量值加1,并唤醒等待线程。

析构函数

cpp 复制代码
~sem()
{
    sem_destroy(&_sem);
}

sem_destroy(&_sem),析构函数通过调用sem_destroy来销毁信号量,并释放资源。

11、基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空,也可以预留一个空的位置,作为满的状态。

环形队列实现:

ringqueue.hpp

构造函数

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
#include"sem.hpp"
#include"mutex.hpp"
using namespace std;
using namespace semmodule;
using namespace mutexmodule;
static const int gsize=5;
template<class K>
class ringqueue
{
public:
    ringqueue(int size=gsize)
            :_size(size)
            ,_rq(size)
            ,_p_step(0)
            ,_c_step(0)
            ,_blank_sem(size)
            ,_data_sem(0)
    {}
private:
    vector<K> _rq;
    int _size;
    int _p_step;
    sem _blank_sem;
    sem _data_sem;
    int _c_step;
    mutex _cmutex;
    mutex _pmutex;
};

ringqueue构造函数用于初始化环形队列,_size为队列容量,默认为gsize,_rq(size),用于创建size个元素的vector,使用默认值初始化,_p_step为生产者下标,初始为0,_c_step为消费者下标,初始为0,_blank_sem(size),_blank_sem表示空闲位置信号量,初始值为size,表示所有位置都为空,_data_sem(0),_data_sem为数据信号量,初始值为0,表示没有数据。

入队

cpp 复制代码
void Equeue(const K& in)
{
    _blank_sem.p();
    {
        lockguard guard(_pmutex);
        _rq[_p_step]=in;
        _p_step++;
        _p_step%=_size;
    }
    _data_sem.v();
}

Equeue函数作为生产者,将数据放入队列,_blank_sem.p(),等待空闲位置,如果队列满了,会阻塞等待,lockguard guard(_pmutex),加锁保护生产者下标,_rq[_p_step]=in,将数据放入当前生产者位置,_p_stepp++,生产者下标后移,_p_step%=_size,环形移动,到达末尾后回到0,_data_sem.v(),将数据量+1,唤醒可能正在等待数据的消费者。

出队

cpp 复制代码
void pop(K* out)
{
    _data_sem.p();
    {
        lockguard guard(_cmutex);
        *out=_rq[_c_step];
        _c_step++;
        _c_step%=_size;
    }
    _blank_sem.v();
}

pop函数作为消费者,将数据从队列取出,_data_sem.p(),等待数据可用,lockguard guard(_cmutex),加锁保护消费者下标,*out=_rq[_c_step],对数据进行读取,_c_step++,消费者下标后移,_c_step%=_size进行环形回绕,离开作用域后自动解锁,_blank_sem.v()增加一个空闲位置。

12、日志

日志是记录系统和软件运行中发生事件的文件,主要作用是监控运行状态、记录异常信息,帮助快速定位问题进行问题修复。它是系统维护、故障排查和安全管理的重要工具。

日志格式如下:

日志实现:

log.hpp

基类

cpp 复制代码
#ifndef _LOG_HPP_
#define _LOG_HPP_
#include<iostream>
#include<string>
#include<cstdio>
#include<filesystem>
#include<fstream>
#include<sstream>
#include<memory>
#include<ctime>
#include<unistd.h>
#include"mutex.hpp"
using namespace std;
namespace logmodule
{
    using namespace mutexmodule;
    const string gsep ="\r\n";
    class logstrategy
    {
    public:
        ~logstrategy()=default;
        virtual void synclog(const string& message)=0;
    };
}

logstrategy为抽象策略类,用于定义日志输出策略的接口,作为基类使用。

consolelogstrategy:控制台策略

cpp 复制代码
class consolelogstrategy:public logstrategy
{
public:
    consolelogstrategy(){}
    void synclog(const string& message) override
    {
        lockguard guard(_mutex);
        cout<<message<<gsep;
    }
    ~consolelogstrategy(){}
private:
    mutex _mutex;
};

consolelogstrategy为控制台实现类,该类功能是将日志输出到控制台,lockguard guard(_mutex),使用互斥锁来保证多线程安全,每条日志后添加\r\n换行。

filelogstrategy:文件策略

cpp 复制代码
const string defaultpath="./log";
const string defaultfile="my.log";
class filelogstrategy:public logstrategy
{
public:
    filelogstrategy(const string& path=defaultpath,const string& file=defaultfile)
        :_path(path),
         _file(file)
        {
            lockguard guard(_mutex);
            if(filesystem::exists(_path))
            {
                return;
            }
            try
            {
                filesystem::create_directories(_path);
            }
            catch(const filesystem::filesystem_error& e)
            {
                cerr<<e.what()<<endl;
            }
        }
    void synclog(const string& message) override
    {
        lockguard guard(_mutex);
        string filename=_path+(_path.back()=='/'?"":"/")+_file;
        ofstream out(filename,ios::app);
        if(!out.is_open())
        {
            return;
        }
        out<<message<<gsep;
        out.close();
    }
    ~filelogstrategy(){}
private:
    string _path;
    string _file;
    mutex _mutex;
};

filelogstrategy为文件策略实现类,功能是将日志输出到文件,filelogstrategy构造函数自动创建目录,synclog以追加的方式将日志信息写入文件,通过互斥锁来保证线程安全。

loglevel:日志等级

cpp 复制代码
enum class loglevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

loglevel将日志分为5个等级,从低到高为:DEBUG(调试)、INFO(信息)、WARNING(警告)、ERROR(错误)、FATAL(致命)。

levelstr:日志级别转字符串

cpp 复制代码
string levelstr(loglevel lev)
{
    switch(lev)
    {
        case loglevel::DEBUG return "DEBUG";
        case loglevel::INFO return "INFO";
        case loglevel::WARNING return "WARNING";
        case loglevel::ERROR return "ERROR";
        case loglevel::FATAL return "FATAL";
        default: return "UNKNOWN";
    }
}

levelstr用于将枚举类型转换为可读字符串,即将日志级别转化为字符串。

gettimestamp:获取时间戳

cpp 复制代码
string gettimestamp()
{
    time_t t=time(nullptr);
    struct tm curr_tm;
    localtime_r(&t,&curr_tm);
    char timebuffer[128];
    snprintf(timebuffer, sizeof(timebuffer),"%4d-%02d-%02d %02d:%02d:%02d",
    curr_tm.tm_year+1900,
    curr_tm.tm_mon+1,
    curr_tm.tm_mday,
    curr_tm.tm_hour,
    curr_tm.tm_min,
    curr_tm.tm_sec);
    return timebuffer;
}

gettimestamp用于获取当前时间的格式化字符串。

logger:日志器

cpp 复制代码
class logger
{
public:
    logger()
    {
        enableconsolelogstrategy();
    }
    void enableconsolelogstrategy()
    {
        _ptr=make_unique<consolelogstrategy>();
    }
    void enablefilelogstrategy()
    {
        _ptr=make_unique<filelogstrategy>();
    }
private:
    unique_ptr<logstrategy> _ptr;
};

构造函数logger通过调用enableconsolelogstrategy来初始化日志器,默认使用控制台输出。

enableconsolelogstrategy、enablefilelogstrategy分别为控制台、文件输出策略,实现动态切换日志输出策略,使用unique_ptr自动管理内存。

logmessage:内部类

cpp 复制代码
class logmessage
{
public:
    logmessage(string& src,loglevel& level,int num,logger& logger)
    :_pid(getpid())
    ,_src(src)
    ,_num(num)
    ,_curr_time(gettimestamp())
    ,_level(level)
    ,_logger(logger)
    {
        stringstream ss;
        ss<<"["<<_curr_time<<"]"<<"["<<levelstr(_level)<<"]"<<"["<<_pid<<"]"<<"["<<_src<<"]"<<"["<<_num<<"]"<<"-";
        _loginfo=ss.str();
     }
     template<class K>
     logmessage& operator<<(const K& info)
     {
         stringstream ss;
         ss<<info;
         _loginfo+=ss.str();
         return *this;
     }
     ~logmessage()
     {
         if(_logger._ptr)
         {
             _logger._ptr->synclog(_loginfo);
         }
     }
private:
     string _curr_time;
     loglevel _level;
     pid_t _pid;
     string _src;
     int _num;
     string _loginfo;
     logger& _logger;
};

logmessage构造函数用来构造日志头,自动记录时间、级别、进程ID、文件名、行号,operator<<重载用于支持流式输出任意类型。~logmessage析构函数将在临时对象销毁时输出日志信息,体现了RAII设计的核心思想,在构造时准备,析构时自动调用策略对象的synclog,输出日志信息。

operator()

cpp 复制代码
logmessage operator()(loglevel lev,string name,int line)
{
    return logmessage(level,name,line,*this);
}

operator()用于创建并返回一个临时的logmessage对象,用于构建日志消息,level为日志级别,name为文件名,line为行号,将返回一个临时的logmessage对象,这个对象负责收集日志内容,并在析构时输出日志信息。

全局对象和宏

cpp 复制代码
logger log;
#define LOG(level) logger(level,__FILE__,__LINE__);
#define Enable_Console_Log_Strategy() logger.enableconsolelogstrategy()
#define Enable_File_Log_Strategy() logger.enablefilelogstrategy()

logger log为全局单例,提供全局日志访问点,LOG(level)用于简化日志调用,自动传入文件名和行名,Enable_Console_Log_Strategy() 、Enable_File_Log_Strategy() 为策略切换宏,方便切换全局日志策略。

13、线程池

(1)概念

线程池,是一种线程使用模式。线程过多会带来调度开销,进而影响缓冲局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务,避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度,可用线程数量应该取决于可用的并发处理器、处理器内核、内存。网络sockets等的数量。

(2)应用场景

需要大量的线程来完成任务,且完成任务的时间比较短。

对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,但短时间内产生大量线程可能使内存达到极限,出现错误。

下面创建的是固定数量的线程池,循环从任务队列中获取任务对象,获取到任务对象后,执行任务对象中的任务接口,如下图所示:

(3)单例模式

某些类,只具有一个对象(实例),就称之为单例。在很多服务器开发场景中,经常需要让服务器加载很多的数据到内存中,此时往往需要用一个单例的类来管理这些数据。单例有两种实现方式。

<1> 饿汉实现方式

吃完饭,立刻洗碗,这种就是饿汉方式。

cpp 复制代码
template<class K>
class Singleton
{
    static K data;
public:
    static K* GetInstance()
    {
        return &data;
    }
};

只要通过Singleton这个包装类来使用K对象,则一个进程中只有一个K对象的实例。

<2> 懒汉实现方式
cpp 复制代码
template<class K>
class Singleton
{
    static K* inst;
public:
    static K* GetInstance()
    {
        if (inst == NULL)
        {
            inst = new K();
        }
        return inst;
    }
};

懒汉实现方式存在一个严重的问题,线程不安全,第一次调用GetInstance的时候,如果两个线程同时调用,可能会创建出两份K对象的实例,但是后续再次调用,就没有问题了。

线程安全版本:

cpp 复制代码
template<class K>
class Singleton
{
    volatile static K* inst;
    static std::mutex lock;
public:
    static K* GetInstance() 
    {
        if (inst == NULL) 
        {     
            lock.lock();          
            if (inst == NULL) 
            {
                inst = new K();
            }
            lock.unlock();
        }
        return inst;
    }
};

volatile static K* inst,需要设置volatile关键字,否则可能被编译器优化,双重if判定空指针,降低锁冲突的概率,提高性能,使用互斥锁,保证多线程情况下也只能调用一次new。懒汉模式最核心的思想是"延时加载",从而能够优化服务器的启动速度。

(4)实现

单例式线程池

threadpool.hpp

wakeupallthread

cpp 复制代码
#pragma once
#include<iostream>
#include<string>
#include<vector>
#include<queue>
#include<pthread.h>
#include"cond.hpp"
#include"thread.hpp"
#include"log.hpp"
#include"mutex.hpp"
using namespace std;
namespace threadpoolmodule
{
    using namespace threadmodule;
    using namespace logmodule;
    using namespace condmodule;
    using namespace mutexmodule;
    static const int gnum=5;
    template<class K>
    class threadpool
    {
    private:
        void wakeupallthread()
        {
            lockguard guard(_mutex);
            if(_sleepernum>0)
            _cond.broadcast();
            LOG(loglevel::INFO)<<"唤醒所有线程";
        }
    private:
        vector<thread> _threads;
        int _num;
        queue<K> _tasks;
        cond _cond;
        mutex _mutex;
        bool _isrunning;
        int _sleepernum;
        static threadpool<K>* _tp;
        static mutex _lock;
    };
}

lockguard guard(_mutex),_cond.broadcast(),wakeupallthread加锁通过调用broadcast唤醒所有正在等待任务的休眠线程,并通过LOG输出日志信息。

wakeupone

cpp 复制代码
void wakeupone()
{
    _cond.signal();
    LOG(loglevel::INFO)<<"唤醒一个线程";
}

_cond.signal(),wakeupone通过调用signal唤醒一个正在等待任务的线程,并通过LOG输出日志信息。

私有构造函数

cpp 复制代码
threadpool(int num=gnum)
        :_num(num)
        ,_isrunning(false)
        ,_sleepernum(0)
{
    for(int i=0;i<num;i++)
    {
        _threads.emplace_back([this]() {handler();});
    }
}

threadpool构造函数用于创建线程池对象,初始化线程数量、运行状态、休眠线程数,通过for循环创建num个工作线程,每个线程执行handler函数。

start

cpp 复制代码
void start()
{
    if(_isrunning) return;
    _isrunning=true;
    for(auto& thread:_threads)
    {
        thread.start();
        LOG(loglevel::INFO)<<"start new thread success"<<thread.name();
    }
}

start用于启动所有线程,将_running标志位设置为true,thread.start(),通过范围for启动所有工作线程,通过LOG记录启动日志。

get

cpp 复制代码
static threadpool<K>* get()
{
    if(_tp==nullptr)
    {
        lockguard guard(_lock);
        LOG(loglevel::DEBUG)<<"获取单例";
        if(_tp==nullptr)
        {
            LOG(loglevel::DEBUG)<<"首次创建单例 ";
            _tp=new threadpool<K>();
            _tp->start();
        }
    }
    return _tp;
}

get用于获取线程池的唯一实例,双重if检查保证多线程环境下只创建一个实例,_tp=new threadpool<K>(),_tp->start(),首次调用时创建线程池并启动,成功创建后,后续调用直接return _tp即可。

stop

cpp 复制代码
void stop()
{
    if(!_isrunning) return;
    _isrunning=false;
    wakeupallthread();
}

stop用于停止线程池,不再接受新任务,_isrunning=false,将_isrunning标志设置为false,wakeupallthread(),唤醒所有线程,检查退出条件。

join

cpp 复制代码
void join()
{
    for(auto& thread:_threads)
    {
        thread.join();
    }
}

thread.join(),join通过范围for阻塞等待每个线程执行结束,避免资源泄漏。

handler

cpp 复制代码
void handler()
{
    char name[128];
    pthread_getname_np(pthread_self(),name,sizeof(name));
    while(1)
    {
        K task;
        {
            lockguard guard(_mutex);
            while(_tasks.empty() && _isrunning)
            {
                _sleepernum++;
                _cond.wait(_mutex);
                _sleepernum--;
            }
            if(!_isrunning && _tasks.empty())
            {
                LOG(loglevel::INFO)<<name<<"线程退出";
                break;
            }
            task=_tasks.front();
            _tasks.pop();
        }
        task();
    }
}

handler为线程工作主函数,通过pthread_getname_np获取线程名,用于日志记录,通过while循环持续处理任务,lockguard guard(_mutex),通过加锁访问任务队列,_tasks.pop(),从队列前端取出任务,_cond.wait(),释放锁并等待,当线程停止且队列为空时,退出循环,在锁外执行任务,避免阻塞其他线程。

Equeue

cpp 复制代码
bool Equeue(const K& in)
{
    if(_isrunning)
    {
        lockguard guard(_mutex);
        _tasks.push(in);
        if(_threads.size()==_sleepernum)
        {
            wakeupone();
        }
        return true;
    }
    return false;
}

lockguard guard(_mutex),_tasks.push(in),Equeue用于向任务队列中添加新任务,加锁保护任务队列,并将任务放入队列中,如果所有线程都在休眠,则通过wakeupone唤醒一个线程,调用成功返回true,失败返回false。

析构函数

cpp 复制代码
~threadpool()
{
    stop();
    join();
}

析构函数~threadpool用于销毁线程池对象,调用stop停止接收新任务,调用join等待所有线程退出,确保资源正确释放。

成员函数删除

cpp 复制代码
threadpool(const threadpool<K>& )=delete;
threadpool<K>& operator=(const threadpool<K>& )=delete;

删除拷贝构造和赋值函数,确保线程池单例的唯一性。

静态成员变量

cpp 复制代码
template<class K>
threadpool<K>* threadpool<K>::_tp=nullptr;
template<class K>
mutex threadpool<K>::_lock;

定义和初始化静态成员变量_tp、_lock,将_tp初始化为nullptr,_lock为静态互斥锁,线程池使用该互斥锁,调用默认构造进行初始化。

心得:

线程是操作系统调度的最小执行单元,同一进程内的多个线程共享堆内存与方法区,但拥有独立的程序计数器,Linux中的线程是一种轻量级进程,多线程共享父进程的地址空间,文件描述符和信号处理函数。这一设计决定了Linux线程在创建和切换上的成本远低于传统进程,但同时也意味着线程间的耦合度更高。线程同步机制方面,引入了条件变量、互斥锁,互斥锁在无竞争时采用快速路径的原子操作,一旦产生竞争则会触发系统调用将线程挂起于内核等待队列,条件变量必须配合互斥锁使用,用于实现生产者-消费者模型中的等待和唤醒,关于日志和线程池,日志核心价值在于问题排查、行为追踪和性能监控,两者的关联在于,线程池的任务执行情况往往通过日志来记录,而日志框架自身的异步也依赖线程池来实现,线程池的核心价值在于复用线程、控制并发上限和隔离业务场景,掌握Linux线程的这些底层行为,才能在性能敏感的场景中做出正确的开发决策。

相关推荐
那我懂你的意思啦2 小时前
Vue2+Vue3学习
前端·vue.js·学习
蓝净云2 小时前
RESP 协议的工作原理
学习
minji...2 小时前
Linux 多线程(五)用C++语言以面向对象方式封装线程
linux·运维·服务器·网络·jvm·数据库
来鸟 鸣间2 小时前
mutex_lock 流程
linux·c语言
秋风&萧瑟2 小时前
【Linux系统编程】system函数和exec函数族的使用
linux·运维·服务器
炽烈小老头2 小时前
【 每天学习一点算法 2026/04/06】常数时间插入、删除和获取随机元素
学习·算法
秋风&萧瑟2 小时前
【Linux系统编程】Linux多进程介绍及使用
linux·运维·网络
三万棵雪松2 小时前
【Linux 物联网网关主控系统-Web部分(四)】
linux·前端·物联网·嵌入式linux
宵时待雨2 小时前
linux笔记归纳1:linux初识
linux·运维·笔记