Cpp 单例模式的线程安全的探讨

预备知识

静态变量的构造和析构顺序

See Destruction order of static objects in C++

静态变量之间的构造顺序是不确定的。但是可以通过 global-static 的方式手动的控制它们的构造顺序:

for example:

file_system.cc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

class FileSystem {
public:
  FileSystem() {
    std::cout << "FileSystem()" << std::endl;
  }
  ~FileSystem() {
    std::cout << "~FileSystem()" << std::endl;
  }
};

FileSystem& tfs() {
  static FileSystem fs;
  return fs;
}

directory.cc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <iostream>

class Directory {
public:
  Directory() {
    std::cout << "Directory()" << std::endl;
  }
  ~Directory() {
    std::cout << "~Directory()" << std::endl;
  }
};

Directory& tdir() {
  static Directory dir;
  return dir;
}

main.cc:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <iostream>

class FileSystem {};
class Directory {};
extern FileSystem& tfs();
extern Directory& tdir();

int main() {
  tdir();
  tfs();

  std::cout << "main end" << std::endl;
  return 0;
}

静态变量的析构顺序是和构造顺序顺序相反的。比如:

1
2
3
4
5
6
7
int main() {
  tfs();
  tdir();

  std::cout << "main end" << std::endl;
  return 0;
}

静态变量的初始化

静态变量的初始化是否是线程安全的,是由编译器决定的:

  • VS2013 中不是线程安全的, VS2014 是线程安全的。See ref
  • C++11 之后的标准是线程安全的。

在C中,静态变量的初始化器必须是常量,在C++中,引入了类, 静态变量可以用非常量初始化, 所以要解决静态变量不使用常量初始化的问题。那么 C++ 是如何保证静态变量的初始化语义(只初始化一次)的呢:

for example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct Foo {
  Foo() {
    abcdefg = 12345678;
  }
  int abcdefg;

  Foo& operator+(int i) {
    abcdefg += i;
    return *this;
  }
};

Foo& get_instance() {
  static Foo local_static_var = Foo() + 1 + 2 + 3;
  return local_static_var;
}

int main() {
  auto v = get_instance();

  return 0;
}
1
2
g++ -S main.cc -std=c++17
cat main.s | c++filt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
get_instance():
.LFB4:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	subq	$16, %rsp
	movq	%fs:40, %rax
	movq	%rax, -8(%rbp)
	xorl	%eax, %eax

    // 判断 local_static_var 是否已经被初始化
	movzbl	guard variable for get_instance()::local_static_var(%rip), %eax
	testb	%al, %al
	sete	%al
	testb	%al, %al
	je	.L5         // 如果初始化了, 跳到 .L5

    // 调用加锁函数
	leaq	guard variable for get_instance()::local_static_var(%rip), %rax
	movq	%rax, %rdi
	call	__cxa_guard_acquire@PLT

    // 判断 `__cxa_guard_acquire@PLT` 的返回值, 判断 local_static_var 是否已经被初始化?
	testl	%eax, %eax
	setne	%al
	testb	%al, %al
	je	.L5

    // Foo() + 1 + 2 + 3
	leaq	-12(%rbp), %rax
	movq	%rax, %rdi
	call	Foo::Foo()
	leaq	-12(%rbp), %rax
	movl	$1, %esi
	movq	%rax, %rdi
	call	Foo::operator+(int)
	movl	$2, %esi
	movq	%rax, %rdi
	call	Foo::operator+(int)
	movl	$3, %esi
	movq	%rax, %rdi
	call	Foo::operator+(int)
	movl	(%rax), %eax
	movl	%eax, get_instance()::local_static_var(%rip)

    // 调用解锁函数
	leaq	guard variable for get_instance()::local_static_var(%rip), %rax
	movq	%rax, %rdi
	call	__cxa_guard_release@PLT
.L5:
    // 返回 local_static_var
	leaq	get_instance()::local_static_var(%rip), %rax
	movq	-8(%rbp), %rdx
	subq	%fs:40, %rdx
	je	.L7
	call	__stack_chk_fail@PLT

从上可知, 是对 Foo() + 1 + 2 + 3 加锁保护初始化的, 而不是只对静态变量的内存加锁保护。

main 结束之后, 什么时候回收静态变量?

静态变量是属于进程的。进程结束时会调用 exit(), 静态变量会被回收。

如果 main 结束之后, 如果还有 detach 线程, 则不同的操作系统会有不同的处理。有些调用 exit, detach 线程会退出。有些调用 pthread_exit, 会等待 detach 线程结束。ref

“Unix/Linux系统编程手册"一书的说法: 其他线程调用了 exit(),或是主线程执行 return 语句(return 之后会调用 exit)时,即便遭到分离的线程也还是会受到影响。此时,不管线程处于可连接状态还是已分离状态,进程的所有线程会立即终止。

Linux/Unix 系统: 进程 exit 的过程, 个人猜测应该会保证所有线程 exit 之后, 才回收静态变量。这样能保证线程可以安全地使用进程的静态变量。

各种实现版本

Scott Meyers 优雅的单例模式 (懒汉模式):

1
2
3
4
Foo& getInst() {
  static Foo inst(...);         // 从"静态变量的初始化"可知, 进入 static 语句和退出 static 语句是加锁保护的, 所以是线程安全的。
  return inst;
}

unique_ptr 版本:

1
2
3
4
5
6
struct Singleton {
  static Singleton* get_instance() {
    static auto inst = make_unique<Singleton>();
    return inst.get();
  }
};

如果静态变量的初始化是线程安全的,那么这个实现是线程安全的。如果单例对象和其他静态对象有关联, 返回指针则会出现析构顺序的问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <vector>
#include <iostream>
#include <memory>

class FileSystem {
public:
  FileSystem() {
    std::cout << "FileSystem" << std::endl;
  }
  ~FileSystem() {
    std::cout << "~FileSystem" << std::endl;
  }

  static FileSystem* get_instance() {

    static auto inst = std::make_unique<FileSystem>();
    return inst.get();
  }

  void close_files() {
    for (const auto& f : m_files) {
      std::cout << f << std::endl;
    }
  }

private:
  std::vector<std::string> m_files;
};

class Diretory {
public:
  ~Diretory() {
    std::cout << "~Diretory" << std::endl;
    close_files();
  }

  static Diretory* get_instance() {
    static auto inst = std::make_unique<Diretory>();
    return inst.get();
  }

  void close_files() {
    FileSystem::get_instance()->close_files();
  }
};

int main() {
  Diretory::get_instance()->close_files();

  return 0;
}

Directory 对象先构造, 然后是 FileSystem 对象。main 函数结束后, FileSystem 对象先析构,然后是 Diretory 对象。但是 Directory 的析构函数调用 close_files, close_files 调用 FileSystem::get_instance(), 但是这时 main 已经结束了, 系统开始回收进程的资源, 这时再在静态区创建对象, 会出现问题。为了解决这个问题, 返回 shared_ptr 即可: ref:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

#include <vector>
#include <iostream>
#include <memory>

class FileSystem {
public:
  FileSystem() {
    std::cout << "FileSystem" << std::endl;
  }
  ~FileSystem() {
    std::cout << "~FileSystem" << std::endl;
  }

  static std::shared_ptr<FileSystem> get_instance() {

    static auto inst = std::make_shared<FileSystem>();
    return inst;
  }

  void close_files() {
    for (const auto& f : m_files) {
      std::cout << f << std::endl;
    }
  }

private:
  std::vector<std::string> m_files;
};

class Diretory {
public:
  ~Diretory() {
    std::cout << "~Diretory" << std::endl;
    close_files();
  }

  static Diretory* get_instance() {
    static auto inst = std::make_unique<Diretory>();
    return inst.get();
  }

  void init() {
    m_fs = FileSystem::get_instance();
  }

  void close_files() {
    FileSystem::get_instance()->close_files();
  }

private:
  std::shared_ptr<FileSystem> m_fs;
};

int main() {
  Diretory::get_instance()->init();

  Diretory::get_instance()->close_files();

  return 0;
}

单例模式的 get_instance 是很可能是调用比较频繁的, get_instance 返回会构造 shared_ptr, 而 shared_ptr 是线程安全的, 所以会有一点性能损耗。

C++11 提供中的 std::call_once: 如果使用 pthread 则使用 pthread_once。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

#include <thread>
#include <vector>
#include <iostream>
#include <mutex>

class Singleton {
public:
  static Singleton* get_instance() {
    std::call_once(m_once_flag, []() {
        m_instance.reset(new Singleton());
    });

    return m_instance.get();
  }

private:
  static std::once_flag m_once_flag;
  static std::unique_ptr<Singleton> m_instance;
};

std::once_flag Singleton::m_once_flag;
std::unique_ptr<Singleton> Singleton::m_instance = nullptr;

int main() {
  std::vector<std::thread> v;
  for (int i = 0; i < 100; i++) {
    v.emplace_back([](){
        auto inst = Singleton::get_instance();
        if (!inst) {
          std::cerr << "inst is nullptr" << std::endl;
        }
      });
  }

  for (auto& e : v) {
    e.join();
  }

  return 0;
}

如果有关联其他静态变量,get_instance 返回 shared_ptr 即可。

如果是静态变量的初始化是线程安全的编译标准, 该选择哪个实现版本? 最安全不一定是最适合的, 我要依据自己的情况来选择合适的版本:

  • Scott Meyers 优雅的单例模式是可以解决大多数情况的。如果该静态变量不关联其他静态变量, 选择该版本即可。同时 get_instance 是比较频繁的, 返回指针效率是比较高的。link
  • 如果该单例不关联其他静态变量, 选择 unique_ptr 版本即可。link
  • 如果该单例关联其他静态变量, 选择 sharded_ptr 版本即可。link
  • 如果想要用 call_once, 选择 call_once 版本即可。link

原子操作的版本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class singleton {
public:
    static singleton* instance() 
    {
        singleton* ptr = inst_ptr_.load(std::memory_order_acquire);     // 1
        if (inst_ptr_ == nullptr)
        {
            std::lock_guard<std::mutex> lk(mutex_);
            ptr = inst_ptr_.load(std::memory_order_relaxed);            // 2
            if (inst_ptr_ == nullptr) {
                ptr = new singleton();                                  // 4
                inst_ptr_.store(ptr, std::memory_order_release);        // 3
            }
        }
        return ptr;
    }
private:
    singleton() {}
    singleton(const singleton&) {}
    singleton& operator = (const singleton&);
private:
    static std::atomic<singleton*> inst_ptr_;
    static std::mutex mutex_;
};

std::atomic<singleton*> singleton::inst_ptr_;
std::mutex singleton::mutex_;

是线程安全的。我觉得 1, 3 可以改为 memory_order_relaxed。因为 3 依赖 4, 指令重排后, 4 是保证先序于 3 的, 且没有其他各个线程共享的内存需要修改, 所以就不需要用 release-acquire 同步修改。同时 mutex/sem 之类的同步原语会使用内存屏障同步修改。See ref

References

上述不正确之处, 欢迎指出。

0%