深入理解Linux多线程

致前行的人:

昨日渐多,明日愈少,今日还在,不要为成功而努力,要为做一个有价值的人而努力。人生道路上充满了坎坷,谁也不可能一帆风顺。只有在最困难的时刻,才能体会到无助的含义。

目录

1.理解页表

1.1.如何看待地址空间和页表

2.2页表是如何从虚拟地址转化为物理地址

2.线程概念

3.线程的特点

4.线程的优点

5.线程的缺点

6.线程异常

7.线程的用途

8.Linux下进程与线程

9.Linux线程控制

9.1.创建一批线程:

9.2.线程终止

9.3线程等待

9.4分离线程

9.5如何理解线程tid,线程栈

9.6对pthread库的封装

10.Linux线程互斥

10.1为什么存在线程互斥

10.2进程线程间的互斥相关概念

10.3实现互斥的接口

10.4互斥量实现原理探究

11.可重入VS线程安全

12.死锁

13.Linux线程同步

14.生产者消费者模型

14.1为何要使用生产者消费者模型

14.2生产者消费者模型

14.3生产者消费者模型优点

14.4基于BlockingQueue的生产者消费者模型

14.5如何理解生产者和消费者提高效率的问题

15.POSIX信号量

15.1之前代码中“不足”的地方

15.2什么是信号量

15.3信号量的特点

15.4如何使用信号量

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

16.线程池

16.1线程池的概念

16.2线程池实例

17.线程安全的单例模式

17.1什么是单例模式

17.2什么是设计模式

17.3单例模式

17.3.1饿汉方式实现单例模式

17.3.2懒汉方式实现单例模式

17.3.3饿汉模式与懒汉模式的优缺点

18.STL,智能指针和线程安全

18.1STL中的容器是否是线程安全的?

18.2智能指针是否是线程安全的?

19. 其他常见的各种锁

20. 读者写者模型

20.1读者写者模型概念理解

20.2读写锁

20.3读写锁加锁和解锁实现伪代码实例

21.总结


1.理解页表

1.1.如何看待地址空间和页表

1.地址空间是进程能看到的资源窗口

2.页表决定,进程真正拥有资源的情况

3.合理的地址空间+页表进行资源划分,就可以对一个进程的所有资源进行划分

2.2页表是如何从虚拟地址转化为物理地址

在之前的学习中,从虚拟地址空间到物理内存,页表映射如图所示:

在32位的操作系统中,地址编号从0~2^32,每一个地址占1byte,所以总共占4GB的空间,这也就是说页表在映射的过程中,也必须要有2^32个地址,假设一个页表中一个条目占6byte,总共就会占24GB,这样看来,如上图页表的映射显然是不合理的,那么页表是如何映射的呢?

真实的页表是通过建立索引的方式解决上述问题的,如何建立索引呢?

一个字节总共有32个bit位,操作系统将前10个bit位排列作为页目录,将中间10个bit位排列作为页表,将后面的12个bit位排列作为虚拟地址的业内偏移。

物理内存:是被划分为一个个的页框,每个页框占4byte的大小,所以磁盘向内存加载数据的时候也是以4kb为基本单位进行加载的

如图所示:

2.线程概念

通过之前的学习,我们已经知道进程的相关概念,如图所示:

当代码和数据加载到内存时,创建PCB,虚拟地址空间,然后通过页表进行映射,当创建多个进程的时候,操作系统为了维护每个进程的独立性,会为每个进程单独创建虚拟地址空间,页表等,其中,虚拟地址空间决定了进程能看到的”资源“。

相比于进程而言,线程是进程内的一个执行流,创建多个线程的时候,不会给每一个线程单独创建mm_struct和页表,而是指向进程所创建的mm_struct,然后通过mm_struct给每一个线程划分对应的资源,让线程去执行相应的任务。

如图所示:

为什么要这样设计线程呢?这是跟线程被创建的作用是相关的,一个线程被创建一定是为了去执行某个任务,这与进程的作用不谋而合,所以如果将线程设计的和进程一样,给每一个线程创建对应的对象加内核数据结构进行管理,这样的方式提高了代码的复杂性和进程与线程的高耦合性,所以在Linux中为了解决这个问题,线程是直接复用进程的PCB结构,所以在Linux中并没有存在真正意义上的线程,所谓的线程就是进程内的一个执行流,拥有该进程的一部分资源,每一个线程都是一个轻量级进程,站在CPU的角度线程是调度的基本单位。

在之前的学习中,我们知道进程=内核数据结构+代码和数据,对进程的学习是以单个执行流的方式,当了解了线程的概念之后,我们对进程又有了一个新的认识,进程是承担资源分配的基本实体。

总的来说,线程与进程的关系是线程是CPU调度的基本单位,进程是承担资源分配的基本实体,进程用来整体申请资源,线程从进程申请资源。

如图所示:

上面提到Linux中没有真正意义上的线程,所以Linux就无法直接创建线程的系统调用接口,而只能提供创建轻量级进程的接口,但是这对上层用户的使用是不友好的,用户希望以线程的概念对线程进行操作,为了解决这个问题,在轻量级进程和线程之间加入了一个第三方库pthread,让用户以“线程”的视角看待轻量级进程。

创建线程:

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                    void *(*start_routine) (void *), void *arg);

参数:

thread表示线程id.attr不关心设为nullptr,start_routine回调函数,用来让创建的线程执行任务,arg给该函数传递的参数

返回值:

On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
 

#include<iostream>
#include<pthread.h>
#include<cassert>
#include<unistd.h>
using namespace std;

//新线程
void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    (void)n;
    //主线程
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

编译运行:出现错误

原因:pthread是第三方库,所以在使用的时候必须要先链接:-lpthread 

g++ -o mythread mythread.cc -std=c++11 -lpthread

运行截图:

如何查看创建的新线程?

PID:进程标识符id,LWP线程标识符id,可以看到不同的线程PID都是相同的,这也证明了上面说的线程是进程内部的一个执行流,不同线程LWP是不相同的,也就说明了CPU是以线程为基本单位进行调度的 。

3.线程的特点

1.线程一旦被创建,几乎所有的资源都是共享的

int g_val = 0;
void fun()
{
    cout << "我是一个方法" << endl;
}
void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << g_val++ <<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    while(true)
    {
        cout << "我是主线程" << g_val++ << endl;
        fun();
        sleep(1);
    }
    return 0;
}

运行截图:

2.线程也有自己的私有资源

1.PCB属性私有

2.要有一定私有上下文结构

3.每个线程都有自己私有的栈结构

4.线程的优点

1.创建一个新线程的代价要比创建一个新进程小得多
2.与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

a.进程切换:切换页表 && 切换虚拟地址空间 && 切换PCB && 切换上下文

b.线程切换:切换PCB && 切换上下文

c.线程切换不需要切换cache,进程需要切换cache


操作系统为了提高程序的运行效率,在CPU内部集成了高速缓存,当从内存中获取数据的时候会一次性将访问数据周围的数据也拿到存储到cache中,下一次获取数据的寄存器首先会在高速缓存中查找,而高速缓存中存放了很多热点数据,大概率可以访问到,但是进程间切换是保证进程间的独立性,cache中的数据不是共享的,而线程之间数据是共享的,包括cache中的数据,所以线程切换比进程切换要做的工作少很多!

3.线程占用的资源要比进程少很多
4.能充分利用多处理器的可并行数量
5.在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

5.线程的缺点

性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
编写与调试一个多线程程序比单线程程序困难得多

6.线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cassert>
using namespace std;

void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << (const char*)args <<endl;
        //线程内部出现野指针,线程异常崩溃,进程也会崩溃
        int* p = nullptr;
        *p = 100;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,start_routine,(void*)"one thread");
    assert(n == 0);
    (void)n;
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

运行截图:

7.线程的用途

合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

8.Linux下进程与线程

进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器

errno
信号屏蔽字
调度优先级

进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
进程和线程的关系如下图:

9.Linux线程控制

9.1.创建一批线程:

写法1:

void* start_routine(void* args)
{
    while(true)
    {
        cout << "我是新线程" << (const char*)args <<endl;
        sleep(1);
    }
}
#define NUM 10
int main()
{
    for(size_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
        pthread_create(&tid,nullptr,start_routine,namebuffer);
    }
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

运行截图:

当主线程创建好新线程后,新线程的执行方法未来得及调用,主线程又创建新的线程,导致执行方法被覆盖,因为namebuffer缓冲区对每个线程都是共享的,所以看到只有一个线程执行方法被调用

写法2:

class ThreadData
{
public:
    pthread_t tid;
    char namebuffer[64];
};
void* start_routine(void* args)
{
    ThreadData* td = (ThreadData*)args;
    int cnt = 10;
    while(cnt--)
    {
        cout << "我是新线程" << td->namebuffer <<"cnt: " << cnt << endl;
        sleep(1);
    }
    delete td;
    return nullptr;
}
#define NUM 10
int main()
{
    vector<ThreadData*> threads;
    for(size_t i = 0; i < NUM; i++)
    {
        ThreadData* tid = new ThreadData();
        snprintf(tid->namebuffer,sizeof(tid->namebuffer),"%s:%d","thread",i);
        pthread_create(&tid->tid,nullptr,start_routine,tid);
        //创建好的线程结果保存下来
        threads.push_back(tid);
    }
    for(auto& e : threads)
    {
        cout << e->tid << e->namebuffer << "sucess" << endl;
    }
    while(true)
    {
        cout << "我是主线程" << endl;
        sleep(1);
    }
    return 0;
}

运行截图:

 注:

start_routine被10个线程调用,不同的执行流调用同一个函数,这个函数就是处于重入状态,在函数内部定义的变量,具有临时性,在多线程中仍然适用,也从侧面说明了每个线程都有自己独立的栈结构,所以数据不会产生二义性,所以start_routine是一个可重入函数。

9.2.线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:
1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
2. 线程可以调用pthread_ exit终止自己。
3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit函数

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel函数 

功能:取消一个执行中的线程
原型
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

9.3线程等待

1.获取新线程的退出信息

2.回收新线程对应的PCB等内核资源,防止内存泄漏。

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED(-1)。
3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。
4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

注:pthread_join:默认认为函数调用成功,不考虑异常退出的问题,所以在线程退出的时候不会获取到对应的信号,线程异常,收到信号,整个进程都会退出!

9.4分离线程

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

int pthread_detach(pthread_t thread);

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

pthread_detach(pthread_self());

pthread_self()获取线程的id

#include<iostream>
#include<pthread.h>
#include<stdio.h>
#include<cstring>
#include<string>
#include<assert.h>
#include<unistd.h>

using namespace std;
string changeId(const pthread_t& thread_id)
{
    char buf[64];
    snprintf(buf,sizeof buf,"0x%x",thread_id);
    return buf;
}
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    pthread_detach(pthread_self());//设置自己为分离状态
    int cnt = 5;
    while(cnt--)
    {
        string tid = changeId(pthread_self());
        cout << "我是新线程" << "线程名:"<< name << " " << tid <<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    cout << "新线程id: " << changeId(tid) << endl;
    //主线程id:
    string id = changeId(pthread_self());
    cout << "我是主线程" << "主线程id: " << id << endl;
    //一个线程默认是joinable的,如果设置了分离状态,就不会再进行等待了
    int wait = pthread_join(tid,nullptr);
    cout << "result: " << wait << ": " << strerror(wait) << endl;
    return 0;
}

运行截图:

9.5如何理解线程tid,线程栈

在Linux操作系统中没有真正意义上的线程,所谓的线程大部分复用了进程的属性,所以线程也被叫做轻量级进程,但是为了让上层用户体验感更好,将轻量级进程当作线程进行使用,所以针对轻量级进程做了进一步的封装,提供了pthread库,虽然线程复用了许多进程的属性,但是线程也有自己的属性,当用户像线程库申请线程资源时就会设计到资源管理的问题,而管理的方法是先描述在组织,所以每一个线程都有自己的结构体对象,而每个线程结构体对象是都保存在虚拟地址空间的共享区中,如图所示:

在共享区中保存了每个线程的属性,其中就包含了线程的tid,tid本质就是线程创建后被保存在共享区的起始地址,并且每个线程包含了自己独有的栈结构。 

对线程局部存储的理解:

//添加__thread选项,可以将一个内置类型设置为线程局部存储
__thread int g_val = 100;
string changeId(const pthread_t& thread_id)
{
    char buf[64];
    snprintf(buf,sizeof buf,"0x%x",thread_id);
    return buf;
}
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        string tid = changeId(pthread_self());
        cout << "我是新线程" << " 新线程id: " << tid <<" g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
        g_val++;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,start_routine,(void*)"thread 1");
    string id = changeId(pthread_self());
    while(true)
    {
        cout << "我是主线程" << " 主线程id: " << id << " g_val: "<<g_val<<" &g_val: "<<&g_val<<endl;
        sleep(1);
    }
    return 0;
}

运行截图:

9.6对pthread库的封装

#pragma once
#include<iostream>
#include<stdio.h>
#include<functional>
#include<string>
#include<pthread.h>

class Thread;
class Context
{
public:
    Thread* _this;
    void* _args;
public:
    Context():_this(nullptr),_args(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
public:
    typedef std::function<void*(void*)> func_t;
    const int num = 1024;
    Thread(func_t fun,void* args,int number):_fun(fun),_args(args)
    {
        char buffer[num];
        snprintf(buffer,sizeof buffer,"thread->%d",number);
        _name = buffer;
    }
    static void* start_routine(void* args)
    {
        Context* ctx = static_cast<Context*>(args);
        void* ret = ctx->_this->run(ctx->_args);
        delete ctx;
        return ret;
    }
    void start()
    {
        Context* ctx = new Context();
        ctx->_this = this;
        ctx->_args = _args;
        int n = pthread_create(&_tid,nullptr,start_routine,ctx);
        //编译器debug发布的时候存在,以release方式发布,assert就不存在了,n就是一个定义了但是没有使用的变量
        assert(n == 0);
        (void)n;//在有些编译器下可能会会有告警,所以用(void)n消除告警!
    }
    void join()
    {
        int n = pthread_join(_tid,nullptr);
        assert(n == 0);
        (void)n;
    }
    void *run(void* args)
    {
        return _fun(args);
    }
    ~Thread()
    {}
private:
    std::string _name;
    pthread_t _tid;
    func_t _fun;
    void* _args;
};

10.Linux线程互斥

10.1为什么存在线程互斥

当存在多个线程同时访问同一个资源的时候,就会存在数据发生异常。例如下面这段代码:

int tickets = 10000;
void *getTickets(void* args)
{
    string use_name = static_cast<const char*>(args);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1234);
            cout << use_name <<" 正在进行抢票 " << tickets-- << endl;
        }
        else
        {
            break;
        }
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(getTickets,(void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(getTickets,(void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(getTickets,(void*)"user3",3));
    thread1->start();
    thread2->start();
    thread3->start();
    thread1->join();
    thread2->join();
    thread3->join();
    return 0;
}

运行截图:

当多个用户抢票的时候,出现票数为-1的情况,此时就产生了数据异常,这是为什么呢?

操作系统为了解决这种问题,就提出了对线程进行加锁,让每个线程进行串行的访问公共资源--把这种方式就称为互斥!

10.2进程线程间的互斥相关概念

临界资源:多线程执行流访问共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

10.3实现互斥的接口

初始化互斥量
初始化互斥量有两种方法:
方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL


销毁互斥量
销毁互斥量需要注意:
        使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
        不要销毁一个已经加锁的互斥量
        已经销毁的互斥量,要确保后面不会有线程再尝试加锁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

改进上面的售票系统:

#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;

int tickets = 10000;
pthread_mutex_t lock;
void *getTickets(void* args)
{
    string use_name = static_cast<const char*>(args);
    while(true)
    {
        //加锁:
        pthread_mutex_lock(&lock);
        if(tickets > 0)
        {
            usleep(1234);
            cout << use_name <<" 正在进行抢票 " << tickets-- << endl;
            //解锁
            pthread_mutex_unlock(&lock);
        }
        else
        {
            //解锁
            pthread_mutex_unlock(&lock);
            break;
        }
        //让该线程生成订单,其它线程再申请抢票
        usleep(1234);
    }
}
int main()
{
    //初始化锁
    pthread_mutex_init(&lock,nullptr);
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,getTickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,getTickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,getTickets,(void*)"thread 3");
    pthread_create(&t4,nullptr,getTickets,(void*)"thread 4");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);
    //销毁锁
    pthread_mutex_destroy(&lock);
    return 0;
}

运行截图:

加上锁之后就解决了数据出现异常的问题! 

10.4互斥量实现原理探究

1.如何看待锁?

a. 锁,本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?

b. pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!

c. 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞!

d. 谁持有锁,谁进入临界区!

2..如何理解加锁和解锁的本质

因为线程在访问临界资源的时候可能会被切换,所以为了保证实现互斥锁操作,线程是带着锁一起被切走的,即使线程被切换,其它线程也就无法申请锁成功,操作系统实现这种方式的机制是提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

加锁的实现过程:

解锁的实现过程:

 3.对锁进行封装

#include<iostream>
#include<pthread.h>

using namespace std;

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock_p = nullptr):lock_p_(lock_p)
    {}
    void lock()
    {
        if(lock_p_)
            pthread_mutex_lock(lock_p_);
    }
    void unlock()
    {
        if(lock_p_)
            pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }
private:
    Mutex mutex_;
};

此时当使用锁的时候定义一个对象,然后将锁传给这个对象,让该对象自动处理该锁,我们把这种处理的方式称为RAII分格

11.可重入VS线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

常见的线程安全的情况

每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

类或者接口对于线程来说都是原子操作

多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构

常见可重入的情况

不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全联系

函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

12.死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件
        互斥条件:一个资源每次只能被一个执行流使用
        请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
        不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
        循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
        破坏死锁的四个必要条件
        加锁顺序一致
        避免锁未释放的场景
        资源一次性分配

避免死锁算法
死锁检测算法
银行家算法

13.Linux线程同步

条件变量
当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

图例解释:

条件变量函数 初始化

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);

测试代码: 

#include<iostream>
#include<string>
#include<pthread.h>
#include<unistd.h>
using namespace std;

//对条件变量和锁进行初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int tickets = 1000;
void* getTickets(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);
        cout << name << " -> " << tickets-- << endl;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t[5];
    for(int i = 0; i < 5; i++)
    {
        char *name = new char[64];
        snprintf(name,64,"Thread %d",i+1);
        pthread_create(t+i,nullptr,getTickets,name); 
    }
    while(true)
    {
        sleep(1);
        //唤醒线程:
        pthread_cond_signal(&cond);
    }
    for(int i = 0; i < 5; i++)
    {
        pthread_join(t[i],nullptr);
    }
    return 0;
}

运行截图:

从上图可以看出,当加上条件变量之后每个线程都在有序的抢票!
同步概念与竞态条件
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

14.生产者消费者模型

14.1为何要使用生产者消费者模型

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

14.2生产者消费者模型

总结为"321"原则:

3种关系:生产者和生产者之间存在互斥关系,消费者和消费者之间存在互斥关系,生产者和消费者之间存在互斥和同步关系

2种角色:生产者线程,消费者线程

1个交易场所:一段特定结构的缓冲区

 如图所示:

14.3生产者消费者模型优点

生产者和消费者解耦,生产者和消费者忙闲不均的问题,提高效率(支持并发)

在支持并发操作的时候,因为涉及到消费者和生产者访问公共资源的问题,所以必须要保证互斥,而互斥就会存在生产者或者消费者一直占用公共资源的问题,也就无法保证并发,所以为了解决这个问题就需要用到条件变量:

14.4基于BlockingQueue的生产者消费者模型

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

C++ queue模拟阻塞队列的生产消费模型

//BlockQueue.hpp:
#include<iostream>
#include<queue>
#include<pthread.h>

using namespace std;
const int maxCapacity = 5;
template<class T>
class BlockQueue
{
public:
    BlockQueue(const int& capacity = maxCapacity)
    :_maxCapacity(capacity)
    {
        pthread_mutex_init(&_mutex,nullptr);
        pthread_cond_init(&_pcond,nullptr);
        pthread_cond_init(&_ccond,nullptr);
    }

    void push(const T& in)
    {
        pthread_mutex_lock(&_mutex);
        //充当条件的是必须是while不能是if,因为可能会存在一次唤醒多个生产者线程
        //而此时只有一个空间,就可能存在异常
        while(is_full())
        {
            //pthread_cond_wait:这个函数的参数必须是正在使用的互斥锁
            //1.该函数调用的时候,会以原子性的方式,将锁释放,并将它挂起
            //2.该函数在唤醒的时候会自动重新获取你传入的锁
            pthread_cond_wait(&_pcond,&_mutex);
        }
        _q.push(in);
        //pthread_cond_signal:可以放在临界区的内部,也可以放在外部
        pthread_cond_signal(&_ccond);
        pthread_mutex_unlock(&_mutex);
    }
    void pop(T* out)
    {
        pthread_mutex_lock(&_mutex);
        //这里while判断和上面的同理
        while(is_empty())
        {
            pthread_cond_wait(&_ccond,&_mutex);
        }
        *out = _q.front();
        _q.pop();
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }
private:
    bool is_empty()
    {
        return _q.empty();
    }
    bool is_full()
    {
        return _q.size() == _maxCapacity;
    }
private:
    queue<T> _q;
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond;
    pthread_cond_t _ccond;
    int _maxCapacity;//队列中最大元素上线
};
//mainCP.cc
#include<iostream>
#include<pthread.h>
#include<ctime>
#include<unistd.h>
#include"BlockQueue.hpp"
using namespace std;

void* producer(void* bq_)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*> (bq_);
    while(true)
    {
        //生产活动:
        int data = rand() % 10 + 1;
        bq->push(data);
        cout << "生产数据:" << data << endl;
        sleep(1);
    }
}
void* cosumer(void* bq_)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*> (bq_);
    while(true)
    {
        //消费活动:
        int data;
        bq->pop(&data);
        cout << "消费数据:" << data << endl;
    }
}
int main()
{
    srand((unsigned int)time(nullptr));
    BlockQueue<int>* bq = new BlockQueue<int>();
    pthread_t p,c;
    pthread_create(&p,nullptr,producer,bq);
    pthread_create(&c,nullptr,cosumer,bq);

    pthread_join(p,nullptr);
    pthread_join(c,nullptr);
    delete bq;
    return 0;
}

运行截图:

14.5如何理解生产者和消费者提高效率的问题

生产者像阻塞队列中写入数据的前提是首先要获取数据,而获取数据可能花费比较长的时间,此时可以让多个线程并发的获取数据,然后像阻塞队列中写入数据,同理,对于消费者而言,从阻塞队列中获取数据之后可能会花费比较长的时间处理数据,此时就可以交给多个线程并发的处理数据,所以生产者和消费者模型提高效率是在获取数据和处理数据的时候!

如图所示:

15.POSIX信号量

15.1之前代码中“不足”的地方

一个线程在访问临界资源的时候,临界资源必须是要满足条件的,但是公共资源是否满足生产或者消费条件,提前是无法得知的,即使条件不满足,也只能是先加锁,再检测,再操作,再解锁,而每次申请锁是有消耗的。如何解决这种问题呢?

15.2什么是信号量

针对上面出现的问题,对公共资源的访问不再是整体加锁,而是对一块公共资源进行再划分,让多个线程可以并发访问对一块公共资源中的一块小的公共资源,而对小块公共资源的管理引入了信号量,信号量本质是一把计数器,用来衡量临界资源中资源数量多少的问题。

以后再访问临界资源的时候不需要上来直接加锁,然后检测,而是可以通过先申请信号量,申请成功就一定可以访问一块临界资源,申请失败就说明没有临界资源。

15.3信号量的特点

因为信号量是可以被多个线程申请的,所以信号量本身就是公共资源,所以对信号量的操作必须保证原子性,包含两种情况:

1.信号量--  —— 申请资源 —— 保证原子性(P操作)

2.信号量++ ——归还资源 —— 保证原子性(V操作)

上面这种操作方式被称为:PV原语

15.4如何使用信号量

初始化信号量

#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()

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

1.对于环形队列数据结构的理解

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

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

2.如何实现基于环形队列实现生产者消费者模型

环形队列实现生产消费模型需要满足的条件:

1.消费者必须再生产者的后面——保证消费者获取数据的时候一定有数据

2.消费者不能超过生产者一圈以上——保证消费者获取数据的时候一定有数据

3.生产者消费者处在同一块地方的情况:

        a.队列为空的时候——生产者先运行

        b.队列满的时候——消费者先行

如何保证环形队列实现生产消费模型的条件:通过信号来保证!

生产者定义一个信号量标识空间资源

消费者定义一个信号量标识数据资源

3.实现基于环形队列实现生产者消费者模型代码:

3.1.实现逻辑细节:

3.2代码实现:

#include<iostream>
#include<vector>
#include<semaphore.h>
#include<cassert>
#include<pthread.h>

using namespace std;
const int gCapcity = 10;
template<class T>
class RingQueue
{
public:
    RingQueue(const int& capcity = gCapcity)
    :_queue(capcity),_capacity(capcity)
    {
        //信号量进行初始化:
        sem_init(&_PSem,0,_capacity);
        sem_init(&_CSem,0,0);
        //对锁进行初始化:
        pthread_mutex_init(&_Pmutex,nullptr);
        pthread_mutex_init(&_Cmutex,nullptr);
        _PIndex = _CIndex = 0;
    }
    //生产者放入数据:
    void Push(const T& in)
    {
        //1.通过申请信号量来判断是否有空间:
        int n = sem_wait(&_PSem); //信号量的值减1
        assert(n == 0);
        pthread_mutex_lock(&_Pmutex);
        //2.申请信号量成功:放入数据
        _queue[_PIndex++] = in;
        _PIndex %= _capacity;
        pthread_mutex_unlock(&_Pmutex);
        //3._CSem++
        sem_post(&_CSem);//信号量的值加1
    }
    //消费者拿数据:
    void Pop(T* out)
    {
        //1.通过申请信号量来判断是否有数据:
        int n = sem_wait(&_CSem);
        assert(n == 0);
        pthread_mutex_lock(&_Cmutex);
        //2.申请信号量成功:拿出数据
        *out = _queue[_CIndex++];
        _CIndex %= _capacity;
        pthread_mutex_unlock(&_Cmutex);
        //3._PSem++;
        sem_post(&_PSem);
    }
    ~RingQueue()
    {
        //销毁信号量:
        sem_destroy(&_PSem);
        sem_destroy(&_CSem);
        //销毁锁:
        pthread_mutex_destroy(&_Pmutex);
        pthread_mutex_destroy(&_Cmutex);
    }
private:
    vector<T> _queue;
    int _capacity;
    sem_t _PSem;//标识生产者
    sem_t _CSem;//标识消费者
    int _PIndex;
    int _CIndex;
    pthread_mutex_t _Pmutex;
    pthread_mutex_t _Cmutex;
};

 代码测试:

#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<time.h>
#include"RingQueue.hpp"
using namespace std;

void* ProductorRoutine(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
    while(true)
    {
        int data;
        data = rand() % 10;
        rq->Push(data);
        cout << "生产完成,生产的数据是: " << data << endl;
        sleep(1);
    }
    return nullptr;
}
void* ConsumerRoutine(void* args)
{
    RingQueue<int>* rq = static_cast<RingQueue<int>*>(args);
    while(true)
    {
        int data;
        rq->Pop(&data);
        cout << "消费完成,消费的数据是: "  << data << endl;
    }
    return nullptr;
}
int main()
{
    srand((unsigned int)time(nullptr));
    //1.创建线程:单生产,单消费
    pthread_t tidP,tidC;
    RingQueue<int> * rq = new RingQueue<int>();
    pthread_create(&tidP,nullptr,ProductorRoutine,rq);
    pthread_create(&tidC,nullptr,ConsumerRoutine,rq);
    pthread_join(tidP,nullptr);
    pthread_join(tidC,nullptr);
    return 0;
}

运行截图:

16.线程池

16.1线程池的概念

线程池是一种常见的多线程实现方式,它可以在程序启动时预先创建一定数量的线程,这些线程在运行过程中可以被重复利用,从而避免了线程的频繁创建和销毁带来的性能开销。

16.2线程池实例

1. 创建固定数量线程池,循环从任务队列中获取任务对象
2. 获取到任务对象后,执行任务对象中的任务接口

//Thread.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<cstdio>
#include<pthread.h>
using namespace std;

class Thread 
{
    static void* start_routine(void* args)
    {
        Thread* _this = static_cast<Thread*> (args);
        return _this->fun();
    }
public:
    using func_t = function<void*(void*)>;
    Thread()
    {
        char buf[128];
        snprintf(buf,sizeof buf,"thread->%d",threadnum++);
        _name = buf;
    }
    void start(func_t fun,void* args = nullptr)
    {
        _fun = fun;
        _args = args;
        pthread_create(&_tid,nullptr,start_routine,this);
    }
    void join()
    {
        pthread_join(_tid,nullptr);
    }
    string threadname()
    {
        return _name;
    }
    ~Thread()
    {}
    void* fun()
    {
        return _fun(_args);
    }
private:
    string _name;//线程名
    pthread_t _tid;//线程id
    void* _args;//线程调用函数传递参数
    func_t _fun;
    static int threadnum;
};
int Thread::threadnum = 1;
//ThreadPool.hpp
#pragma once

#include<iostream>
#include<vector>
#include<queue>
#include<pthread.h>
#include<mutex>
#include"Thread.hpp"
#include"LockGuard.hpp"
using namespace std;

//将数据封装成结构体传给线程函数
template <class T>
class ThreadPoll;
template<class T>
class ThreadData
{
public:
    ThreadPoll<T>* threadpool;
    string name;
    ThreadData(ThreadPoll<T>* tp,const string& n)
    :threadpool(tp),name(n) {}
};
const int gnum = 5; //默认线程数
template<class T>
class ThreadPoll
{
private:
    static void* handlerTask(void* args)
    {
        ThreadData<T>* tp = static_cast<ThreadData<T>*>(args);
        while(true)
        {
            T t;
            {
                LockGuard lock_guard(tp->threadpool->mutex());
                while(tp->threadpool->isQueueEmpty())
                {
                    tp->threadpool->threadWait();
                }
                //将任务从公共队列中拿到自己独立的栈中
                t = tp->threadpool->pop();
            }
            std::cout << tp->name << " 获取了一个任务: " << t.toTaskString() << " 并处理完成,结果是:" << t() << std::endl;
        }
        delete tp;
        return nullptr;
    }
public:
    void lockQueue()
    {
        pthread_mutex_lock(&_mut);
    }
    void unlock()
    {
        pthread_mutex_unlock(&_mut);
    }
    bool isQueueEmpty()
    {
        return _task.empty();
    }
    void threadWait()
    {
        pthread_cond_wait(&_cond,&_mut);
    }
    T pop()
    {
        T t = _task.front();
        _task.pop();
        return t;
    }
    ThreadPoll(int num = gnum)
    :_num(num)
    {
        pthread_mutex_init(&_mut,nullptr);
        pthread_cond_init(&_cond,nullptr);
        for(int i = 0; i < _num; i++)
        {
            _threads.push_back(new Thread());
        }
    }
    pthread_mutex_t* mutex()
    {
        return &_mut;
    }
    void run()
    {
        for(const auto&t : _threads)
        {
            ThreadData<T>* tp = new ThreadData<T>(this,t->threadname());
            t->start(handlerTask,tp);
            std::cout << t->threadname() << " start ..." << std::endl;
        }
    }
    void push(const T& in)
    {
        LockGuard lock_guard(&_mut);
        _task.push(in);
        pthread_cond_signal(&_cond);
    }
    ~ThreadPoll()
    {
        pthread_mutex_destroy(&_mut);
        pthread_cond_destroy(&_cond);
        for(const auto& t: _threads)
        {
            delete t;
        }
    }
private:
    int _num;//最大开辟线程数
    vector<Thread*> _threads;
    queue<T> _task;
    pthread_mutex_t _mut;
    pthread_cond_t _cond;
};
//Task.hpp
#pragma once
#include<iostream>
#include<cstdio>
#include<functional>
#include<string>
class Task
{
public:
    using fun_c = std::function<int(int,int,char)>;
    Task() {}
    //构造任务对象:
    Task(int x,int y,char op,fun_c fun)
    :_x(x),_y(y),_op(op),_fun(fun){}
    //重载operator():对象调用任务:
    std::string operator()()
    {
        int result = _fun(_x,_y,_op);
        //结果转换成字符串返回:
        char buf[64];
        snprintf(buf,sizeof buf,"%d %c %d = %d",_x,_op,_y,result);
        return buf;
    }
    std::string toTaskString()
    {
        int result = _fun(_x,_y,_op);
        char buf[64];
        snprintf(buf,sizeof buf,"%d %c %d = ?",_x,_op,_y);
        return buf;
    }
private:
    int _x;
    int _y;
    char _op;
    fun_c _fun;
};
const string oper = "+-*/%";
int mymath(int x,int y,char op)
{
    int result = 0;
    switch(op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if(y == 0)
        {
            std::cerr<<"div 0 error" << endl;
            result = -1;
        }
        else
        result = x / y;
    }
        break;
    case '%':
    {
        if(y == 0)
        {
            std::cerr<<"mod 0 error" << endl;
            result = -1;
        }
        else
        result = x % y;
    }
        break;
    }
    return result;
}
//LockGuard.hpp
#include<iostream>
#include<pthread.h>

using namespace std;

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock_p = nullptr):lock_p_(lock_p)
    {}
    void lock()
    {
        if(lock_p_)
            pthread_mutex_lock(lock_p_);
    }
    void unlock()
    {
        if(lock_p_)
            pthread_mutex_unlock(lock_p_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t* lock_p_;
};
class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex):mutex_(mutex)
    {
        mutex_.lock(); //在构造函数中加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();//在析构函数中解锁
    }
private:
    Mutex mutex_;
};
//Test.cpp
#include<iostream>
#include<pthread.h>
#include<memory>
#include<cstdlib>
#include<ctime>
#include<unistd.h>
#include"ThreadPool.hpp"
#include"Task.hpp"
using namespace std;

int main()
{
    srand((unsigned int)time(nullptr));
    unique_ptr<ThreadPoll<Task>> tp(new ThreadPoll<Task>());
    tp->run();
    while(true)
    {
        int x = rand() % 10;
        int y = rand() % 5;
        char op = oper[rand()%oper.size()];
        Task t(x,y,op,mymath);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

运行截图

17.线程安全的单例模式

17.1什么是单例模式

是一种经典的常用的设计模式

17.2什么是设计模式

对一些经典的常用场景,给定了一些对应的解决方案,这个就是设计模式

17.3单例模式

某些类,只应该具有一个对象实例,就称为单例。

17.3.1饿汉方式实现单例模式

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

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

17.3.2懒汉方式实现单例模式

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

存在一个严重的问题, 线程不安全.
第一次调用 GetInstance 的时候, 如果两个线程同时调用, 可能会创建出两份 T 对象的实例.

懒汉方式实现单例模式(线程安全版本)

// 懒汉模式, 线程安全
template <typename T>
class Singleton {
	volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
	static std::mutex lock;
public:
	static T* GetInstance() {
		if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
			lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
			if (inst == NULL) {
				inst = new T();
			}
			lock.unlock();
		}
		return inst;
	}
};

17.3.3饿汉模式与懒汉模式的优缺点

饿汉模式

优点:在多线程高并发环境下使用,可以避免资源竞争,提高相应速度

缺点:

1.初始化数据多,会导致启动慢

2.多个单例类初始化如果有依赖关系是无法控制顺序的

懒汉模式:

优点:

  1. 节约资源:在程序启动时不会创建对象,只有在需要时才会创建对象,能够节约资源。
  2. 线程安全性较高:因为懒汉模式在第一次调用时才创建对象,所以在多线程环境下,只有一个线程能够创建对象,从而保证线程安全。
  3. 可以延迟初始化:懒汉模式可以在需要时再初始化,可以避免不必要的初始化。

缺点:

  1. 线程不安全:当多个线程同时调用getInstance()方法时,有可能会创建多个实例,从而破坏单例模式。
  2. 可能存在性能问题:每次调用getInstance()方法时都需要加锁,可能会影响程序的性能。
  3. 实现复杂:需要考虑线程同步和线程安全问题,实现比较复杂。

综上所述,懒汉模式适用于对象创建比较耗费资源且在程序运行时不一定需要创建的情况下使用,但需要注意线程安全问题。

18.STL,智能指针和线程安全

18.1STL中的容器是否是线程安全的?

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

18.2智能指针是否是线程安全的?

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

19. 其他常见的各种锁

悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁,公平锁,非公平锁?

自旋锁介绍:

当一个成功申请临界资源的线程在临界区待多长的时间,时间较长,可以使用自旋锁提高效率,时间的长短的衡量是在具体场景中由程序员自己评估的

自旋锁常用接口介绍:

自旋锁的创建和销毁:

#include <pthread.h>
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);

自旋锁加锁的接口:

#include <pthread.h>

int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);

自旋锁解锁的接口:

#include <pthread.h>
int pthread_spin_unlock(pthread_spinlock_t *lock);

20. 读者写者模型

20.1读者写者模型概念理解

相对于生产和消费者模型,读者和写者模型在消费者和消费者之间与读者和读者之间的关系是不相同的,因为消费者和消费者之间是互斥关系而读者和读者之间是没有关系的,因为消费者会拿走数据,而读者是不会动数据,所以针对这个特性,读者和写者模型一般是用于大部分时间是在读取,而少量时间在写入。

20.2读写锁

读写锁是在代码编写的过程中保证读者和写者的特性

注意:写独占,读共享,读锁优先级高 

读写锁的接口

初始化和销毁

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

加锁和解锁

//读加锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
//写加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

20.3读写锁加锁和解锁实现伪代码实例

21.总结

以上是对线程从概念理解到使用以及在使用的过程中遇到线程安全问题如何解决,并且介绍了多线程写的生产和消费者模型以及读写者模型等,相信看完本篇文章,你对线程的理解和使用会有非常大的帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/13334.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot集成MyBatis-yml自动化配置原理详解

SpringBoot集成MyBatis-yml自动化配置原理详解 简介&#xff1a;spring boot整合mybatis开发web系统目前来说是市面上主流的框架&#xff0c;每个Java程序和springboot mybatis相处的时间可谓是比和自己女朋友相处的时间都多&#xff0c;但是springboot mybatis并没有得到你的真…

适用于 Windows 的 5 个最好的 PDF 转换器应用程序

由于稳定性、高分辨率、高安全性、易于传输等特点&#xff0c;PDF已经成为我们日常工作中最常用的格式。我们在享受PDF带来便利的同时&#xff0c;也发现PDF带来了一些不便&#xff0c;其中最大的问题就是PDF内容的编辑难度。同时&#xff0c;并不是所有的文件都是PDF格式的&am…

代码优化- 前端优化

常量折叠 基本思想&#xff1a;在编译期间计算表达式的值&#xff08;编译时静态计算&#xff09; 例如&#xff1a;a 3 5 > a 8&#xff0c;if (true && false) ... > if (false) 好处是&#xff1a;语法树的节点数量减少了&#xff0c;意味着编译器要维护…

Ubuntu上跑通PaddleOCR

书接上文。刚才说到我已经在NUC8里灌上了Windows Server 2019。接下来也顺利的启用了Hyper-V角色并装好了一台Ubuntu 22.04 LTS 的虚机。由于自从上回在树莓派上跑通了Paddle-Lite-Demo之后想再研究一下PaddleOCR但进展不顺&#xff0c;因此决定先不折腾了&#xff0c;还是从x6…

【论文写作】如何写科技论文?万能模板!!!(以IEEE会议论文为例)

0. 写在前面 常言道&#xff0c;科技论文犹如“八股文”&#xff0c;有固定的写作模式。本篇博客主要是针对工程方面的论文的结构以及写作链条的一些整理&#xff0c;并不是为了提高或者润色一篇论文的表达。基本上所有的论文&#xff0c;都需要先构思好一些点子&#xff0c;有…

一文搞懂Session和JWT登录认证

前言 目前在开发的小组结课项目中用到了JWT认证&#xff0c;简单分享一下&#xff0c;并看看与Session认证的异同。 登录认证&#xff08;Authentication&#xff09;的概念非常简单&#xff0c;就是通过一定手段对用户的身份进行确认。 我们都知道 HTTP 是无状态的&#xf…

强化学习技巧

此软件包处于维护模式&#xff0c;请使用Stable-Baselines3 (SB3)获取最新版本。您可以在 SB3 文档中找到迁移指南。 本节的目的是帮助您进行强化学习实验。它涵盖了有关 RL 的一般建议&#xff08;从哪里开始、选择哪种算法、如何评估算法等&#xff09;&#xff0c;以及使用自…

【Linux】System V 共享内存、消息队列、信号量

&#x1f34e;作者&#xff1a;阿润菜菜 &#x1f4d6;专栏&#xff1a;Linux系统编程 system V共享内存介绍 System V 共享内存是一种进程间通信的机制&#xff0c;它允许多个进程共享一块物理内存区域&#xff08;称为“段”&#xff09;。System V 共享内存的优点是效率高&…

OTG是什么意思?

OTG是什么意思&#xff1f; OTG是怎么样实现的&#xff1f; TYPE-C接口的手机如何实现同时充电OTG功能&#xff1f; OTG是什么意思&#xff1f; OTG是On-The-Go的缩写&#xff0c;是一项新兴技术&#xff0c;主要应用于不同的设备或移动设备间的联接&#xff0c;进行数据交…

基于遥感的自然生态环境检测——实验三:生态因子提取

实验三&#xff1a;生态因子提取 一、实验目标 生态因子生成&#xff1b;生态因子归一化&#xff1b;生态环境评价 二、实验内容 根据经过大气校正后的影像生产土地覆盖指数、土壤指数以及坡度等&#xff0c;对土地覆盖指数、土壤指数以及坡度进行密度分割归一化&#xff1…

“SCSA-T学习导图+”系列:下一代防火墙

本期引言&#xff1a; 近年来&#xff0c;随着数字化业务带给我们高效和便捷的同时&#xff0c;信息暴露面的增加、网络边界的模糊化以及黑客攻击的产业化&#xff0c;使得网络安全事件相较以往成指数级增加。传统防火墙基于五元组的方式对进出网络的数据流量进行访问控制&…

JavaScript(JS)-1.JS基础知识

1.JavaScript概念 (1)JavaScript是一门跨平台&#xff0c;面向对象的脚本语言&#xff0c;来控制网页行为的&#xff0c;它能使网页可交互 (2)W3C标准&#xff1a;网页主要由三部分组成 ①结构&#xff1a;HTML负责网页的基本结构&#xff08;页面元素和内容&#xff09;。 …

【Linux网络服务】Linux网络设置

一、查看网络配置 1.1ifconfig 1.2ip a 1.3什么是mtu 最大传输单元MTU&#xff0c;是指网络能够传输的最大数据包大小&#xff0c;以字节为单位。MTU的大小决定了发送端一次能够发送报文的最大字节数。如果MTU超过了接收端所能够承受的最大值&#xff0c;或者是超过了发送路径…

EIGRP 配置,详解拓扑表,路由汇聚

1.3 EIGRP 拓扑&#xff0c;路由以及汇聚 1.3.1 实验目的 通过对 EIGRP 拓扑&#xff0c;路由以及汇聚相关实验的练习&#xff0c;掌握 EIGRP 建立拓扑信息的方式&#xff0c; 度量计算方法&#xff0c;如何调整度量&#xff0c;非等价负载均衡&#xff0c;以及 EIGRP 末节路…

做完自动化测试,但别让不会汇报毁了你...

pytest 是一个成熟的全功能Python测试工具&#xff0c;可以帮助您编写更好的程序。它与 python 自带的 unittest 测试框架类似&#xff0c;但 pytest 使用起来更简洁和高效&#xff0c;并且兼容 unittest 框架。pytest 能够支持简单的单元测试和复杂的功能测试&#xff0c;pyte…

Verilog带参数的`define用法

宏除了可以进行简单的文本替换,还可以像函数和任务一样传递指定多个参数分别对文本进行对应的替换. 示例1&#xff1a; define Disp(pa,pb,pc) \initial \begin \#1200; \$display("%d \n",(papbpc)); \$display(" data_ pa data_ pb data_ pc %d",(…

C#中用程序代码修改了datagridview中的数据,保存时只对光标当前行有保存解决办法

C#中DataGridView绑定了DataTable后&#xff0c;通过代码修改DataGridView中的数据&#xff0c;总有一行&#xff08;被修改过并被用户选中的行集合中索引为0的行&#xff09;不能被UpDate回数据库的问题和解决办法 长江黄鹤 2017-06-26 | 300阅读 | 1转藏 转藏全屏朗读分…

真题详解(UML部署图)-软件设计(五十二)

真题详解&#xff08;地址索引&#xff09;-软件设计&#xff08;五十一)https://blog.csdn.net/ke1ying/article/details/130211684 瀑布模式&#xff1a;适应 开发大型项目&#xff0c;且需求明确。 演化模式&#xff1a;适应 对软件需求缺乏准确认知。 螺旋模式&#xff…

【linux】yum “应用商店” 的基本用法

好多工具 yum软件包查看软件包安装软件卸载软件 yum 通俗的讲&#xff0c;这就似我们手机上的应用商店&#xff0c;只不过是在linux下的。 我们可以用yum来下载东西。 软件包 在Linux下安装软件, 一个通常的办法是下载到程序的源代码, 并进行编译, 得到可执行程序。 但是这样…

19 calloc 和 realloc 虚拟内存分配的调试

前言 前面提到了 malloc 虚拟内存分配相关的内容 malloc 虚拟内存分配的调试(1) malloc 虚拟内存分配的调试(2) 这里提 calloc 和 realloc, 这两个函数 虽然没有 malloc 使用频率那么高 但是 还是有很大的知名度的, 本文这里 我们来看一下 calloc 此函数传入两个参数, 第…