std::string 线程安全

gcc3.3.3版本。
std::string低层使用内存池,即第一次使用std::string对象的时候,会申请960字节的内存池(一个长度为16的数组,每个里面的内存长度分别为8、16、24、32.....128,等差队列和=((8+128/2 )*15=960)。以后再次使用string,则直接从池中申请。这点可以通过valgrind验证。


实现代码里有pthread_mutex_t相关代码,
实现代码里也有pthread_spin_lock相关代码,这个应该是多核下使用的吧。
有类似如下的汇编__asm__ __volatile__ ("lock;......",看起来也是一种高效的锁实现。


sgi默认的allocator就是thread-safe
http://www.sgi.com/tech/stl/Allocators.html
看了一下libstdc++的代码。是用gcc的__gthread_mutex_lock函数保证的。
而__gthread_mutex_lock在不同平台上是不同实现的。
x86是CPU的原子操作,比如win32上的InterlockedIncrement.
arm-linux上就是用的pthread_mutex_lock了。


1、stl内存池有很多种实现,目标是尽量优化小对象的内存分配;你的数据显然没有真正反映gcc的内部实现,建议自己看看源码。



2、请注意,string是一个typedef;它是basic_string的一个特化版。
   basic_string的最后一个模板化参数用于指定内存池;默认内存池一般是全局公用的。

   另外,不仅是basic_string,凡声称自己可以支持多线程的stl组件,都会通过traits(似乎是这个词儿)自动在多线程环境下加入锁保护——当然,你也可以手工去掉它。
   具体实现各家皆有细微差别,看源码即知。

3、关于性能,论坛上颇有一番“一群阔人说要读经,嗡的一声一群狭人也说要读经”的气象。


先反问一下:知道“锁操作慢”是搞什么的人才有资格说的吗?

——对那美克星的亚光速飞船来说,F22比猎豹眼里植物生长的速度还慢。



看下数据(原始数据来自IBM;是在下为写另一帖子找的,现在原文不在,就暂时不给链接了):

对PIII 700来说,这款CPU每时钟周期可以执行两条整数指令,它的时钟周期是1.4ns。
这款CPU有两种原子操作指令,消耗时间分别是7xns和14xns的样子(具体数字忘了)。
作为对比,一旦此CPU要访问的数据不在cache中,那么哪怕只从内存中读取一个字节,也需要100多ns。

换言之: 一次原子操作,需要消耗的时间基本和一次cpu二级缓存未命中相当。



再做个对比:现在最好的磁盘,平均寻道时间大约是9ms不到,仅仅是在两个相邻磁道间移动磁头就需要消耗2ms;然后,它还需要等待对应扇区转到磁头下——对于10000转的硬盘,这个时间(平均等待时间)是3ms。

加上其他种种延迟,缓存未命中时,从磁盘读取一个字节平均需要至少21ms。
这段时间,足够老掉牙的PIII 700执行30万条原子指令(或3000万条整数指令)。



进一步的,锁的实现可分为两种:
一种是简单调用原子操作指令实现基本逻辑,由用户负责调度算法;另一种则还要通知操作系统内核,以将合适的进/线程挂起或恢复执行。

于是,一个锁操作,全部消耗可能是一次函数调用(可以被inline优化掉)+一个原子指令;也可能是一次用户态-内核态-用户态切换+若干次函数调用+一个原子操作。

这点开销,与一个本该inline而没有inline的函数所造成额外消耗基本在同一个数量级上。



总结:
1、锁速度慢是那些有能力玩优化玩到单条机器指令级的算法大佬们才有资格说的(或者说:只有当你可以准确估算出自己的代码将消耗多少个指令周期、有多少次CPU二级缓存未命中时,谈论锁对性能的影响才有意义——这时候,你也往往必须自己用CPU原始原子操作指令完成保护以提高性能了)。

2、对普通用户来说,请记住这句话:“和普通函数调用相比,锁操作基本没有额外消耗”。

3、论坛上那些不分情况不看场合张嘴就是“锁很慢”“xxx操作效率低”的牛人,他们其实对速度、性能、效率等等几乎毫无概念,只是人云亦云而已。切莫被他们误了。


new本身就有锁,不然多线程下你的堆还不被捣毁了。


malloc的测试结果也说明 单线程下,内存池引入对性能提高的重要性。


一般来说,"C++标准库的线程安全性"并没有统一明确的规定,实际上在很大的程度上取决于所用的编译
器. 通常只能说是"部分线程安全的".
  

在所有的主流STL实现方案中,几乎所有的容器都是线程安全的:  
   
  1).一个线程读写一个实例时,另一个线程可以读写另一个实例。  
   
  2).多个线程可以在同时读同一个container。  
   
  3).多个线程写同一个container时,你应该负责安排互斥性操作。  

    
   
  一个特例是std::string。在一些STL的实现厂商(包括MS   VC6),使用的是带引用计数的string!   这就意味着可能有两个std::string的实例共享着同一块底层数据。这使得前面说的第一个规则被打破! 
看一下这样的代码:  
   
  string   s1=   “abcd”;  
   
  string   s2   =   s1;  
   
     
   
  在引用计数的实现版本中,这几句话意味着:先分配一块内存给”abcd”,一个引用计数的数;s1和s2都将引用这块内存,引用计数将为2。引用计数的本意是在把strings传出functions时优化copy行为。  
   
  但是这种算法并不是线程安全的!  
   
  如果你将s2传给另一个线程,那么就很可能有两个线程企图修改这同一块内存!那将会有不可预料的行为发生。  
   
  理论上,你可以在两个线程之间增加线程同步,但是这个同步的代价将会大于你从引用计数中获得的好处!  
   
  这就是为什么主流的STL厂商不再使用引用计数的原因。比如,Dinkumware   STL   shipped   with   VC7


C++标准里,整个循环体构成一个作用域,循环不结束,作用域不结束。
//初始化语句在这个作用域里只会执行一次——这是常识。
//如果还不明白,拜托阁下给我解释下下边这个循环的执行过程:
for (int i = 0; i <= 100; i++)
{
    int j = 0;
    j += i;
    printf("%d, %d",i,j);
};

结果无论是GCC还是G++编译,结果都证明j每次都被初始化成了0

./a.out
0, 0
1, 1
2, 2

....

100,100


string的copy on write实现本身就依赖引用计数。
这种string内部实现可用伪码表示如下(为简单计,忽略一切泛型相关问题):

struct inner_storage
{
   uint ref_count(0);
   char * pMem;
}

class string
{
   private:
      inner_storage * p;
   public:
      //当前对象作为左值使用时
    {
          保存inner_storage * p
          if //赋值/初始化
       {
             //复制源string的inner_storage * p 且 p->ref_count++;
                   //多线程环境,++操作需要保护(锁,不可直接atomic_inc)
           }
           else
           {
                   //为inner_storage * p分配空间
             //这里可以优化
             //   如: 若ref_count为1则说明目前此块为本string实例独享
             //         于是前面的内存分配操作可优化掉
             //         此时后面的ref_count--也要做调整
             //视情况检查是否需要复制旧的字符串,然后完成修改动作
            }
            //旧inner_storage * p指向结构的ref_count--;re_fcount为0则删除
        //这里的--操作同样需要锁保护
      }
}


考虑多线程环境下string的copy on write究竟该如何实现。
现在看来,如果string使用了copy on write上述优化,那么锁还是逃不过。
测试结果错误,显然只与楼主的测试方法或判断有关。


另外,stl容器一般会用稍微多分配一些内存空间的办法避免多次重新分配;内容清空,内存空间一般不会归还。
stl内存池一般只为加速小块内存的使用而设计;大于一定大小的内存会直接交给系统管理;

内存归还时,也不一定第一时间把它们挂回free链表——某些基于trunk的实现会把它们加入可回收队列中;只有池中空间不足时才真正执行回收操作。

这些都是设计测试用例时必须仔细考察的。


string的线程安全性是有几个等级的。
1、完全不支持多线程
比如:它使用的内存池本身没有加锁;string内部有静态变量且没有加锁等等。



2、部分支持多线程
比如:使用的内存池有锁;string内部没有静态变量或虽然有静态变量但加了锁等等——SGI的就是这一种,如下:
The SGI STL implementation removes all nonconstant static data from container implementations. The only potentially shared static data resides in the allocator implementations. To this end, the code to implement per-class node allocation in HP STL was transformed into inlined code for per-size node allocation in the SGI STL allocators. Currently the only explicit locking is performed inside allocators.

这种库显然可以完全支持//2 的情形;但//1一定会出事。


3、完全支持多线程
比如,在2的基础上,进一步为每个string对象的每个操作都加了锁——于是,不需要做任何工作,你在任何一个线程读写string都会在内部被保护起来。

于是,这种库就可以完全支持//1了。


loki里面就做的非常清晰,它的thread头文件里面就为多线程定义了两个锁: 类级锁和对象级锁。

很显然,类级锁定保护静态变量,提供第二级的部分多线程支持;
而对象级锁保护每个实例,提供第三级的完全多线程支持。


1.用clock_gettime或者rdtsc计算cpu cycle更准确
2.多核上pthread_spin_lock比pthread_mutex_lock更快
3.string 的实现很特殊,你那个测试在某些string版本下可能是通过引用来实现的,自然很快
4.new和malloc都是带锁的
5.锁的办法有很多,这个要具体看实现,用原子操作也可以达到'锁'的目的



ISO C++
Annex C.2.11 Clause 21: strings library [diff.cpp03.strings]
21.3
Change: basic_string requirements no longer allow reference-counted strings
Rationale: Invalidation is subtly different with reference-counted strings. This change regularizes behavor for this International Standard.
Effect on original feature: Valid C++ 2003 code may execute differently in this International Standard.
具体来说:
ISO C++03
21.3/5 References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object:
— As an argument to non-member functions swap() (21.3.7.8), operator>>() (21.3.7.9), and getline() (21.3.7.9).
— As an argument to basic_string::swap().
— Calling data() and c_str() member functions.
— Calling non-const member functions, except operator[](), at(), begin(), rbegin(), end(), and rend().
— Subsequent to any of the above uses except the forms of insert() and erase() which return iterators, the first call to non-const member functions operator[](), at(), begin(), rbegin(), end(), or rend().
ISO C++11
21.4.1/6 References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object:
— as an argument to any standard library function taking a reference to non-const basic_string as an argument.234
— Calling non-const member functions, except operator[], at, front, back, begin, rbegin, end, and rend.
234) For example, as an argument to non-member functions swap() (21.4.8.8), operator>>() (21.4.8.9), and getline() (21.4.8.9), or as an argument to basic_string::swap()
允许失效的条件少了,导致无法实现引用计数:修改操作导致失效,但并不只影响被修改的basic_string对象自身,还影响其它的实例——此时它(们)没在C++11允许失效范围之内,这违反标准的约束。
(C++03这里还有个小bug在CD1解决,应该允许构造后直接调用而不止subsequent to ... call引起的失效。)


g++的std::string实现是使用copy on write的引用计数实现,现在来看已经是糟糕实现了

你可能感兴趣的:(std::string 线程安全)