< Linux > 多线程(单例模式、线程安全问题、读者写者问题)

目录

1、单例模式

        饿汉方式实现单例模式

        懒汉方式实现单例模式

        单例模式实现线程池(懒汉模式)

2、STL、智能指针、线程安全

        STL中的容器不是线程安全的

        智能制造是否是线程安全的

        其它常见的各种锁

3、读者写者问题

        读写锁的函数接口

        代码实现读者写者优先问题

        读者加锁和写者加锁的基本原理


1、单例模式

单例模式的概念:

  • 单例(Singleton)模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的类一个类只有一个实例。即一个类只有一个对象实例

单例模式的使用场景:

  1. 语义上只需要一个
  2. 该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中存在冗余数据;

单例模式有两种实现模式:

  • 饿汉模式:吃完饭,立刻洗碗,这种就是饿汉方式。因为下一顿吃的时候可以立刻拿着碗就能吃饭。
  • 懒汉模式:吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗,就是懒汉方式。懒汉模式最核心的思想是 "延时加载"。(例如我们之前所学过的写时拷贝)从而能够优化服务器的启动速度。

饿汉方式实现单例模式

该模式在类被加载时就会实例化一个对象,具体代码如下:

template  
class Singleton 
{ 
private:
    static Singleton data;//饿汉模式,在加载的时候对象就已经存在了 
public: 
    static Singleton* GetInstance() 
    { 
        return &data; 
    } 
};

该模式能简单快速的创建一个单例对象,而且是线程安全的(只在类加载时才会初始化,以后都不会)。但它有一个缺点,就是不管你要不要都会直接创建一个对象,会消耗一定的性能(当然很小很小,几乎可以忽略不计,所以这种模式在很多场合十分常用而且十分简单) 


懒汉方式实现单例模式

该模式只在你需要对象时才会生成单例对象(比如调用GetInstance方法) 

template  
class Singleton 
{ 
private:
    static Singleton* inst; //懒汉式单例,只有在调用GetInstance时才会实例化一个单例对象
public: 
    static Singleton* GetInstance() 
    { 
        if (inst == NULL) 
        { 
            inst = new Singleton(); 
        }
    return inst; 
    } 
};

看上去,这段代码没什么明显问题,但它不是线程安全的。假设当前有多个线程同时调用GetInstance()方法,由于当前还没有对象生成,那么就会由多个线程创建多个对象。

// 懒汉模式, 线程安全 
template  
class Singleton 
{
private: 
    static Singleton* inst; 
    static std::mutex lock; 
public: 
    static T* GetInstance() 
    { 
        if (inst == NULL) // 双重判定空指针, 降低锁冲突的概率, 提高性能 
        {                 
            lock.lock();  // 使用互斥锁, 保证多线程情况下也只调用一次 new
            if (inst == NULL) 
            { 
                inst = new T(); 
            }
            lock.unlock(); 
        }
    return inst;
    } 
};

这种形式是在懒汉方式的基础上增加的,当多个线程调用GetInstance方法时,此时类中没有对象,那么多个线程就会来到锁的位置,竞争锁。必然只能有一个线程竞争锁成功,此时再次判断有没有对象被创建(就是inst指针),如果没有就会new一个对象,如果有就会解锁,并返回已有的对象;总的来说,这样的形式使得多个线程调用GetInstance方法时,无论成功与否,都会有返回值;


单例模式实现线程池(懒汉模式)

我们在上篇博文原有线程池的基础上做修改,改成单例模式版本。变动如下:

ThreadPool.hpp文件:

  • 将构造函数私有化,利用delete删除拷贝构造和拷贝复制函数。在类内定义线程池类型的static指针变量instance,类外初始化此static变量。为了在类外获得此static静态变量instance,在内部实现一个getInstance函数,需要对其加锁,直接复用先前实现的RAII风格的加锁解锁方式。我们可以使用prctl来指定创建线程的名字,便于后续使用ps命令查看相关信息。
#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include  //智能指针
#include 
#include "Log.hpp"
#include "Lock.hpp"
using namespace std;

int gThreadNum = 5; // 线程池的容量
template 
class ThreadPool
{
private:
    // 构造函数
    ThreadPool(int threadNum = gThreadNum)
        : threadNum_(threadNum), isStart_(false)
    {
        assert(threadNum_ > 0);
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool &) = delete;
    void operator=(const ThreadPool &) = delete;

public:
    static ThreadPool *getInstance()
    {
        static Mutex mutex;
        if (nullptr == instance) // 仅仅是过滤重读的判断
        {
            LockGuard lockguard(&mutex); // RAII风格的加锁解锁方式
            if (nullptr == instance)
            {
                instance = new ThreadPool();
            }
        }

        return instance;
    }

    // 线程函数(注意这里是类内成员函数,具有隐含的this指针, 而定义成static成员函数,则没有this指针)
    static void *threadRoutine(void *args)
    {
        pthread_detach(pthread_self());
        ThreadPool *tp = static_cast *>(args);
        prctl(PR_SET_NAME, "follower");

        while (1)
        {
            tp->lockQueue();
            while (!tp->haveTask())
            {
                // 没有任务
                tp->waitForTask();
            }
            // 有任务, 这个任务被拿到线程的上下文中
            T t = tp->pop();
            tp->unlockQueue();

            // for debug
            int one, two;
            char oper;
            t.get(one, two, oper);
            // 所有的任务都必须有一个run方法
            Log() << "新线程完成计算任务: " << one << oper << two << "=" << t.run() << endl;
        }
    }
    // 让线程池启动, 让所有线程跑起来
    void start()
    {
        assert(!isStart_);
        for (int i = 0; i < threadNum_; i++)
        {
            // 创建线程
            pthread_t temp;
            pthread_create(&temp, nullptr, threadRoutine, this);
        }
        isStart_ = true;
    }
    // 向任务队列里放任务(主线程调用)
    void push(const T &in)
    {
        lockQueue();
        taskQueue_.push(in);
        choiceThreadForHandler();
        unlockQueue();
    }
    // 析构函数
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }

private:
    // 加锁
    void lockQueue()
    {
        pthread_mutex_lock(&mutex_);
    }
    // 解锁
    void unlockQueue()
    {
        pthread_mutex_unlock(&mutex_);
    }
    // 检测是否有任务
    bool haveTask()
    {
        return !taskQueue_.empty();
    }
    // 等待任务
    void waitForTask()
    {
        pthread_cond_wait(&cond_, &mutex_);
    }
    // 选择某一个线程去执行
    void choiceThreadForHandler()
    {
        pthread_cond_signal(&cond_);
    }
    // 从任务队列里获取任务(线程池中的线程调用)
    T pop()
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }

private:
    bool isStart_;          // 表示当前线程是否已经启动
    int threadNum_;         // 线程池中线程的数量
    queue taskQueue_;    // 任务队列
    pthread_mutex_t mutex_; // 让线程互斥的获得任务队列里的内容
    pthread_cond_t cond_;   // 让线程没有任务时在条件变量下等待,有任务时再唤醒线程

    static ThreadPool *instance; // 定义线程池指针
};

template 
ThreadPool *ThreadPool::instance = nullptr;

Task.hpp文件:

#pragma once
#include 
#include 

class Task
{
public:
    Task()
        : elemOne_(0), elemTwo_(0), operator_('0')
    {}
    Task(int one, int two, char op)
        : elemOne_(one), elemTwo_(two), operator_(op)
    {}

    // 仿函数
    int operator()()
    {
        return run();
    }

    // 执行任务
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "div zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }
        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "mod zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            std::cout << "非法操作: " << operator_ << std::endl;
            break;
        }
        return result;
    }
    // 获取参与计算的三个操作数
    int get(int &e1, int &e2, char &op)
    {
        e1 = elemOne_;
        e2 = elemTwo_;
        op = operator_;
    }

private:
    int elemOne_;
    int elemTwo_;
    char operator_; // 具体的运算符号
};

Log.hpp文件:

#pragma once
#include 
#include 
#include 

std::ostream &Log()
{
    std::cout << "Fot Debug |" << " timestamp: " << (uint64_t)time(nullptr) << " | " << "Thread[" << pthread_self() << "] | ";
    return std::cout;
}

Lock.hpp文件:

#pragma once
#include 
#include 
using namespace std;

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&lock_, nullptr);
    }
    void lock()
    {
        pthread_mutex_lock(&lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(&lock_);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&lock_);
    }
private:
    pthread_mutex_t lock_;
};

class LockGuard
{
public:
    LockGuard(Mutex *mutex)
        : mutex_(mutex)
    {
        mutex_->lock();
        cout << "加锁成功..." << endl;
    }
    ~LockGuard()
    {
        mutex_->unlock();
        cout << "解锁成功..." << endl;
    }
private:
    Mutex *mutex_;
};

ThreadPoolTest.cc文件:

#include "ThreadPool.hpp"
#include "Task.hpp"
#include 

int main()
{
    prctl(PR_SET_NAME, "master");

    const string operators = "+-*/%";
    unique_ptr> tp(ThreadPool::getInstance());
    tp->start();

    // 定义一个随机数
    srand((unsigned long)time(nullptr) ^ getpid() ^ pthread_self());

    // 派发任务的线程
    while (true)
    {
        // 构建任务
        int one = rand() % 50;
        int two = rand() % 10;
        char oper = operators[rand() % operators.size()];
        Log() << "主线程派发计算任务: " << one << oper << two << "=?" << endl;
        Task t(one, two, oper);
        // 派发任务
        tp->push(t);
        sleep(1);
    }
    return 0;
}

Makefile文件:

CC=g++
FLAGS=-std=c++11
LD=-lpthread
bin=threadpool
src=ThreadPoolTest.cc

$(bin):$(src)
	$(CC) -o $@ $^ $(LD) $(FLAGS)
.PHONY:clean
clean:
	rm -f $(bin)

测试结果:

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第1张图片

我们使用如下指令复制观察现象:

[xzy@ecs-333953 threadpool]$ ps -aL | grep -E 'master|follower'

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第2张图片


2、STL、智能指针、线程安全

STL中的容器不是线程安全的

原因如下:

  • STL 的设计初衷是将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。
  • 而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。
  • 因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全,智能指针是否是线程安全的。

智能制造是否是线程安全的

  • 对于 unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题.
  • 对于 shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证 shared_ptr 能够高效,原子的操作引用计数。

其它常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 挂起等待锁:当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁。当临界区运行的时间较长时,我们一般使用挂起等待锁。我们先让线程PCB加入到等待队列中等待,等锁被释放时,再重新申请锁。之前所学的互斥锁就是挂起等待锁 
  • 自旋锁:当某个线程没有申请到锁的时候,该线程不会被挂起,而是每隔一段时间检测锁是否被释放。如果锁被释放了,那就竞争锁;如果没有释放,过一会儿再来检测。如果这里使用挂起等待锁,可能线程刚加入等待队列,锁就被释放了,因此,当临界区运行的时间较短时,我们一般使用自旋锁。
pthread_spin_lock

自旋锁的函数接口只需要把mutex变成spin即可,这里不过多演示。


3、读者写者问题

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。

读和写的行为:

当前锁状态 读锁请求 写锁请求
无锁 可以 可以
读锁 可以 阻塞
写锁 阻塞 阻塞
  • 注意:写独占,读共享,读锁优先级高

对比生产者消费者模型:

生产者消费者模型中,我们说到过321原则:

  • 三种关系:生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥、同步)
  • 两种角色:生产者和消费者(通常是由线程承担的)
  • 一个交易场所:通常是指内存中特定的一种内存结构(数据结构)

在讲解读者写者问题前,来看如下的一个例子:

  • 比如一个人在画画,可能会有很多人在欣赏,在他画完之前,人们对其作品的各种猜测,在他画完前,人们读到的信息都不准确,因为该画家(写者)并没有画完,这就是读取数据不一致的问题。
  • 在他画画的时候,不能说另一个人也参与此作品的绘画过程,这是常识,每个人的画工,笔法都不一样,不能一同参与绘画。对于吃瓜群众(读者和读者之间),它们之间是没有关系的。

下面来总结读者写者的321原则:

  • 3种关系:写者和写者(互斥),读者和读者(没有关系),读者和写者(互斥关系)
  • 2种角色:读者、写者
  • 1个交易场所:读写场所

问:为什么读者和读者之间没有像消费者和消费者之间的互斥关系?

  • 因为消费者会把数据拿走,而读者不会。这也是读者写者 vs 生产者消费者的本质区别。

总结:一般读者很多(n),写者很少(1),它们对应的操作伪代码如下

读者              写者
加读锁            加写锁
读取内容          写入修改内容
释放锁            释放锁

读写锁的函数接口

定义读写锁变量

pthread_rwlock_t rwlock;

初始化读写锁

动态初始化:pthread_rwlock_init

#include 
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

参数:

  • rwlock: 读写锁的变量的地址
  • attr:属性设置为NULL即可。

返回值:

  • 成功:0
  • 失败:非0

静态初始化:PTHREAD_RWLOCK_INITIALIZER

pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

上读锁pthread_rwlock_rdlock

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

参数:

  • rwlock:读写锁的变量的地址

返回值:

  • 成功:0
  • 失败:非0

上写锁pthread_rwlock_wrlock

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);

参数:

  • rwlock:读写锁的变量的地址

返回值:

  • 成功:0
  • 失败:非0

解锁pthread_rwlock_unlock

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

参数:

  • rwlock:读写锁的变量的地址

返回值:

  • 成功:0
  • 失败:非0

销毁读写锁pthread_rwlock_destroy

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

参数:

  • rwlock:读写锁的变量的地址

返回值:

  • 成功:0
  • 失败:非0

代码实现读者写者优先问题

#include 
#include 
#include 
using namespace std;

int board = 0;

pthread_rwlock_t rw;

void *reader(void *args)
{
    const char *name = static_cast(args);
    sleep(2);
    cout << "sleep done" << endl;
    while (true)
    {
        // 给读者加锁
        pthread_rwlock_rdlock(&rw);
        cout << "reader read: " << board << endl;
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
void *writer(void *args)
{
    const char *name = static_cast(args);
    while (true)
    {
        // 给写者加锁
        pthread_rwlock_wrlock(&rw);
        board++;
        cout << "I am a writer" << endl;
        sleep(10);
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
int main()
{
    // 初始化读写锁
    pthread_rwlock_init(&rw, nullptr);

    pthread_t r1, r2, r3, r4, r5, r6, w;
    pthread_create(&r1, nullptr, reader, (void *)"reader1");
    pthread_create(&r2, nullptr, reader, (void *)"reader2");
    pthread_create(&r3, nullptr, reader, (void *)"reader3");
    pthread_create(&r4, nullptr, reader, (void *)"reader4");
    pthread_create(&r5, nullptr, reader, (void *)"reader5");
    pthread_create(&r6, nullptr, reader, (void *)"reader6");
    pthread_create(&w, nullptr, writer, (void *)"writer");

    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(r3, nullptr);
    pthread_join(r4, nullptr);
    pthread_join(r5, nullptr);
    pthread_join(r6, nullptr);
    pthread_join(w, nullptr);

    // 释放读写锁
    pthread_rwlock_destroy(&rw);
    return 0;
}

如上我用sleep函数控制读者先不读,让写者先进入,并待的时间长一点,当读者醒来的时候,一定是写者先拿到锁了,所以看到的现象应该是写者拿到锁进入临界区休眠了,读者醒来时再想来加锁是会阻塞挂住的,后续很长的时间写者什么都不干,读者也不会区读取:

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第3张图片

现在控制代码让读者先跑起来,让读者拿到锁后sleep休眠10s ,写者休眠1s后再启动。此时观察的现象是读者拿到锁进入临界区休眠了,写者醒来时是进不来的

#include 
#include 
#include 
using namespace std;

int board = 0;

pthread_rwlock_t rw;

void *reader(void *args)
{
    const char *name = static_cast(args);
    cout << "run..." << endl;
    while (true)
    {
        // 给读者加锁
        pthread_rwlock_rdlock(&rw);
        cout << "reader read: " << board << endl;
        sleep(10);
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
void *writer(void *args)
{
    const char *name = static_cast(args);
    sleep(1);
    while (true)
    {
        // 给写者加锁
        pthread_rwlock_wrlock(&rw);
        board++;
        cout << "I am a writer" << endl;
        sleep(10);
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
int main()
{
    // 初始化读写锁
    pthread_rwlock_init(&rw, nullptr);

    pthread_t r1, r2, r3, r4, r5, r6, w;
    pthread_create(&r1, nullptr, reader, (void *)"reader1");
    pthread_create(&r2, nullptr, reader, (void *)"reader2");
    pthread_create(&r3, nullptr, reader, (void *)"reader3");
    pthread_create(&r4, nullptr, reader, (void *)"reader4");
    pthread_create(&r5, nullptr, reader, (void *)"reader5");
    pthread_create(&r6, nullptr, reader, (void *)"reader6");
    pthread_create(&w, nullptr, writer, (void *)"writer");

    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(r3, nullptr);
    pthread_join(r4, nullptr);
    pthread_join(r5, nullptr);
    pthread_join(r6, nullptr);
    pthread_join(w, nullptr);

    // 释放读写锁
    pthread_rwlock_destroy(&rw);
    return 0;
}

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第4张图片

多个读者是可以并行运行的,下面修改代码来观察现象:

#include 
#include 
#include 
using namespace std;

int board = 0;

pthread_rwlock_t rw;

void *reader(void *args)
{
    const char *name = static_cast(args);
    cout << "run..." << endl;
    while (true)
    {
        // 给读者加锁
        pthread_rwlock_rdlock(&rw);
        cout << "reader read: " << board << "tid: " << pthread_self() << endl;
        sleep(10);
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
void *writer(void *args)
{
    const char *name = static_cast(args);
    sleep(1);
    while (true)
    {
        // 给写者加锁
        pthread_rwlock_wrlock(&rw);
        board++;
        cout << "I am a writer" << endl;
        sleep(10);
        // 释放锁
        pthread_rwlock_unlock(&rw);
    }
}
int main()
{
    // 初始化读写锁
    pthread_rwlock_init(&rw, nullptr);

    pthread_t r1, r2, r3, r4, r5, r6, w;
    pthread_create(&r1, nullptr, reader, (void *)"reader1");
    pthread_create(&r2, nullptr, reader, (void *)"reader2");
    pthread_create(&r3, nullptr, reader, (void *)"reader3");
    pthread_create(&r4, nullptr, reader, (void *)"reader4");
    pthread_create(&r5, nullptr, reader, (void *)"reader5");
    pthread_create(&r6, nullptr, reader, (void *)"reader6");
    pthread_create(&w, nullptr, writer, (void *)"writer");

    pthread_join(r1, nullptr);
    pthread_join(r2, nullptr);
    pthread_join(r3, nullptr);
    pthread_join(r4, nullptr);
    pthread_join(r5, nullptr);
    pthread_join(r6, nullptr);
    pthread_join(w, nullptr);

    // 释放读写锁
    pthread_rwlock_destroy(&rw);
    return 0;
}

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第5张图片


读者加锁和写者加锁的基本原理

读者写者的伪代码如下:

< Linux > 多线程(单例模式、线程安全问题、读者写者问题)_第6张图片

  • 一旦读者先进入了,readers计数器++了,只要读者不退出,写者无论如何都拿不到锁,只有当readers--到0的时候,写者才能进入写操作,

读者写者进行操作的时候,读者非常多,频率个别高,写者比较少,频率不高。写者随时都会来,但是和读者没有同步关系,只有等读者读完才轮到写者,所以会存在写者饥饿的问题。默认读者优先。所以读者写者同时到来的时候,我们会有两种优先级策略来解决读者写者问题:

  • 读者优先:读写同时到来时,让读者优先拿到锁,读者没读完就一直读,写者必须等读者读完才轮到自己
  • 写者优先:限制往后到来的其它读者先不要进入临界资源,等当前正在读取的人读完了,先让写者去写,写完再读

你可能感兴趣的:(Linux,linux,读者写者问题)