Linux 多线程

文章目录

一、背景知识

1、地址空间

[!IMPORTANT]

  • os进行内存管理,不是以字节为单位,而是以内存块为单位。(内存块默认大小4KB)
  • 系统和磁盘进行IO的基本单位是4KB(8个连续的扇区)

2、物理内存

image-20240822180637960

3、页表

image-20240822180800282

4、文件缓冲区、虚拟地址

什么是文件缓冲区?

将物理内存的页框(struct page)和文件(struct file)关联起来,这个页框就是文件缓冲区。

什么是函数?

连续的代码地址构成的代码块。

函数有地址吗?

有,每行代码都有地址,而且对于同一个函数内部的代码语句,我们认为地址是连续的。

代码数据划分的本质是什么?

拆分页表。

虚拟地址的本质是什么?

虚拟地址本质是一种资源!拥有者可以通过页表进行访问。

二、多线程

1、线程的概念

[!IMPORTANT]

  • 线程定义:在进程内部运行,是cpu调度的基本单位
  • 进程定义(内核观点):承担分配系统资源的基本实体

可以粗略的理解为:同一个进程内,一个task_struct对应的执行流就是一个线程。

image-20240822181548751

os怎么管理线程?

[!IMPORTANT]

先描述、再组织。

描述:struct tcb{//线程id,优先级,状态,上下文,连接属性…};

但是,在Linux中,线程是复用的进程的pcb。这样就不需要单独为线程单独设计数据结构和调度算法了。

Linux中,进程和线程的关系?

[!IMPORTANT]

在Linux中,没有线程的概念,只有轻量级进程的概念。

一个进程中,可能存在不止一个执行流,也就是说,可能存在多个pcb。以前学的进程是一个进程仅存在一个pcb,而现在是一对多的关系。

Linux中没有线程的概念,用户是怎么用线程的?

[!IMPORTANT]

在Linux底层中没有线程的概念,只有轻量级进程,为了上层能用线程,Linux把它进行了封装成了线程库。所以要用到线程的时候,需要连接一个线程库。

image-20240822182745578

2、线程的操作

创建线程

想要创建线程,必须手动连接pthread库

g++ $^ -o &@ -lpthread
// $^:是目标文件 --- main.exe->可执行程序
// $@:是依赖文件 --- main.cc->源文件
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
// 第三个参数,传一个函数指针,新线程所要执行的代码
// 第四个参数,作为第三个函数指针指向函数的参数。

// 主线程继续向下运行,新线程去执行其他的代码。
// 想要创建线程,必须手动连接pthread库
image-20240822211908525线程的唯一标识

对于同一个进程中的不同线程,两个线程的PID相同,但是LWP不同。

所以,唯一标识一个线程的是LWP(Light Weight Process:轻量级进程)

image-20240822202912643

LWP作为线程唯一标识,对单线程有影响吗?

没有影响,因为对于单进程而言,LWP 和 PID是一一对应的。

已经有多进程了,为什么还需要多线程?

[!IMPORTANT]

  • 进程的创建,成本高;线程的创建,成本低。—启动
  • 线程的调度成本低。—运行
  • 删除一个线程的成本低。—删除

线程的优势:

image-20240822205718215

线程的缺点:

对于一个进程中的多个线程,只要有一个线程出现了问题,多个线程都会崩溃。

对于不同系统实现线程的方式

[!IMPORTANT]

  • 不同的系统对于进程和线程的实现方式都不一样,但是实现的原则都是一样的。
  • 对于Linux:没有单独实现线程,只是复用pcb。
  • 对于Windows:单独实现线程。
为什么线程调度的成本更低?

[!IMPORTANT]

cpu的寄存器只有一套。当调度另外一个线程时,cpu中,cache等寄存器的上下文数据和物理空间之间的拷贝的操作。

如果是两个进程的话,代码可能不一样,对与进程a来说cache里面存储的代码,在进程b用不上,需要重新进行加载。

不同线程之间的共享部分?

[!IMPORTANT]

是的。

不同线程之间的私有部分?

[!IMPORTANT]

  • 一组寄存器:硬件上下文数据—线程可以动态运行。
  • 栈:每个线程都要有自己的栈结构。线程在运行的时候,会形成各种临时变量,临时变量会被每个线程保存在自己的栈空间。
全面看待线程函数传参?

[!IMPORTANT]

参数是能传递进去的,只需要在线程函数中将参数强转成需要的类型。

我们可以传递任意类型,可以传递类对象地址(用于传递多个参数)。

传递线程函数参数是类对象的时候,建议从堆上空间开辟。

全面的看待线程函数返回?

[!IMPORTANT]

线程退出的时候,只需要考虑正常的返回,不考虑异常,因为异常了,整个进程就崩溃了,包括主线程。

创建多线程
std::vector<pthread_t> tids;
for (int i = 0; i < num; i ++ )
{
    pthread_t tid;
    char *name = new char[128];
    snprintf(name, 128, "thread-%d", i+1);
    pthread_create(&tid, nullptr, threadrun, name);
    
    tids.emplace_back(tid);
}

for(auto tid:tids)
{
    pthread_join(tid, nullptr);
}

线程等待

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

image-20240822212703838

主线程和新线程哪一个先运行?

[!IMPORTANT]

不确定!

我们期望谁最后退出?怎么能保证呢?

[!IMPORTANT]

主线程应该最后退出。理由是要和子进程一样,退出信息要被父进程接收。

保证主线程最后退出:用pthread_join函数来进行保证,如果主线程先完成部分代码,会等待新线程。

tid是什么呢?(pthread_t tid)

[!IMPORTANT]

是一个地址(虚拟地址)。

给用户提供的的线程ID,不是内核中的lwp,而是pthread库维护的一个唯一值。

线程退出

[!IMPORTANT]

线程退出有三种情况:1、正常退出(线程函数返回 或者 主线程返回)2、通过pthread_exit退出 3、线程被取消(pthread_cancel)

#include <pthread.h>
void pthread_exit(void *retval); // 线程退出

#include <pthread.h>
int pthread_cancel(pthread_t thread); // 线程取消
// pthread_cancel是在主线程内部调用函数。

#include <pthread.h>
int pthread_detach(pthread_t thread); // 线程分离
// 可以在线程函数中调用线程分离函数,也可以在主线程中调用线程分离函数。

线程退出

线程如何终止?

[!IMPORTANT]

1、新线程:线程函数return。

2、主线程:main函数结束,表示进程结束。

可不可以不join线程,让他执行完就退出呢?

[!IMPORTANT]

可以!

a、一个线程被创建,默认是joinable的,必须要被join的。

b、如果一个线程被分离,线程的工作状态->分离状态,不需要被join,也不能被join。依旧属于线程内部,但是不需要被等待。

库的理解

[!IMPORTANT]

创建线程,前提就是把库加载到内存,映射到进程的地址空间!

  • 每个线程都有自己独立的栈,本质上就是线程在自己的tcb中维护了一段大小合适的栈结构。
  • Linux线程 = pthread中线程的属性集+LWP

image-20240823001814169

image-20240823001845969

在新线程内部获取该线程的线程id
#include <pthread.h>
pthread_t pthread_self(void);
// 在线程的内部,获取线程id -> 也就是tid
怎么保证pthread中线程和LWP一一对应?

os要提供一个LWP相关的系统调用。pthread库就是对其进行封装。

线程互斥(互斥锁)

[!IMPORTANT]

共享资源:多个线程能够看到的资源。

我们需要对共享资源进行保护。

锁的接口
// 互斥锁的类型:pthread_mutex_t
// 锁是全局的或者静态的,只需要initializer即可
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
// 锁是动态的
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
// 锁的销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
对临界资源保护的理解

[!IMPORTANT]

所谓的对临界资源进行保护,本质是对临界区代码及进行保护!—> 我们对所有资源进行访问,本质都是通过代码进行访问的 —> 保护资源,本质就是把访问资源的代码进行保护

image-20240823002347391

原理角度理解锁

[!IMPORTANT]

如何理解申请锁成功,允许你进入临界区。—申请锁成功,pthread_mutex_lock()函数会返回。

如何理解申请锁失败,不允许你进入临界区。—申请所失败,pthread_mutex_lock()函数不返回,线程就是阻塞。线程会在每次时间

片中检查,临界资源是否仍被加锁,如果没有,则申请加锁,成功,就继续执行。

实现角度理解锁

[!IMPORTANT]

大多数的体系结构中,都提供了swap或者exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,只有一条指令,保证了原子性。

1、cpu的寄存器只有一套,被所用的线程共享,但是寄存器里面的数据属于执行流的上下文,属于执行流私有的数据。

2、cpu在执行代码的时候,一定也要有对应的执行载体—线程&&进程

3、数据在内存中,被所有线程是共享的。

结论:把数据从内存移动到cpu寄存器中,本质是把数据从共享变成线程私有!

饥饿问题:某些线程长时间在等待队列中等待资源

[!IMPORTANT]

解决方案:要二次申请时,必须排队。—具有一定的顺序性(同步)

抢票小程序
// ticket.cc
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
#include <vector>
#include <mutex>
#include <memory>
#include "LockGuard.hpp"

int g_ticket_num = 10000;
int g_cnt = 0;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void *func(void *args)
{
    while (g_ticket_num > 0)
    {
        LockGuard lockguard(&mtx);
        if (g_ticket_num > 0)
        {
            std::cout << g_ticket_num << "票" << std::endl;
            g_ticket_num--;
        }
    }
    return nullptr;
}
int main()
{

    std::vector<pthread_t> tids(10);

    for (int i = 0; i < 10; i ++ )
    {
        pthread_t tid;
        char *name = new char[128];
        sprintf(name, "pthread -%d", i);
        pthread_create(&tid, nullptr, func, (void *)name);
        tids.emplace_back(tid);
    }
    for (auto x : tids)
        pthread_join(x, nullptr);

    sleep(100);
    return 0;
}

// LockGuard.hpp
#pragma once
#include <pthread.h>
class LockGuard
{
private:
    pthread_mutex_t *_mtx;

public:
    LockGuard(pthread_mutex_t *mtx)
        : _mtx(mtx)
    {
        pthread_mutex_lock(_mtx);
    }
    ~LockGuard()
    {
        pthread_mutex_unlock(_mtx);
    }
};

线程同步(唤醒线程)

条件变量

[!IMPORTANT]

条件变量:使得一个或多个线程可以在某个条件变量上等待,直到另一个线程发出信号。避免了繁忙等待。

1、需要一个线程队列。

2、需要通知机制。(可以唤醒一个,也可以唤醒多个)

条件变量的接口
// 条件变量的类型是pthread_cond_t;
// 条件变量局部的
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 条件变量是全局或者静态的
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 销毁
int pthread_cond_destroy(pthread_cond_t *cond);
// 等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 通知、唤醒
int pthread_cond_signal(pthread_cond_t *cond);

条件变量等待的时候为什么会加锁?

[!IMPORTANT]

因为条件变量也属于共享资源。

pthread_cond_wait被调用时为什么会释放锁?

[!IMPORTANT]

pthread_cond_wait被调用的时候,除了让自己继续排队等待,还会自己释放传入的锁。

为什么?避免死锁,当线程等待的时候,如果还加锁,唤醒等待线程的线程会被阻塞在尝试获取锁上。导致死锁。

函数返回的时候,还在临界区,但是锁释放了,这样怎么保证原先的加锁操作?

[!IMPORTANT]

函数返回时,必须先参与锁的竞争,重新加上锁,该函数才会返回!

测试代码
// 主要代码的内容:
// 先创建一批线程,然后让其进入等待队列,等待被唤醒
// 然后让主线程对其进行唤醒,执行被唤醒线程的代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <string>
#include <unistd.h>

pthread_mutex_t gmtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER;

void *func(void *args)
{
    while (true)
    {
        pthread_mutex_lock(&gmtx);

        // 条件等待
        pthread_cond_wait(&gcond, &gmtx);
        char *name = (char *)args;
        std::cout << name << std::endl;
        pthread_mutex_unlock(&gmtx);
        usleep(100000);
    }

    return nullptr;
}
int main()
{
    std::vector<pthread_t> tids;
    for (int i = 0; i < 10; i ++ )
    {
        char *name = new char[128];
        sprintf(name, "pthread -%d", i + 1);
        pthread_t tid;
        pthread_create(&tid, nullptr, func, name);
    }

    // 唤醒
    while (true)
    {
        pthread_cond_signal(&gcond);
        std::cout << "唤醒一个线程..." << std::endl;
        usleep(100000);
    }
    return 0;
}

三、生产消费者模型—多执行流并发模型

image-20240823093724703

作用

[!IMPORTANT]

  • 协调忙闲不均。比如说,有10个线程,每回来认为,都使用第一个线程来处理任务,导致其他线程一直不处理任务。
  • 效率高。

如何体现生产者消费者模型的效率高?

[!IMPORTANT]

1、并发

  • 生产者生产任务(向缓存中输入构建好的任务)的同时,其他生产者在构建任务。
  • 消费者获得任务的同时,其他消费者可能在处理任务。

2、解耦

image-20240823003910747

321原则

[!IMPORTANT]

1:一个交易场所(一段内存空间)

2:两种角色(生产角色,消费角色):生产线程,消费线程

3:三种关系:生产-生产,消费-消费,生产-消费 —> 都是互斥关系,生产-消费还有同步关系

同步机制用于确保生产者和消费者的操作按照正确的顺序进行。例如,生产者不应该在缓冲区已满时继续生产数据,消费者也不应该在缓冲区为空时尝试消费数据。

生产者消费者模型的选择

[!IMPORTANT]

如果生产快,消费慢:单生产多消费。

如果生产慢,消费慢:多生产多消费。

如果生产快,消费快:单生产单消费。

如果生产慢,消费快:多生产单消费。

生产消费者模型测试代码

// BlockQueue.hpp
#include <iostream>
#include <pthread.h>
#include <queue>
#include <unistd.h>
#include <time.h>
#include <cstdlib>

template<class T>
class BlockQueue
{
private:
    std::queue<T> _blockQueue;
    pthread_mutex_t _mtx;
    pthread_cond_t _p_cond;
    pthread_cond_t _c_cond;
    int _max_capacity;

private:
    bool isFull()
    {
        return _blockQueue.size() == _max_capacity;
    }
    bool isEmpty()
    {
        return _blockQueue.size() == 0;
    }
public:
    BlockQueue(int max_capacity)
        : _max_capacity(max_capacity)
    {}
    void bqInit()
    {
        pthread_mutex_init(&_mtx, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
    }
    // 出队列 --- 消费者消费
    void bqPop(T *out)
    {
        pthread_mutex_lock(&_mtx);
        while (isEmpty())
        {
            pthread_cond_wait(&_c_cond, &_mtx);
        }
        // 非空、被唤醒
        *out = _blockQueue.front();
        _blockQueue.pop();

        pthread_mutex_unlock(&_mtx);

        pthread_cond_signal(&_p_cond);
    }
   	// 入队列 --- 生产者生产
    void bqEqueue(const T& in)
    {
        pthread_mutex_lock(&_mtx);
        while (isFull())
        {
            pthread_cond_wait(&_p_cond, &_mtx);
        }
        // 非满:空或者不满、或者被唤醒
        _blockQueue.push(in);

        pthread_mutex_unlock(&_mtx);

        pthread_cond_signal(&_c_cond);
    }
    ~BlockQueue(){}
};

// main.cc
#include "BlockQueue.hpp"

void* Consumer(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    while (true)
    {
        sleep(1);
        int data = 0;
        bq->bqPop(&data);
        std::cout << "consumer :" << data<< std::endl;
    }
    return nullptr;
}

void* Productor(void *args)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(args);
    srand(time(nullptr));
    while (true)
    {
        int data = rand();
        bq->bqEqueue(data);
        std::cout << "Productor success" << std::endl;
        sleep(1);
    }
}
int main()
{
    BlockQueue<int> bq(10);
    bq.bqInit();

    pthread_t c, p;

    pthread_create(&p, nullptr, Productor, (void *)&bq);
    pthread_create(&c, nullptr, Consumer, (void *)&bq);

    sleep(100);
    return 0;
}

四、POSIX 信号量

信号量就是解决互斥与同步的。

1、接口

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);// 初始化
int sem_destroy(sem_t *sem);// 销毁
int sem_wait(sem_t *sem);// 等待的信号量大于0,值-1
int sem_post(sem_t *sem);// 唤醒信号量,值+1
int sem_getvalue(sem_t *sem, int *sval);// 获得当前信号量的值

2、生产消费模型 — 环形队列

[!IMPORTANT]

1.队列为空,让谁先访问?生产者先生产。(顺序 && 互斥特点)

2.队列满,让谁再访问?消费者来消费(顺序 && 互斥特点)

3.队列不空 && 队列不满 — 生产和消费同时进行。

​ a.不能让生产者把消费者套圈。

​ b.不能让消费者超过生产者。

消费者:关心资源?— 数据资源。

生产者:关系资源?— 空间资源。

image-20240823203624122

// RingQueue.hpp
#pragma once

#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
#include <cstdlib>
#include <time.h>
#include <unistd.h>

template <class T>
class RingQueue
{
private:
    // 循环队列
    std::vector<T> _ringQueue;
    // 最大容量
    int _max_size;
    // 互斥锁
    // 防止多线程的时候生产者一直生产,直到满了,死锁;消费者一直消费,直到空了,死锁
    pthread_mutex_t _p_mtx;
    pthread_mutex_t _c_mtx;
    // 条件变量---实现同步
    pthread_cond_t _c_cond;
    pthread_cond_t _p_cond;
    // 记录在循环队列中,生产者消费者的下标
    int _c_step;
    int _p_step;
    // 生产者消费者的信号量
    sem_t _c_sem;
    sem_t _p_sem;

public:
    RingQueue(int max_size)
        : _ringQueue(max_size), _max_size(max_size), _c_step(0), _p_step(0)
    {
        pthread_mutex_init(&_p_mtx, nullptr);
        pthread_mutex_init(&_c_mtx, nullptr);
        pthread_cond_init(&_c_cond, nullptr);
        pthread_cond_init(&_p_cond, nullptr);
        sem_init(&_c_sem, 0, 0);
        sem_init(&_p_sem, 0, _max_size);
    }
    // 出队 ---  消费者消费
    void rqPop(T *out)
    {
        // 如果信号量不为0, 减减;为0,则被阻塞,直到信号量大于0
        sem_wait(&_c_sem);
        // 对共享资源进行操作---> 将数据从循环队列中删除
        pthread_mutex_lock(&_c_mtx);

        *out = _ringQueue[_c_step];
        _c_step++;
        _c_step %= _max_size;

        pthread_mutex_unlock(&_c_mtx);
        sem_post(&_p_sem);
    }
    void rqEqueue(const T& in)
    {
        sem_wait(&_p_sem);
        pthread_mutex_lock(&_p_mtx);

        _ringQueue[_p_step] = in;
        _p_step++;
        _p_step %= _max_size;

        pthread_mutex_unlock(&_p_mtx);
        sem_post(&_c_sem);
    }
    ~RingQueue() 
    {
        pthread_mutex_destroy(&_p_mtx);
        pthread_mutex_destroy(&_c_mtx);

        sem_destroy(&_c_sem);
        sem_destroy(&_p_sem);
    }
};

// main.cc
#include "RingQueue.hpp"

void* Productor(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    srand(time(nullptr));
    while (true)
    {
        int data = rand();
        rq->rqEqueue(data);
        std::cout << "生产了一个数据: " << data << std::endl;
        sleep(1);
    }
}
void *Consumer(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    while (true)
    {
        sleep(1);
        int data = 0;
        rq->rqPop(&data);
        std::cout << "消费了一个数据: " << data << std::endl;
    }
}
int main()
{
    RingQueue<int> rq(1000);
    pthread_t c1, c2, c3, p1, p2;

    pthread_create(&p1, nullptr, Productor, (void *)&rq);
    pthread_create(&p2, nullptr, Productor, (void *)&rq);

    pthread_create(&c1, nullptr, Consumer, (void *)&rq);
    pthread_create(&c2, nullptr, Consumer, (void *)&rq);
    pthread_create(&c3, nullptr, Consumer, (void *)&rq);

    sleep(100);
    return 0;
}

信号量这里对资源进行使用,申请,为什么不判断一下条件是否满足?

[!IMPORTANT]

信号量本身就是判断条件!

int sem_wait(sem_t *sem);

  • 目的:等待(阻塞)信号量的值大于0。如果信号量的值大于0,则将其减1并立即返回;如果信号量的值为0,则调用线程将被阻塞,直到信号量的值变为大于0。
  • 返回值:成功时返回0;失败时返回-1,并设置errno以指示错误。

int sem_post(sem_t *sem);

  • 目的:增加(发布)信号量的值。将信号量的值增加1,并唤醒一个可能正在等待该信号量的线程(如果有的话)。
  • 返回值:成功时返回0;失败时返回-1,并设置errno以指示错误。

什么是信号量?

[!IMPORTANT]

信号量:是一个计数器,是资源的预订机制。预定:在外部可以不判断资源是否满足,就可以直到内部资源的情况!

五、日志

#include <time.h>
time_t time(time_t *tloc); // 获得一个时间戳
struct tm *localtime(const time_t *timep); // 将时间戳转化成年月日,时分秒,想得到准确的年,需要加上1900

// 获取可变参数
#include <stdarg.h>
// 将可变参数转化为字符串
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
//  处理可变参数
va_list ap;// va_list本质就是char*,定义一个指针
va_start(ap, 离可变参数最近的参数);// 让指针指向可变参数
va_arg(ap,  参数类型);// 获取下一个可变参数
va_end(ap);// 将指针置空

预处理符:
__FILE__// 被替换成文件名
__LINE__// 被替换成当前所在行
_VA_ARGS_// 宏获取可变参数
## // 连接操作符,用于在宏定义中将两个标记连接起来
// Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <time.h>
#include <stdarg.h>

pthread_mutex_t mtx;

enum level
{
    DEBUG = 1,
    INFO,
    WARNING,
    ERROR,
    FATAL
};
enum Type
{
    PRINTTOSCREEN,
    PRINTTOFILE
};

// [日志等级][pid][filename][filenumber][time]日志内容(支持可变参数)
struct LogMessage
{
    std::string _level;
    pid_t _pid;
    std::string _filename;
    int _filenumber;
    std::string _time;
    std::string _message_info;
};
std::string LevelToString(int level)
{
    switch (level)
    {
    case 1:
        return "DEBUG";
    case 2:
        return "INFO";
    case 3:
        return "WARNING";
    case 4:
        return "ERROR";
    case 5:
        return "FATAL";
    default:
        return "NONE";
    }
}
std::string GetCurrentTime()
{
    time_t now = time(nullptr);
    struct tm *curr_time = localtime(&now);
    char buffer[128] = {0};
    // 这里得到的年 = 当前的年份 - 1900
    // 月 :[0, 11]
    sprintf(buffer, "%04d/%02d/%02d %02d:%02d:%02d", curr_time->tm_year + 1900, curr_time->tm_mon + 1, curr_time->tm_mday, curr_time->tm_hour, curr_time->tm_min, curr_time->tm_sec);
    return buffer;
}
// [日志等级][pid][filename][filenumber][time]日志内容(支持可变参数)
void ptS(const LogMessage &lg)
{
    pthread_mutex_lock(&mtx);
    printf("[%s][%d][%s][%d][%s] %s\n", lg._level.c_str(), lg._pid, lg._filename.c_str(), lg._filenumber, lg._time.c_str(), lg._message_info.c_str());
    pthread_mutex_unlock(&mtx);
}
void ptF(const LogMessage &lg)
{
    pthread_mutex_lock(&mtx);
    FILE *fp = fopen("log.txt", "a");

    fprintf(fp, "[%s][%d][%s][%d][%s] %s\n", lg._level.c_str(), lg._pid, lg._filename.c_str(), lg._filenumber, lg._time.c_str(), lg._message_info.c_str());

    fclose(fp);
    pthread_mutex_unlock(&mtx);
}

class Log
{
private:
    int _type = PRINTTOSCREEN;

public:
    Log() {}
    void flushLog(const LogMessage &lg)
    {
        int type = _type;
        switch (type)
        {
        case PRINTTOSCREEN:
            ptS(lg);
            break;
        case PRINTTOFILE:
            ptF(lg);
            break;
        }
    }
    void logMessage(int level, std::string filename, int filenumber, const char *format, ...)
    {
        LogMessage lg;
        lg._level = LevelToString(level);
        lg._pid = getpid();
        lg._filename = filename;
        lg._filenumber = filenumber;
        lg._time = GetCurrentTime();
        char info[1024] = {0};
        va_list ap;
        va_start(ap, format);
        vsnprintf(info, sizeof(info), format, ap);
        lg._message_info = info;

        // 打印信息
        flushLog(lg);
    }
    ~Log() {}
};

// 简便,不用重复定义Log对象
Log lg;
#define LOG(level, Format, ...)                                        \
    do                                                                 \
    {                                                                  \
        lg.logMessage(level, __FILE__, __LINE__, Format, __VA_ARGS__); \
    } while (0)

// main.cc
#include "Log.hpp"

int main()
{

    LOG(DEBUG, __FILE__, __LINE__, "%s %d %lf", "hello Linux", 12, 3.14);

    return 0;
}

六、可重入 Vs 线程安全

1、概念

[!IMPORTANT]

可重入:函数被不同的执行流调用,当前一个执行流没有执行完,就有其他的执行流继续执行称为重入。如果运行结果不会出现任何任务称为可重入,反之,称为不可重入。

线程安全:多个线程并发执行一段代码时,不会出现不同的结果。在没有锁的情况下,就会出现问题。

  • 如果一个函数可重入,那一定是线程安全的。
  • STL库里的函数多数是线程不安全的。

2、智能指针是否线程安全?

[!IMPORTANT]

std::unique_ptr 是线程安全的,前提是对象的所有权转移是正确的,并且所有权转移操作在不同线程间的同步是保证的。

std::shared_ptr 的引用计数操作是线程安全的,但对对象的实际操作需要额外的同步机制来确保线程安全。

七、死锁

1、死锁的四个必要条件

[!IMPORTANT]

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求和保持条件:一个执行流因请求资源而阻塞时,对以获得的资源保持不放。
  • 不剥夺条件:一个执行流获得以获得的资源,在未使用完前,不能强行剥夺。
  • 循环等待条件:若干执行流之前形成一种头尾相接的循环等待资源的关系。

2、避免死锁

[!IMPORTANT]

  • 破坏死锁的四个条件。
  • 加锁顺序一致。
  • 避免锁未释放的场景。
  • 资源一次性分配。
                                                            \
{                                                                  \
    lg.logMessage(level, __FILE__, __LINE__, Format, __VA_ARGS__); \
} while (0)

// main.cc
#include “Log.hpp”

int main()
{

LOG(DEBUG, __FILE__, __LINE__, "%s %d %lf", "hello Linux", 12, 3.14);

return 0;

}


# 六、可重入 Vs 线程安全

## 1、概念

> [!IMPORTANT]
>
> 可重入:函数被不同的执行流调用,当前一个执行流没有执行完,就有其他的执行流继续执行称为重入。如果运行结果不会出现任何任务称为可重入,反之,称为不可重入。
>
> 线程安全:多个线程并发执行一段代码时,不会出现不同的结果。在没有锁的情况下,就会出现问题。
>
> - 如果一个函数可重入,那一定是线程安全的。
> - STL库里的函数多数是线程不安全的。

## 2、智能指针是否线程安全?

> [!IMPORTANT]
>
> **`std::unique_ptr`** 是线程安全的,前提是对象的所有权转移是正确的,并且所有权转移操作在不同线程间的同步是保证的。
>
> **`std::shared_ptr`** 的引用计数操作是线程安全的,但对对象的实际操作需要额外的同步机制来确保线程安全。

# 七、死锁

## 1、死锁的四个必要条件

> [!IMPORTANT]
>
> - 互斥条件:一个资源每次只能被一个执行流使用。
> - 请求和保持条件:一个执行流因请求资源而阻塞时,对以获得的资源保持不放。
> - 不剥夺条件:一个执行流获得以获得的资源,在未使用完前,不能强行剥夺。
> - 循环等待条件:若干执行流之前形成一种头尾相接的循环等待资源的关系。

## 2、避免死锁

> [!IMPORTANT]
>
> - 破坏死锁的四个条件。
> - 加锁顺序一致。
> - 避免锁未释放的场景。
> - 资源一次性分配。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

仍有未知等待探索

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值