之前日志系统的开发没有考虑线程安全的问题,本篇中综合考虑日志系统在多线程下的表现

加锁的原则:复合类型的一定考虑加锁,因为多个线程同时操作会有多个”状态”出现,产生严重的内存错误;基础类型不一定全要加锁,因为多个线程操作不会有多个状态只会造成这个变量的”含义”不准确,不至于内存错误。

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 ScopedLockImpl Lock;

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

  1. ![image.png](https://cdn.nlark.com/yuque/0/2021/png/25460685/1639108194091-cd5ed9cb-8220-498c-ab09-5c18e152a8de.png#clientId=u97e3c525-92d7-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=331&id=u4ae84e36&margin=%5Bobject%20Object%5D&name=image.png&originHeight=331&originWidth=548&originalType=binary&ratio=1&rotation=0&showTitle=false&size=53122&status=done&style=none&taskId=uc6e2fbef-f2b7-4949-851d-e23294cfdbd&title=&width=548)
  2. <a name="FHyHN"></a>
  3. ## 2.2 方案:选用原子锁atomic
  4. 理由:C++11推出的多线程下线程安全解决方案,比较方便直观。
  5. - `CASMutex`类:
  6. ```cpp
  7. //原子锁
  8. class CASMutex
  9. {
  10. public:
  11. typedef ScopedLockImpl<CASMutex> Lock;
  12. CASMutex(){ m_mutex.clear();}
  13. ~CASMutex(){}
  14. void lock()
  15. {
  16. //执行本次原子操作之前 所有读原子操作必须全部完成
  17. while(std::atomic_flag_test_and_set_explicit(&m_mutex, std::memory_order_acquire));
  18. }
  19. void unlock()
  20. {
  21. //执行本次原子操作之前 所有写原子操作必须全部完成
  22. std::atomic_flag_clear_explicit(&m_mutex, std::memory_order_release);
  23. }
  24. private:
  25. volatile std::atomic_flag m_mutex;
  26. };

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"这种看似荒谬的结果。

  1. atomic<int> a;
  2. atomic<int> b;
  3. void func1()
  4. {
  5. int t = 1;
  6. a = 1;
  7. b = 2;
  8. }
  9. void func2()
  10. {
  11. while(b != 2);
  12. cout << "a=" << a << endl;
  13. }
  14. int main()
  15. {
  16. std::thread t1(func1);
  17. std::thread t2(func2);
  18. t1.join();
  19. t2.join();
  20. return 0;
  21. }