之前日志系统的开发没有考虑线程安全的问题,本篇中综合考虑日志系统在多线程下的表现
加锁的原则:复合类型的一定考虑加锁,因为多个线程同时操作会有多个”状态”出现,产生严重的内存错误;基础类型不一定全要加锁,因为多个线程操作不会有多个状态只会造成这个变量的”含义”不准确,不至于内存错误。
1. 改动:对类中可能需要加锁的地方进行加锁
目的:由于日志系统中很多变量属于写多读少的情形,选用普通锁Mutex
比较合理。
1.1 加锁过的地方:
LogFormatter
类中:setFormatter(LogFormatter::ptr val)
getFormatter()
Logger
类中:addAppender(LogAppender::ptr appender)
m_appenders
输出器队列
delAppender(LogAppender::ptr appender)
clearAppender()
setFormatter(const std::string& val)
m_appenders
输出器队列
getFormatter()
toYamlString()
log(...)
FileLogAppender
类中:reopen()
log(...)
toYamlString()
StdoutLogAppender
类中:log(...)
toYamlString()
2. 由于锁的性能不行,考虑换锁
2.1 方案:选用SpinLock自旋锁
理由:日志系统中涉及的修改变量的工作并不复杂,能实现互斥保护共享资源的需求即可。
SpinMutex
类: ```cpp //自旋锁 class SpinMutex { public: typedef ScopedLockImplLock; SpinMutex() {pthread_spin_init(&m_mutex, 0);}
~SpinMutex() {pthread_spin_destroy(&m_mutex);}
void lock() {pthread_spin_lock(&m_mutex);}
void unlock() {pthread_spin_unlock(&m_mutex);}
private: pthread_spinlock_t m_mutex; };

<a name="FHyHN"></a>
## 2.2 方案:选用原子锁atomic
理由:C++11推出的多线程下线程安全解决方案,比较方便直观。
- `CASMutex`类:
```cpp
//原子锁
class CASMutex
{
public:
typedef ScopedLockImpl<CASMutex> Lock;
CASMutex(){ m_mutex.clear();}
~CASMutex(){}
void lock()
{
//执行本次原子操作之前 所有读原子操作必须全部完成
while(std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire));
}
void unlock()
{
//执行本次原子操作之前 所有写原子操作必须全部完成
std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
}
private:
volatile std::atomic_flag m_mutex;
};
C++知识点补充复习1:SpinLock自旋锁
概念:当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么当前线程将循环等待,不断去检查锁是否能被获取,直到获取到锁会退出死循环。自旋锁里其实没有锁,是一种无锁算法,依靠的是CAS算法原理。
涉及算法理念:CAS算法(compare and swap),是一种乐观锁机制。
- 和互斥锁Mutex的异同:
- 相同点:
都是为了保护共享资源,在任意时刻,只让一个线程持有共享资源。
- 不同点:
① 互斥锁,当前线程发现资源被占用,则会进入休眠,锁释放后再对线程进行唤醒。
②自旋锁,当前线程发现资源被占用,则会死循环并不断检查是否可以获得锁,获得锁退出循环
- 意义:休眠再唤醒线程的过程,开销是巨大的。在CPU性能消耗不大的情况,或者说线程执行的不是很重的任务的情况下,可以将线程”空转”一会儿,保持”热度”。无锁自然快。
C++知识点补充复习2:原子操作atomic
- 为何C++11后会出现原子操作以及原子类型?
- 出处《深入理解C++11新特性解析与应用》P214
答:
在C++11之前,C\C++一直是一种顺序编程语言,即:所有指令都是串行执行的。但随着多核处理器的风潮趋势,编程语言也随着向着并行化编程方式发展。
并行编程的模型包括:多线程、共享内存、消息传递等。多线程的优势在于:同一时间有多个处理器单元执行进程中一段统一的代码部分,借由分离的栈空间、共享数据区和堆栈区域,线程可以拥有独立的执行状态和快速的数据通信。
但是C\C++新标准迟迟没有推出相关的多线程的库,一直都是POSIX的C语言pthread
库顶替。于是C++11中引入原子操作的概念来弥补空缺。
概念:所谓的原子操作,就是多线程环境下,程序中”最小的且不可并行化”的操作。即:对一个共享资源的操作属于原子操作的话,意味着多线程访问该资源时,有且仅有唯一一个线程在对该资源进行操作。
意义:为并行编程提供”去锁化”可能,提供无锁编程。方便理解和操作。
- 如果仅仅只是满足线程间的同步,原子类型可以满足,但这是建立在内存模型为顺序一致性的前提下,实际情况并非如此。
冷知识:
①首先理解内存模型(memory model),即:机器指令(可以理解为汇编)将被处理器以什么样的顺序被执行。按照设计者写的指令顺序,一致执行下去的为强顺序型;反之,按照处理器打乱的顺序执行的为弱顺序型。
②由于不同平台的处理器和编译器的策略不同,进而对语句执行顺序进行重排序。这种隐匿的重排有时候会导致非常严重的问题:
如下代码,大概含义:以强顺序内存模型为前提,func2
函数中总是等待a=1
被执行后,才执行b=2
,最终打印的永远都应该是"a=1"
。但是弱顺序型,就有可能导致b=2
锁对应的机器指令被放到了a=1
前面,导致打印结果会出现"a=0"
这种看似荒谬的结果。
atomic<int> a;
atomic<int> b;
void func1()
{
int t = 1;
a = 1;
b = 2;
}
void func2()
{
while(b != 2);
cout << "a=" << a << endl;
}
int main()
{
std::thread t1(func1);
std::thread t2(func2);
t1.join();
t2.join();
return 0;
}