博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

线程安全

Posted on 2020-05-08 09:53  bw_0927  阅读(265)  评论(0)    收藏  举报

·         线程安全 【单线程不存在线程安全的问题,多线程访问共享资源时才需考虑】

如果在多线程并发执行的情况下,一个函数可以安全地被多个线程并发调用,可以说这个函数是线程安全的。

调用时不需要考虑锁和资源访问控制;调用端代码无需额外的同步或其他协调动作

1.    对于简单的基本类型数据如字符、整型、指针等,C++提供了原子模版类 atomic

2.    对于复杂的对象,则提供了最常用的锁机制,比如互斥类 mutex,门锁 lock_guard,唯一锁 unique_lock,条件变量 condition_variable 等。

3.    thread_local int i; 线程本地存储 (TLS) 变量,我们只需要在变量前面声明它是 thread_local 即可。TLS 变量在线程栈内分配,线程栈只有在线程创建之后才生效,在线程退出的时候销毁

shared_ptr/weak_ptr的引用计数操作为原子操作,不需要加锁,没有线程安全问题

实际上boost::function/boost::bind是支持这种方式实现安全的函数回调。(bind内部使用了weak_ptr)

asio::async_read(m_socket, asio::buffer(&m_readBuf[0], m_readBuf.size()), boost::bind(&TcpConnection::handleReceive, shared_from_this(), asio::placeholders::error) );

当执行回调函数handleReceive时,执行时将weak_ptr提升权限如果成功则执行回调函数handleReceive,如果失败说明TcpConnection对象已销毁,不再执行回调函数

 

对象的生与死不能由对象自身拥有的mutex来保护 

C++的容器默认是非线程安全的,一个线程读,另一个线程写,迭代器很容易就会指到错误的范围地址

 

 

 

C++11的新特性中lambda几乎是专门用来解决回调函数的安全问题

 

static 与线程安全

 

 

===============

static变量初始化顺序

1.1 全局变量、文件域的static变量【即全局的static变量,它的作用域在当前文件】和类的static成员变量main函数执行之前初始化
1.2 局部静态变量第一次被使用时初始化

1.3 静态的成员变量的初始化【通过类作用域解析附Class::Static_var被引入到了全局作用域,是non-local的】,和全局对象一样,实际上是在main函数进入后,我们写下的第一行代码之前被执行的。

 

static变量初始化的线程安全问题:

初始化的线程安全问题 跟 使用时的线程安全是两个问题。       使用时,除了原子变量多线程时不需要加锁,其他的都得加锁

因为全局变量,包括文件域的static变量,类的静态成员变量,都是在main之前初始化的,所以他们的初始化过程是线程安全的。

但局部静态变量是在第一次遇到时进行初始化的,所以这就涉及到了线程安全问题; 老标准不安全【用的是flag标记】,新标准安全【用的是锁】。

 

2.1 局部静态变量的初始化过程线程安全的  【全局的静态变量的多线程读写,该加锁还是得加锁】【初始化发生在main之前,不可能存在多个线程同时执行初始化的动作】
2.2 局部静态变量的初始化:在编译时,编译器的实现一般是在初始化语句之前设置一个局部静态变量的标识来判断是否已经初始化,运行的时候每次进行判断,如果需要初始化则执行初始化操作,否则不执行。这个过程本身不是线程安全的【这是老标准的做法】

 

 https://blog.csdn.net/sdoyuxuan/article/details/85708298

【老的c++标准中】

局部的静态变量是非线程安全的,用的是标志位来判断是否已经初始化

https://blog.csdn.net/freakishfox/article/details/72765615

http://www.cppblog.com/lymons/archive/2015/09/03/120638.html

 

 

【新的c++11标准中】,用的是 guard_acquire/release 担保了它的线程安全。

https://blog.csdn.net/imred/article/details/89069750?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-4

https://www.cnblogs.com/william-cheung/p/4831085.html

https://stackoverflow.com/questions/1661529/is-meyers-implementation-of-the-singleton-pattern-thread-safe 

 

前言
大家都知道,C++11标准中,要求局部静态变量初始化具有线程安全性,所以我们可以很容易实现一个线程安全的单例类:

class Foo
{
public:
    static Foo *getInstance()
    {
        static Foo s_instance;
        return &s_instance;
    }
private:
    Foo() {}
};

  

在C++标准中,是这样描述的(在标准草案的6.7节中):

such a variable is initialized the first time control passes through its declaration;

such a variable is considered initialized upon the completion of its initialization.

If the initialization exits by throwing an exception, the initialization is not complete, so it will be tried again the next time control enters the declaration.

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

If control re-enters the declaration recursively while the variable is being initialized, the behavior is undefined.

分析
标准关于局部静态变量初始化,有这么几点要求:

  1. 变量在代码第一次执行到变量声明的地方时初始化。
  2. 初始化过程中发生异常的话视为未完成初始化,未完成初始化的话,需要下次有代码执行到相同位置时再次初始化。
  3. 在当前线程执行到需要初始化变量的地方时,如果有其他线程正在初始化该变量,则阻塞当前线程,直到初始化完成为止。
  4. 如果初始化过程中发生了对初始化的递归调用,则视为未定义行为。

关于第4点,如果不明白,可以参考以下代码:

class Bar
{
public:
    static Bar *getInstance()
    {
        static Bar s_instance;
        return &s_instance;
    }
private:
    Bar()
    {
        getInstance();
    }
};

 

函数内部的静态局部变量的初始化是在函数第一次调用时执行;

在之后的调用中不会对其初始化。 在多线程环境下,仍能够保证静态局部变量被安全地初始化,并只初始化一次。

下面通过代码来分析一些具体的细节:

void foo() {
    static Bar bar;
    // ...
}

通过观察 gcc 4.8.3 为上述代码生成的汇编代码, 我们可以看到编译器生成了具有如下语义的代码:

复制代码
void foo() {
    if ((guard_for_bar & 0xff) == 0) {
        if (__cxa_guard_acquire(&guard_for_bar)) {
            try {
                Bar::Bar(&bar);
            } catch (...) {
                __cxa_guard_abort(&guard_for_bar);
                throw;
            }
            __cxa_guard_release(&guard_for_bar);
            __cxa_atexit(Bar::~Bar, &bar, &__dso_handle);
        }
    }
    // ...
}

==========

  

 

 ===================================

1.线程安全问题都是由全局变量及静态变量引起的。但是,如果每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;如果有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

2.1) 常量始终是线程安全的,因为只存在读操作。 

2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。

3)局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。

============

 

 

前言
c++11 担保了 static 变量的初始化线程安全。但是老的c++标准并没有担保,所以说老版本的编译器可能static 变量初始化在多线程的条件下会造成问题

c++ 98/03 关于静态初始化标准
下面是老版本标准对这个问题的描述,简言而之它只是担保了local static 变量的初始化发生于当该表达式第一次执行时。

Here’s an excerpt from section 6.7 of a working draft (N1095) of the current C++ standard (C++98)
The zero-initialization (8.5) of all local objects with static storage duration (3.7.1) is performed before any other initialization takes place. A local object of POD type (3.9) with static storage duration initialized with constant-expressions is initialized before its block is first entered. An implementation is permitted to perform early initialization of other local objects with static storage duration under the same conditions that an implementation is permitted to statically initialize an object with static storage duration in namespace scope (3.6.2). Otherwise such an object is initialized the first time control passes through its declaration; such an object is considered initialized upon the completion of its initialization.

大多数编译器选择 在 全局作用域内的static 变量会在进入main函数前初始化,而 func内的local static 变量只会在该函数第一次被调用的时候初始化。这就造成了一个问题,可能函数内部的static local 变量的初始化不是线程安全的,所以我们不能假设 static local 变量的初始化就是线程安全的。

编译器的实现
demo
#include <iostream>

using namespace std;

class Foo {
public:
    Foo(const char* s = "") {
        cerr << "Constructing Foo with " << s << endl;
    }
};

void somefunc()
{
    static Foo funcstatic("funcstatic");
    Foo funcauto("funcauto");
}

static Foo glob("global");

int main()
{
    cerr << "Entering main\n";
    somefunc();
    somefunc();
    somefunc();
    return 0;
}


vs2008 实现
现在看看somefunc 这个 local static 变量初始化函数的实现。

    static Foo funcstatic("funcstatic");
00E314FD  mov         eax,dword ptr [$S1 (0E3A148h)]
00E31502  and         eax,1
00E31505  jne         somefunc+71h (0E31531h)
00E31507  mov         eax,dword ptr [$S1 (0E3A148h)]
00E3150C  or          eax,1
00E3150F  mov         dword ptr [$S1 (0E3A148h)],eax
00E31514  mov         dword ptr [ebp-4],0
00E3151B  push        offset string "funcstatic" (0E3890Ch)
00E31520  mov         ecx,offset funcstatic (0E3A14Ch)
00E31525  call        Foo::Foo (0E31177h)
00E3152A  mov         dword ptr [ebp-4],0FFFFFFFFh
    Foo funcauto("funcauto");
00E31531  push        offset string "funcauto" (0E38900h)
00E31536  lea         ecx,[ebp-11h]
00E31539  call        Foo::Foo (0E31177h)

从上面汇编看到编译器对于是否初始化只是简单的通过一个计数器的判断如下面汇编代码。那么就有可能出现俩个线程得到 0E3A148h 这个地址的值都为0,那么这个时候就会发生初始化执行俩次。这个实现也符合 老版本 c++ 标准,所以没问题

00E314FD  mov         eax,dword ptr [$S1 (0E3A148h)]
00E31502  and         eax,1
00E31505  jne         somefunc+71h (0E31531h)
1
2
3
gcc 实现
gcc 4.0 版本以上的实现,使用相同的代码 使用 g++ -O0 -g 编译。从下面的汇编可以看到 gcc 使用了 guard_acquire/release 担保了它的线程安全。
gcc 可以使用-fno-threadsafe-statics 这个选项来关闭对 static local 变量多余的线程安全的开销调用。如果关闭了生成的代码和 vs2008差不多。另一方面说,这段代码引入了一些不可移植的问题。这段代码跑在gcc上编译就没问题,使用 vs2008就不行。

0000000000400a9d <_Z8somefuncv>:
  400a9d:  55                      push   rbp
  400a9e:  48 89 e5                mov    rbp,rsp
  400aa1:  48 83 ec 40             sub    rsp,0x40
  400aa5:  b8 a8 21 60 00          mov    eax,0x6021a8
  400aaa:  0f b6 00                movzx  eax,BYTE PTR [rax]
  400aad:  84 c0                   test   al,al
  400aaf:  75 76                   jne    400b27 <_Z8somefuncv+0x8a>
  400ab1:  bf a8 21 60 00          mov    edi,0x6021a8
  400ab6:  e8 cd fd ff ff          call   400888 <__cxa_guard_acquire@plt>
  400abb:  85 c0                   test   eax,eax
  400abd:  0f 95 c0                setne  al
  400ac0:  84 c0                   test   al,al
  400ac2:  74 63                   je     400b27 <_Z8somefuncv+0x8a>
  400ac4:  c6 45 df 00             mov    BYTE PTR [rbp-0x21],0x0
  400ac8:  be aa 0c 40 00          mov    esi,0x400caa
  400acd:  bf b0 21 60 00          mov    edi,0x6021b0
  400ad2:  e8 89 00 00 00          call   400b60 <_ZN3FooC1EPKc>
  400ad7:  c6 45 df 01             mov    BYTE PTR [rbp-0x21],0x1
  400adb:  bf a8 21 60 00          mov    edi,0x6021a8
  400ae0:  e8 03 fe ff ff          call   4008e8 <__cxa_guard_release@plt>
  400ae5:  eb 40                   jmp    400b27 <_Z8somefuncv+0x8a>
  400ae7:  48 89 45 c8             mov    QWORD PTR [rbp-0x38],rax
  400aeb:  48 89 55 d0             mov    QWORD PTR [rbp-0x30],rdx
  400aef:  8b 45 d0                mov    eax,DWORD PTR [rbp-0x30]
  400af2:  89 45 ec                mov    DWORD PTR [rbp-0x14],eax
  400af5:  48 8b 45 c8             mov    rax,QWORD PTR [rbp-0x38]
  400af9:  48 89 45 e0             mov    QWORD PTR [rbp-0x20],rax
  400afd:  0f b6 45 df             movzx  eax,BYTE PTR [rbp-0x21]
  400b01:  83 f0 01                xor    eax,0x1
  400b04:  84 c0                   test   al,al
  400b06:  74 0a                   je     400b12 <_Z8somefuncv+0x75>
  400b08:  bf a8 21 60 00          mov    edi,0x6021a8
  400b0d:  e8 06 fe ff ff          call   400918 <__cxa_guard_abort@plt>
  400b12:  48 8b 45 e0             mov    rax,QWORD PTR [rbp-0x20]
  400b16:  48 89 45 c8             mov    QWORD PTR [rbp-0x38],rax
  400b1a:  48 63 45 ec             movsxd rax,DWORD PTR [rbp-0x14]
  400b1e:  48 8b 7d c8             mov    rdi,QWORD PTR [rbp-0x38]
  400b22:  e8 11 fe ff ff          call   400938 <_Unwind_Resume@plt>
  400b27:  48 8d 7d ff             lea    rdi,[rbp-0x1]
  400b2b:  be b5 0c 40 00          mov    esi,0x400cb5
  400b30:  e8 2b 00 00 00          call   400b60 <_ZN3FooC1EPKc>
  400b35:  c9                      leave
  400b36:  c3                      ret



总结
使用老版本编译器编译c++代码,还是遵循标准不要做任何假设。

参考
https://eli.thegreenplace.net/2011/08/30/construction-of-function-static-variables-in-c-is-not-thread-safe