文章目录
一、背景知识
1、地址空间
[!IMPORTANT]
- os进行内存管理,不是以字节为单位,而是以内存块为单位。(内存块默认大小4KB)
- 系统和磁盘进行IO的基本单位是4KB(8个连续的扇区)
2、物理内存
3、页表
4、文件缓冲区、虚拟地址
什么是文件缓冲区?
将物理内存的页框(struct page)和文件(struct file)关联起来,这个页框就是文件缓冲区。
什么是函数?
连续的代码地址构成的代码块。
函数有地址吗?
有,每行代码都有地址,而且对于同一个函数内部的代码语句,我们认为地址是连续的。
代码数据划分的本质是什么?
拆分页表。
虚拟地址的本质是什么?
虚拟地址本质是一种资源!拥有者可以通过页表进行访问。
二、多线程
1、线程的概念
[!IMPORTANT]
- 线程定义:在进程内部运行,是cpu调度的基本单位。
- 进程定义(内核观点):承担分配系统资源的基本实体。
可以粗略的理解为:同一个进程内,一个task_struct对应的执行流就是一个线程。
os怎么管理线程?
[!IMPORTANT]
先描述、再组织。
描述:struct tcb{//线程id,优先级,状态,上下文,连接属性…};
但是,在Linux中,线程是复用的进程的pcb。这样就不需要单独为线程单独设计数据结构和调度算法了。
Linux中,进程和线程的关系?
[!IMPORTANT]
在Linux中,没有线程的概念,只有轻量级进程的概念。
一个进程中,可能存在不止一个执行流,也就是说,可能存在多个pcb。以前学的进程是一个进程仅存在一个pcb,而现在是一对多的关系。
Linux中没有线程的概念,用户是怎么用线程的?
[!IMPORTANT]
在Linux底层中没有线程的概念,只有轻量级进程,为了上层能用线程,Linux把它进行了封装成了线程库。所以要用到线程的时候,需要连接一个线程库。
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库
线程的唯一标识
对于同一个进程中的不同线程,两个线程的PID相同,但是LWP不同。
所以,唯一标识一个线程的是LWP(Light Weight Process:轻量级进程)。
LWP作为线程唯一标识,对单线程有影响吗?
没有影响,因为对于单进程而言,LWP 和 PID是一一对应的。
已经有多进程了,为什么还需要多线程?
[!IMPORTANT]
- 进程的创建,成本高;线程的创建,成本低。—启动
- 线程的调度成本低。—运行
- 删除一个线程的成本低。—删除
线程的优势:
线程的缺点:
对于一个进程中的多个线程,只要有一个线程出现了问题,多个线程都会崩溃。
对于不同系统实现线程的方式
[!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);
、
主线程和新线程哪一个先运行?
[!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
在新线程内部获取该线程的线程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]
所谓的对临界资源进行保护,本质是对临界区代码及进行保护!—> 我们对所有资源进行访问,本质都是通过代码进行访问的 —> 保护资源,本质就是把访问资源的代码进行保护
原理角度理解锁
[!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;
}
三、生产消费者模型—多执行流并发模型
作用
[!IMPORTANT]
- 协调忙闲不均。比如说,有10个线程,每回来认为,都使用第一个线程来处理任务,导致其他线程一直不处理任务。
- 效率高。
如何体现生产者消费者模型的效率高?
[!IMPORTANT]
1、并发
- 生产者生产任务(向缓存中输入构建好的任务)的同时,其他生产者在构建任务。
- 消费者获得任务的同时,其他消费者可能在处理任务。
2、解耦
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.不能让消费者超过生产者。
消费者:关心资源?— 数据资源。
生产者:关系资源?— 空间资源。
// 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]
>
> - 破坏死锁的四个条件。
> - 加锁顺序一致。
> - 避免锁未释放的场景。
> - 资源一次性分配。