싱글 톤 모드의 스레드 안전성에 대해 이야기하기

1. 함수에서 정적 지역 변수의 초기화를 깊이 이해합니다
. 1.1 장면 분석
먼저 코드를 분석합니다.

#include<iostream>
using namespace std;
void foo(bool recur);
int bar(bool recur) {
    
    
 cout<<"bar called\n";
 if(recur) {
    
    
    foo(false);
 }
 return 0xFAFAFA;
}
void foo(bool recur) {
    
    
 cout<<"foo called\n";
 static int i = bar(recur);
 cout<<"Static is:"<< i<<"\n";
}
int main() {
    
    
 foo(true);
 return 0;
}

위의 코드는 정상적으로 보이지만 실행 한 후에는 다음과 같습니다.

$ g++ test.cpp
$ ./a.out
foo called
bar called
foo called
terminate called after throwing an instance of '__gnu_cxx::recursive_init_error'
  what():  std::exception
Aborted (core dumped)

1.2. 소개
먼저 재귀 적 잠금을 도입합니다. 재귀 적 잠금은 단일 스레드의 잠금을 나타냅니다. 예를 들어 동일한 함수가 잠금을 여러 번 사용합니다. 잠금 해제 중첩은 잠기지 않지만 다중 스레드 액세스는 차단을 유발합니다. 비 재귀 잠금은 수행합니다. 중요하지 않습니다. 단일 스레드 또는 다중 스레드가 차단됩니다.
지역 정적 변수를 초기화하면 그 안에서 무슨 일이 일어날까요?

  if (obj_guard.first_byte == 0)
  {
    
    
    if ( __cxa_guard_acquire (&obj_guard) ) {
    
    
      try {
    
    
      // ... initialize the object ...;
      } catch (...) {
    
    
        __cxa_guard_abort (&obj_guard);
        throw;
      }
      // ... queue object destructor with __cxa_atexit() ...;
      __cxa_guard_release (&obj_guard);
    }
  }
 guard_for_bar 是⼀个⽤来保证线程安全和⼀次性初始化的整型变量,是编译器⽣成的,存储在 bss段。它的最低的⼀个字节被⽤作相应静态变量是否已被初始化的标志, 若为 0 表示还未被初始化,否则表示已被初始化,它的第二个标志是判断是否正在使用的。__cxa_guard_acquire 实际上是⼀个加锁的过程, 相应的 __cxa_guard_abort 和__cxa_guard_release 释放锁 。

C ++ 코드로 모방 할 수 있습니다.

// Double check that the initializer has not already been run
if ( initializerHasRun(guard_object) ) // 如果对象已被初始化
    return 0;
// We now need to acquire a lock that allows only one thread
// to run the initializer. If a different thread calls
// __cxa_guard_acquire() with the same guard object, we want
// that thread to block until this thread is done running the
// initializer and calls __cxa_guard_release(). But if the same
// thread calls __cxa_guard_acquire() with the same guard object,
// we want to abort.
// To implement this we have one global pthread recursive mutex
// shared by all guard objects, but only one at a time.
int result = ::pthread_mutex_lock(guard_mutex());
if ( result != 0 ) {
    
    
	abort_message("__cxa_guard_acquire(): pthread_mutex_lock failed with %d\n", result);
} 
// At this point all other threads will block in __cxa_guard_acquire()
// Check if another thread has completed initializer run
if ( initializerHasRun(guard_object) ) {
    
     // 再次判断, 对象是否已被其他线程初始化
    int result = ::pthread_mutex_unlock(guard_mutex());
    if ( result != 0 ) {
    
    
    abort_message("__cxa_guard_acquire(): pthread_mutex_unlock failed with %d\n",         result);

} 

    return 0;

} 
// The pthread mutex is recursive to allow other lazy initialized
// function locals to be evaluated during evaluation of this one.
// But if the same thread can call __cxa_guard_acquire() on the
// *same* guard object again, we call abort();
if ( inUse(guard_object) ) {
    
    
	abort_message("__cxa_guard_acquire(): initializer for function local static variable called enclosing function\n");
} 
// mark this guard object as being in use
setInUse(guard_object);
// return non-zero to tell caller to run initializer
return 1;
}

}
위의 프로그램 덤프의 이유는 처음에 초기화되지 않고 잠금을 획득 한 후 bar () (현재 잠금 해제되지 않음)를 호출하여 문제를 해결 한 다음 foo를 호출하기 때문입니다. 시간이 지남에 따라 잠금에 문제가 없으며 사용 상태, 즉 사용 중임을 확인하고 이때 예외가 발생합니다. 이것에 반영 :

if ( inUse(guard_object) ) {
    
    
	abort_message("__cxa_guard_acquire(): initializer for function local static variable called enclosing function\n");
}
如果局部静态变量内部不是用的递归锁而是用的非递归锁会出现什么问题呢,那应该就是死锁了。
这里有个说的很好的引入一下https://manishearth.github.io/blog/2015/06/26/adventures-in-systems-programming-c-plus-plus-local-statics/

둘째, 싱글 톤 모드
2.1, 원래 lazy 싱글 톤 모드 lazy singleton은 싱글 톤 객체를 사용해야 할 때만 싱글 톤 객체를 생성하는 것입니다.

class Singleton {
    
    
private:
    static Singleton *m_singleton;
    Singleton() = default;  // 自动生成默认构造函数
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    static Singleton* getInstance(){
    
    
        if (Singleton::m_singleton == nullptr){
    
    
            std::this_thread::sleep_for(std::chrono::milliseconds(10)); //休眠,模拟创建实例的时间
            m_singleton = new Singleton();
        }
        return m_singleton;
    }
};
// 必须在类外初始化
Singleton* Singleton::m_singleton = nullptr;
Singleton::GarbageCollector Singleton::m_gc;

2.2, 스레드로부터 안전한 지연 싱글 톤 모드

// 2 线程安全的懒汉式单例模式
//线程安全的懒汉式单例
class Singleton {
    
    
private:
    static Singleton *m_singleton;
    static mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = delete; // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete;  // 禁用拷贝赋值操作符
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    static Singleton* getInstance() {
    
     // 加锁的粒度大,效率较低, 对高并发的访问
        m_mutex.lock(); // 加锁,保证只有一个线程在访问下面的语句
        if (Singleton::m_singleton == nullptr){
    
    
//            std::this_thread::sleep_for(std::chrono::milliseconds(1000)); //休眠,模拟创建实例的时间
            m_singleton = new Singleton();
        }
        m_mutex.unlock();//解锁
        return m_singleton;
    }
};
Singleton* Singleton::m_singleton = nullptr;
mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc;
这种方式的确定很明显,在高并发访问实例时性能低下

2.3 초기 인스턴스 문을 잠근 후 인스턴스가 다시 생성되는지 확인

class Singleton {
    
    
private:
    static Singleton *m_singleton;
    static mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            if (Singleton::m_singleton) {
    
    
                cout << "free m_singleton\n";
                delete Singleton::m_singleton;
                Singleton::m_singleton = nullptr;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    void *getSingletonAddress() {
    
    
        return m_singleton;
    }
    static Singleton* getInstance() {
    
    
        if (Singleton::m_singleton == nullptr){
    
    
            m_mutex.lock();  // 加锁,保证只有一个线程在访问线程内的代码
            if (Singleton::m_singleton == nullptr) {
    
     //再次检查
                m_singleton = new Singleton();  // 对象的new不是原子操作 1、分配内存,2 调用构造,3 赋值操作,到第3步的时候才是m_singleton非空
                                                //  1、分配内存,2 赋值操作 3 调用构造,到第2步的时候才是m_singleton非空
            }
            m_mutex.unlock();//解锁
        }
        return m_singleton;
    }
};
Singleton* Singleton::m_singleton = nullptr;
mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc

이런 식으로 문제는없는 것 같지만 실제로는 여전히 문제가 있습니다.
Double check lock, 그러나 C ++가 객체를 생성하기 때문에 메모리 읽기 및 쓰기 재정렬이 안전하지 않기 때문에 1, 메모리 할당, 2 호출 생성 및 3 할당 작업의 세 가지 작업을 수행합니다.
그러나 최신 CPU 및 컴파일러는 종료 될 수 있습니다. 높은 동시성 하에서 순서의 순서. 작업이 재 배열되므로 새 CSingleton 개체를 만드는 두 번째 단계가 세 번째 단계보다 늦게 호출
되어 정의되지 않은 동작이 발생할 수 있습니다.
m_singleton = new Singleton ();은 원자 연산이 아니기 때문에 다음과 같은 상황이있을 수 있습니다.
1. 메모리 할당, 2 호출 생성, 3 할당 연산, m_singleton은 세 번째 단계까지 비어 있지 않습니다.
1. 메모리 할당, 2 할당 연산, 3 호출 구조 및 m_singleton은 두 번째 단계까지 비어 있지 않습니다.
첫 번째는 이상적이지만 두 번째에는 문제가 있습니다. 예를 들어 1이 메모리를 할당하고 2가 값을 할당하면 세 번째 단계가 있습니다. getInstance () 함수에 액세스하는 다른 스레드가 있으면 Singleton :: m_singleton을 판단합니다. NULL이 아닌 경우 외부에서 Singleton을 호출 한 다른 작업으로 돌아갑니다 (현재 호출 생성의 세 번째 단계가 완료되지 않았다고 가정). 이때 프로그램의 동작이 덤프됩니다.
2.4, C ++ 11 버전 이후 크로스 플랫폼 구현

class Singleton {
    
    
private:
    static std::atomic<Singleton*> m_instance;
    static std::mutex m_mutex;
    Singleton() = default;
    Singleton(const Singleton& s) = default;
    Singleton& operator=(const Singleton& s) = default;
    class GarbageCollector {
    
    
    public:
        ~GarbageCollector() {
    
    
            cout << "~GarbageCollector\n";
            Singleton* tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp) {
    
    
                cout << "free m_singleton: " << tmp << endl;
                delete tmp;
            }
        }
    };
    static GarbageCollector m_gc;
public:
    void *getSingletonAddress() {
    
    
        return m_instance;
    }
    static Singleton* getInstance() {
    
    
        Singleton* tmp = m_instance.load(std::memory_order_relaxed);

        std::atomic_thread_fence(std::memory_order_acquire);//获取内存fence

        if (tmp == nullptr) {
    
    
            std::lock_guard<std::mutex> lock(m_mutex);
            tmp = m_instance.load(std::memory_order_relaxed);
            if (tmp == nullptr) {
    
    
                tmp = new Singleton();  // 1、分配内存,2 调用构造,3 赋值操作
                std::atomic_thread_fence(std::memory_order_release);//释放内存fence
                m_instance.store(tmp, std::memory_order_relaxed);
            }
        }
        return tmp;
    }
};
std::atomic<Singleton*>  Singleton::m_instance;
std::mutex Singleton::m_mutex;
Singleton::GarbageCollector Singleton::m_gc;

2.5 게으른 남자 추천

// 懒汉式
class Singleton {
    
    
private:
//    Singleton() = default;  // 自动生成默认构造函数
    Singleton() {
    
                               // 构造函数 会影响局部静态变量, 不能用隐式的构造函数
        cout << "Singleton construct\n";
    }
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
public:
    static Singleton* getInstance(){
    
    
        static Singleton s_singleton;  // C++11线程安全, C++11之前不是线程安全  __cxa_guard_acquire 和 __cxa_guard_release
        return &s_singleton;
    }
};

이 메서드는 간단하고 스레드로부터 안전합니다. 위에서 논의한 로컬 정적 변수 static Singleton s_singleton은 내부 잠금에 대해 스레드로부터 안전합니다. 따라서이 방법은 실제 개발에 권장됩니다.
2.6 배고픈 중국 스타일, 주 기능이 실행되기 전에 초기화, 절대적으로 안전함

// 饿汉式,在main函数运行前初始化,绝对安全
class Singleton {
    
    
private:
    //    Singleton() = default;   // 自动生成默认构造函数
    Singleton() {
    
    
        cout << "Singleton construct\n";
    }
    Singleton(const Singleton& s) = delete;    // 禁用拷贝构造函数
    Singleton& operator=(const Singleton& s) = delete; // 禁用拷贝赋值操作符
    static Singleton m_singleton;
public:
    static Singleton* getInstance(){
    
    
        return &m_singleton;
    }
};
Singleton Singleton::m_singleton;

3. 요약
싱글 톤 모드에는 여러 가지 버전이 있으며 여기에서는 다섯 번째 유형의 게으른 남자를 추천합니다. 크게 게으른 남자 형으로 나뉘는데, 일반적으로 주 함수 시작 전에 생성 된 정의는 굶주린 남자 형에 속하고, 주 함수 실행 후 생성되는 호출은 게으른 남자 형에 속합니다. 게으른 남자 모드는 다음과 같습니다. 처음 호출 될 때만 초기화됩니다., 배고픈 사람 모드 프로그램은 처음에 초기화되고 공간은 시간과 교환됩니다.

추천

출처blog.csdn.net/wangrenhaioylj/article/details/108937427