线程的互斥与同步

目录

前言

互斥

存在问题

问题解决

简单使用

相关问题

原理

同步

概念

条件变量

POSIX信号量

概念

相关接口

生产者消费者模型

概念

阻塞队列

环形缓冲区

日志

线程池

概念

实现

单例模式

线程安全和可重入问题


前言

之前我们在使用多线程时发现,一个进程内部的多个线程中,因为所有的线程共享地址空间,进程资源大部分都会被线程共享,如果多个线程同时访问一个共享资源,例如多个线程同时向显示器打前言

互斥

存在问题

我们来看以下代码:

cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>
#include <vector>

int tickets = 10000; //共享资源

void GetTicket()
{
    char name[64];
    pthread_getname_np(pthread_self(), name ,sizeof(name));
    while (1)
    {
        if (tickets > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, tickets);
            tickets--;
        }
        else
        {
            break;
        }
    }
}

int main()
{
    ThreadModule::Thread t1(GetTicket);
    ThreadModule::Thread t2(GetTicket);
    ThreadModule::Thread t3(GetTicket);
    ThreadModule::Thread t4(GetTicket);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    return 0;
}

运行这个程序,如下所示:

我们发现这个票数最终出现了负数,这是因为有多个线程对票数进行更新导致了数据不一致问题。当内存中tickets中为1时,线程1想要对tickets进行修改,此时tickets大于0,正要进行修改时,该线程1由于usleep被切换成其它的线程,线程2进来后,由于线程1还没对tickets进行修改,依旧为1,所以线程2可以对tickets进行修改,线程2也会usleep,依次类推,多个线程都会进入到if代码块中,当线程醒来后,都会对tickets变量进行修改,导致出现负数。在上面的代码中,主要是因为if中的判断出现了问题,判断条件导致了多个线程同时访问if代码块。

问题解决

上面代码出现问题的原因是没有对共享资源进行保护,因为,解决的方法就是将共享资源变成临界资源,将访问公共资源的代码变为临界区,如下所示:

多线程保护共享资源,本质是把访问临界资源的代码保护起来。

简单使用

加锁可以使用pthread_mutex_lock函数,解锁可以使用pthread_mutex_unlock函数,若定义的锁为全局或静态的,可以直接初始化,若开辟的锁在栈上,那么必须使用pthread_mutex_init和pthread_mutex_destory进行初始化和销毁。如下所示:

cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>
#include <vector>

int tickets = 10000; //共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


void GetTicket()
{
    char name[64];
    pthread_getname_np(pthread_self(), name ,sizeof(name));
    while (1)
    {
        pthread_mutex_lock(&mutex);
        if (tickets > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }

    }
}

int main()
{
    ThreadModule::Thread t1(GetTicket);
    ThreadModule::Thread t2(GetTicket);
    ThreadModule::Thread t3(GetTicket);
    ThreadModule::Thread t4(GetTicket);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    return 0;
}

当上锁之后,若有线程已经在访问临界区了,那么对应的线程会阻塞等待。运行结果如下:

使用局部锁,可以通过结构体传给对应的任务函数,使用示例如下:

cpp 复制代码
#include "Thread.hpp"
#include <unistd.h>
#include <vector>


int ticket = 100;

class thread_data
{
    public:
    thread_data(const std::string &n, pthread_mutex_t *p)
    :name(n),
    pmutex(p)
    {

    }
public:
    std::string name;
    pthread_mutex_t *pmutex;
};

void *route(void *arg)
{
    thread_data *td = static_cast<thread_data*>(arg);
    while (1)
    {
        pthread_mutex_lock(td->pmutex);
        if (ticket > 0)
        {
            usleep(1000);
            printf("%s sells ticket:%d\n", td->name.c_str(), ticket);
            ticket--;
            pthread_mutex_unlock(td->pmutex);
        }
        else
        {
            pthread_mutex_unlock(td->pmutex);
            break;
        }
    }

    return nullptr;
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    pthread_t t1, t2, t3, t4;
    thread_data td1("thread 1", &mutex);
    thread_data td2("thread 2", &mutex);
    thread_data td3("thread 3", &mutex);
    thread_data td4("thread 4", &mutex);

    pthread_create(&t1, NULL, route, (void *)&td1);
    pthread_create(&t2, NULL, route, (void *)&td2);
    pthread_create(&t3, NULL, route, (void *)&td3);
    pthread_create(&t4, NULL, route, (void *)&td4);
    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);
    pthread_join(t4, NULL);

    pthread_mutex_destroy(&mutex);

    return 0;
}

运行结果如下:

相关问题

在锁的使用中,存在以下要注意的事项:

  1. 加锁会增加多线程串行执行的场景,会导致效率降低,因此,加锁的粒度,必须足够细。
  2. 锁本身也是共享资源,加锁和解锁过程被设计成为了原子的,这样就可以保证锁本身被保护起来了。
  3. 访问临界资源,所有线程必须遵守加锁和解锁规则,不能有例外。
  4. 申请锁不成功的线程,必须要在锁上进行阻塞等待,加了锁之后也可以保证原子性。

原理

为了实现互斥锁,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相互交换,由于只有一条指令,保证了原子性,即使是多处理平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

内存中的变量是被线程共享的,只要拿到地址就可以使用,内存中的变量交换到CPU内部的寄存器中,本质是把共享数据,变成某个线程的私有数据,exchange方式是没有拷贝的,从始至终只有一个对应的锁资源,谁拥有这个锁资源,谁就拥有了锁,竞争锁,其实就是竞争执行exchange。

互斥的本质是独占,独占的本质我们认为临界资源只有一份,互斥锁可以被理解为一个信号量,只不过信号量值为1,表示一份资源。申请信号量本质就是对资源的预定机制,因此需要加锁。

同步

概念

同步指的是在临界资源安全的前提下,让访问临界资源具有一定的顺序性,使得资源被合理访问。

条件变量

条件变量指的是某一线程不符合对应的条件时,会在等待队列中进行等待,直到另外一个线程满足条件为止条件变量是一种线程同步的机制。条件变量可以有一个,也可以有多个,可以实现单方面的同步,也可以实现多方面的同步。

相关接口的声明如下:

cpp 复制代码
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t 
*restrict attr);

参数:
 cond:要初始化的条件变量
 attr:NULL

//销毁
int pthread_cond_destroy(pthread_cond_t *cond)

//等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
 cond:要在这个条件变量上等待
 mutex:互斥量,后⾯详细解释

//唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

以下是一个简单的测试代码:

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void* Print(void* args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&gmutex);
        std::cout << "我是新线程: " << name <<std::endl;
        pthread_cond_wait(&gcond, &gmutex);
        pthread_mutex_unlock(&gmutex);
        sleep(1);
    }
    return nullptr;
}

int main()
{
    pthread_t tids[4];

    for(int i = 0; i < 4;i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread-%d", i + 1);
        pthread_create(tids + i, nullptr, Print, (void*)name);
    }

    while(true)
    {
        //pthread_cond_signal(&gcond);
        pthread_cond_broadcast(&gcond);
        sleep(1);
    }

    //控制其它线程
    for(int i = 0;i < 4;i++)
    {
        pthread_join(tids[i], nullptr);
    }

    return 0;
}

访问临界资源,必然在临界资源内访问,判断临界资源是否就绪,本质也是在访问临界资源,所以这个判断过程也必须在临界区内完成,因此,判断代码块中的等待也必须在临界区中。

在加锁和解锁之间等到要将锁传进去,这是因为wait等待的时候,是在临界区内部等待的,若直接阻塞,会和锁进行阻塞,其它线程也得不到锁。因此,需要将锁传进去,让pthread_cond_wait自动释放_mutex锁,醒来的时候,是在临界区内部醒来,会让pthread_cond_wait自动竞争并获取锁。

POSIX信号量

概念

之前在进程通信的话题中,我们介绍过,当资源只有一份时,对应的信号量为二元信号量,这时为互斥。POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但 POSIX可以用于线程间同步。

相关接口

信号量的相关接口如下所示:

cpp 复制代码
#include <semaphore.h>

//初始化
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:
 pshared:0表⽰线程间共享,⾮零表⽰进程间共享
 value:信号量初始值

//销毁
int sem_destroy(sem_t *sem);

//锁信号量:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()

//发布信号量:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()

生产者消费者模型

概念

生成者消费者模型是多线程协同的一种模式,该模式是通过⼀个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区, 平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给⽣产者和消费者解耦的。如下图所示:

仓库就是一个临界资源,而生产者线程会往仓库中添加资源,消费者会从仓库中取走资源,在这个过程中,要协同好生产者和生产者、消费者和消费者以及生产者和消费者之间的关系。生产者和生产者之间、消费者和消费者之间主要是互斥关系,生产者和消费者是同步关系,只有生产者提供资源后,消费者才能获取资源。同时,生产者和消费者也会存在互斥关系,这是为了保持资源的的安全性,而资源安全性要通过互斥的方式来实现。

简单来说,消费者生产者模型就是个"321模型",3指的是三种关系,分别为:生产者和生产者、消费者和消费者以及生产者和消费者;2指的是两种角色,分别为生产者和消费者线程,1指的是一个交易场所,这个交易场所通常由指定的数据结构来承担。

阻塞队列

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

由于阻塞队列被我们当成一个整体来使用,即临界资源被我们当做一个完整的资源,要么不用,要用就必须全部拥有。

单生产单消费的简单实现如下:

cpp 复制代码
#ifndef __BLOCK_QUEUE_H
#define __BLOCK_QUEUE_H

#include <iostream>
#include <queue>
#include "Mutex.hpp"
#include "Cond.hpp"

const int defaultcap = 5;

template <class T>
class BlockQueue
{
public:
    BlockQueue(int cap = defaultcap) : _cap(cap)
    {
        sleep_productor_num = 0;
        sleep_consumer_num = 0;
    }

    void Enqueue(T &in)
    {
        {
            LockGuard lockguard(_mutex);
            // 为了增强代码的鲁棒性,将if变为while
            //1. 过量唤醒信息,2. 函数调用失败 3.伪唤醒(操作系统唤醒)
            while (_bq.size() == _cap)
            {
                sleep_productor_num++;
                _productor_cond.Wait(_mutex);
                sleep_productor_num--;
            }

            _bq.push(in);
            if (sleep_consumer_num > 0)
                _consumer_cond.Signal();
        }
    }

    void Pop(T *out)
    {
        {
            LockGuard lockguard(_mutex);
            while (_bq.empty())
            {
                sleep_consumer_num++;
                _consumer_cond.Wait(_mutex);
                sleep_consumer_num--;
            }

            *out = _bq.front();
            _bq.pop();
            if (sleep_productor_num > 0)
                _productor_cond.Signal();
        }
    }

    ~BlockQueue()
    {
    }

private:
    std::queue<T> _bq;
    int _cap;

    Mutex _mutex;
    Cond _consumer_cond;
    Cond _productor_cond;

    int sleep_productor_num;
    int sleep_consumer_num;
};

#endif

环形缓冲区

环形缓冲区类似于环形队列,通过信号量和环形缓冲区,也可以解决生产者消费者问题。当使用多线程访问环形队列时,当队列为空或者为满的时候,会涉及到只让谁访问和谁先访问的问题,前者是个互斥问题,后者是个同步问题,如下所示:

在设计环形队列的过程中,要注意以下几个问题:

  1. 当队列为空时,要让数据入队,也就是tail对应线程工作;当队列满时,要让数据出队,也就是head对应线程要工作,tail对应的线程相当于生产者角色,head对应的线程相当于消费者角色。
  2. 不为空且不为满时,可以并发生产和消费。
  3. 生产者不能把消费者套一个圈后继续访问。
  4. 消费者不能超过生产者。
  5. 从生产者角度来说,空格是资源,从消费者来说,数据是资源,因此要用两个信号量来描述这两种资源。

从生产者角度和消费者来说,它们执行任务的伪代码如下:

bash 复制代码
sem_t blank_sem = N;
sem_t data_sem = 0;

Productor:
    //生产数据
    P(blank_sem); //申请信号量
    
    //生产数据
    ring[tail++] = data;
    tail %= N;
    V(data_sem); //释放信号量

Consumer:
    //消费数据
    P(data_sem); //申请信号量
    //消费数据
    out = ring[head++];
    head %= N;
    V(blank_sem);//释放信号量

环形缓冲区的简单实现如下:

cpp 复制代码
//Sem.hpp
#ifndef __SEM_HPP
#define __SEM_HPP

#include <iostream>
#include <semaphore.h>

class Sem
{
public:
    Sem(int init_val)
    {
        if (init_val >= 0)
        {
            int n = sem_init(&_sem, 0, init_val);
            (void)n;
        }
    }

    void P()
    {
        int n = sem_wait(&_sem);
        (void)n;
    }

    void V()
    {
        int n = sem_post(&_sem);
        (void)n;
    }

    ~Sem()
    {
        int n = sem_destroy(&_sem);
        (void)n;
    }

private:
    sem_t _sem;
};

#endif
cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"
#include "Mutex.hpp"

const int defaultcap = 5;

template <class T>
class RingQueue
{
public:
    RingQueue(int cap = defaultcap)
        : _cap(cap),
          _rq(cap),
          _consumer_step(0),
          _productor_step(0),
          _blank_sem(cap),
          _data_sem(0)
    {
    }

    void Enqueue(T &in) // 生产者调用
    {
        // 1.预定资源
        _blank_sem.P();
        {
            LockGuard lockguard(_pmutex); // 排队
            // 2.找位置生产
            _rq[_productor_step++] = in;
            _productor_step %= _cap;
        }
        // 3.释放资源
        _data_sem.V();
    }

    void Pop(T *out) // 消费者调用
    {
        _data_sem.P();
        {
            LockGuard lockguard(_cmutex);
            *out = _rq[_consumer_step++];
            _consumer_step %= _cap;
        }
        _blank_sem.V();
    }

    ~RingQueue()
    {
    }

private:
    int _cap;           // 环形队列容量
    std::vector<T> _rq; // 环形队列

    int _consumer_step;  // 消费位置
    int _productor_step; // 生产位置

    Sem _blank_sem; // 格子资源,生产者
    Sem _data_sem;  // 数据信号量,消费者

    Mutex _cmutex;
    Mutex _pmutex;
};

日志

在操作系统中,每输入一条指令,都会有一个执行该指令的结果,这个结果会被保存在某个文件中被当做相关的日志信息,如下所示:

日志是计算机中记录系统和软件运行中发生事件的文件,主要用于监控运行状态、记录异常信息、帮助快速定位问题并支持程序员进行问题修复的工具。日志必须包含时间戳、日志等级和日志内容。

线程池的一个简单实现如下所示:

cpp 复制代码
#ifndef __LOGGER_HPP
#define __LOGGER_HPP
#include <iostream>
#include <string>
#include <sys/time.h>
#include <ctime>
#include <cstdio>
#include "Mutex.hpp"
#include <filesystem>
#include <fstream>
#include <memory>
#include <unistd.h>
#include <sstream>

namespace NS_LOG_MODULE
{
    enum class LogLevel
    {
        INFO,
        WARNING,
        ERROR,
        FATAL,
        DEBUG
    };

    std::string LogLevel2Message(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        case LogLevel::DEBUG:
            return "DEBUG";
        default:
            return "UNKNOWN";
        }
    }

    // 日期+时间
    std::string GetCurrentTime()
    {
        struct timeval current_time;
        int n = gettimeofday(&current_time, nullptr);
        (void)n;

        struct tm struct_time;
        localtime_r(&(current_time.tv_sec), &struct_time); // r代表可重入函数
        char timestr[128];
        snprintf(timestr, sizeof(timestr), "%04d-%02d-%02d %02d:%02d:%02d.%ld",
                 struct_time.tm_year + 1900,
                 struct_time.tm_mon + 1,
                 struct_time.tm_mday,
                 struct_time.tm_hour,
                 struct_time.tm_min,
                 struct_time.tm_sec,
                 current_time.tv_usec);

        return timestr;
    }

    // 输出------刷新策略
    //  1. 显示器打印
    //  2. 文件写入

    // 日志生成
    //  1.构建日志字符串
    //  2.根据不同策略,进行刷新

    // 策略模式
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 控制台日志刷新策略,日志将来要向显示器打印
    class ConsoleStrategy : public LogStrategy
    {
    public:
        void SyncLog(const std::string &message) override
        {
            LockGuard lockguard(_mutex);
            std::cerr << message << std::endl;
        }

        ~ConsoleStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    const std::string defaultpath = "./log";
    const std::string defaultfilename = "log.txt";

    // 文件策略
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultfilename)
            : _logpath(path),
              _logfilename(name)
        {
            LockGuard lockguard(_mutex);
            if (std::filesystem::exists(_logpath))
                return;
            try
            {
                std::filesystem::create_directories(_logpath);
            }
            catch (const std::filesystem::filesystem_error &e)
            {
                std::cerr << e.what() << "\n";
            }
        }

        void SyncLog(const std::string &message) override
        {
            {
                LockGuard lockguard(_mutex);
                if (!_logpath.empty() && _logpath.back() != '/')
                {
                    _logpath += "/";
                }
                std::string targetlog = _logpath + _logfilename;
                std::ofstream out(targetlog, std::ios::app); // 追加方式写入
                if (!out.is_open())
                {
                    return;
                }
                out << message << "\n";
                out.close();
            }
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _logpath;
        std::string _logfilename;
        Mutex _mutex;
    };

    // class FileLogLevelStrategy: public LogStrategy
    // {

    // };

    // 日志类
    // 构建日志字符串
    class Logger
    {
    public:
        Logger()
        {
            UseConsoleStrategy();
        }

        void UseConsoleStrategy()
        {
            _strategy = std::make_unique<ConsoleStrategy>();
        }

        void UseFileStrategy()
        {
            _strategy = std::make_unique<FileLogStrategy>();
        }

        // 内部类,标识一条完整的日志信息
        // 一条完整的日志信息 = 左半固部分定的部分 + 右半部分不固定部分
        // LogMessage 以RAII风格进行刷新
        class LogMessage
        {
        public:
            LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
                : _level(level),
                  _curr_time(GetCurrentTime()),
                  _pid(getpid()),
                  _filename(filename),
                  _line(line),
                  _logger(logger)
            {
                // 先构建左半部分
                std::stringstream ss;
                ss << "[" << _curr_time << "]"
                   << "[" << LogLevel2Message(_level) << "]"
                   << "[" << _pid << "]"
                   << "[" << _filename << "]"
                   << "[" << _line << "]"
                   << " - ";
                _loginfo = ss.str();
            }

            template <class T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _loginfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._strategy)
                {
                    _logger._strategy->SyncLog(_loginfo);
                }
            }

        private:
            LogLevel _level;
            std::string _curr_time;
            pid_t _pid;
            std::string _filename;
            int _line;
            std::string _loginfo; // 一条完整的日志信息
            Logger &_logger;      // 一个引用,引用外部Logger类对象,方便后续进行策略式刷新
        };

        // void Debug(const std::string &message)
        // {
        //     if (_strategy != nullptr)
        //     {
        //         _strategy->SyncLog(message);
        //     }
        // }

        LogMessage operator()(LogLevel level, std::string filename, int line)
        {
            return LogMessage(level, filename, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _strategy; // 刷新策略
    };

    // 日志对象,全局使用
    Logger logger;

#define ENABLE_CONSOLE_LOG_STRATEGY() logger.UseConsoleStrategy();
#define ENABLE_FILE_LOG_STRATEGY() logger.UseFileStrategy();

#define LOG(level) logger(level, __FILE__, __LINE__)

}

#endif

线程池

概念

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

线程池的示意图如下:

实现

线程池的简单实现如下:

cpp 复制代码
#pragma once

#include <iostream>
#include <vector>
#include <queue>
#include "Logger.hpp"
#include "Thread.hpp"
#include "Mutex.hpp"
#include "Cond.hpp"

namespace NS_THREAD_POOL_MODULE
{
    using namespace NS_LOG_MODULE;
    using namespace NS_THREAD_MODULE;

    const int defaultnum = 5;

    // void Test()
    // {
    //     while (true)
    //     {
    //         char name[128];
    //         pthread_getname_np(pthread_self(), name, sizeof(name));
    //         LOG(LogLevel::DEBUG) << "我是一个线程,我正在运行: " << name;
    //         sleep(1);
    //     }
    // }

    template <class T>
    class ThreadPool
    {
    private:
        void HandlerTask()
        {
            char name[128];
            pthread_getname_np(pthread_self(), name, sizeof(name));
            while (true)
            {
                T task;
                {
                    // 保护临界区
                    LockGuard lockguard(_mutex);

                    // 检测任务。不休眠:1.队列不为空 2.线程池退出 -> 休眠:队列为空 && 线程池不退出
                    while (_tasks.empty() && _isrunning)
                    {
                        // 没有任务,休眠
                        _slaver_sleeper_count++;
                        _cond.Wait(_mutex);
                        _slaver_sleeper_count--;
                    }
                    // 线程池退出
                    // 1.线程池退出 && _tasks empty
                    if (!_isrunning && _tasks.empty())
                    {
                        _mutex.Unlock();
                        break;
                    }

                    // 有任务,取任务 本质:把任务由公共变成私有
                    task = _tasks.front();
                    _tasks.pop();
                }

                // 处理任务, 约定
                // 处理任务不需要再临界区内部处理
                LOG(LogLevel::INFO) << name << "处理任务";
                task();
                LOG(LogLevel::DEBUG) << task.Result();
            }

            // 线程退出
            LOG(LogLevel::INFO) << name << " quit...";
        }

    public:
        ThreadPool(int slaver_num = defaultnum)
            : _isrunning(false),
              _slaver_sleeper_count(0),
              _slaver_num(slaver_num)
        {
            for (int idx = 0; idx < _slaver_num; idx++)
            {

                // 做法一
                _slavers.emplace_back([this]()
                                      { this->HandlerTask(); });

                // 做法二
                //  auto f = std::bind(&ThreadPool::HandlerTask, this);
                //  _slavers.emplace_back(f);
            }
        }

        void Start()
        {
            if (_isrunning)
            {
                LOG(LogLevel::WARNING) << "Thread Pool Is Already Running";
                return;
            }

            _isrunning = true;
            for (auto &slave : _slavers)
            {
                slave.Start();
            }
        }

        void Wait()
        {
            for (auto &slave : _slavers)
            {
                slave.Join();
            }
        }

        void Stop()
        {
            // version1
            // if (!_isrunning)
            // {
            //     char name[128];
            //     pthread_getname_np(pthread_self(), name, sizeof(name));
            //     LOG(LogLevel::WARNING) << "Thread Pool Is Not Running " << name;
            //     return;
            // }
            // for (auto &slave : _slavers)
            // {
            //     slave.Die();
            // }
            // _isrunning = false;

            // version2
            // 1._isrunning = false
            // 2.处理完成tasks所有的任务
            // 线程状态:休眠,正在处理任务 -> 让所有线程全部唤醒
            // Handler Task自动break
            _mutex.Lock();
            _isrunning = false;
            if (_slaver_sleeper_count > 0)
            {
                _cond.BroadCast();
            }
            _mutex.Unlock();
        }

        void Enqueue(T in)
        {
            _mutex.Lock();

            _tasks.push(in);
            if (_slaver_sleeper_count > 0)
            {
                _cond.Signal();
            }

            _mutex.Unlock();
        }

        ~ThreadPool()
        {
        }

    private:
        bool _isrunning;
        std::vector<Thread> _slavers;
        std::queue<T> _tasks; // 任务队列,临界资源
        Mutex _mutex;
        Cond _cond;
        int _slaver_sleeper_count; // 休眠的线程个数
        int _slaver_num;
    };
}

单例模式

单例模式指的是在代码中只能让某一个类对象存在一份的模式。常见的单例模式实现方式有饿汉实现方式懒汉实现方式

以洗碗为例子,吃完饭, 立刻洗碗, 这种就是饿汉方式,因为下一顿吃的时候可以立刻拿着碗就能吃饭;吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式。懒汉方式最核心的思想是"延时加载",从而能够优化服务器的启动速度。

饿汉方式的一个例子如下:

cpp 复制代码
template <typename T>

class Singleton {
    static T data;
public:
    static T* GetInstance() {
        return &data;
    }
};

懒汉方式的一个例子如下:

cpp 复制代码
template <typename T>

class Singleton {
    static T* inst;
public:
    static T* GetInstance() {
        if (inst == NULL) {
            inst = new T();
        }
        return inst;
    }
};

线程安全和可重入问题

线程安全问题,就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。

重入是指同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入, 我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

线程安全,侧重点在线程的并发执行上;而重入与不可重入问题,是函数的特点。但是二者会有部分的交集,因为线程也会调用函数。如果函数是可重入的,那么就是线程安全的,可重入函数是线程安全函数的一种。

相关推荐
2401_838472512 小时前
C++模拟器开发实践
开发语言·c++·算法
3108748762 小时前
0005.C/C++学习笔记5
c语言·c++·学习
s1hiyu2 小时前
实时控制系统验证
开发语言·c++·算法
tod1132 小时前
Makefile进阶(上)
linux·运维·服务器·windows·makefile·进程
楼田莉子2 小时前
C++现代特性学习:C++14
开发语言·c++·学习·visual studio
阳光九叶草LXGZXJ2 小时前
达梦数据库-学习-50-分区表指定分区清理空洞率(交换分区方式)
linux·运维·数据库·sql·学习
2301_765703142 小时前
C++代码复杂度控制
开发语言·c++·算法
zbliquan2 小时前
SS928v100远程ubuntu交叉编译开发环境搭建
linux·运维·ubuntu
m0_708830962 小时前
C++中的享元模式实战
开发语言·c++·算法