STL中使用引用计数的数据结构存在哪些行为异常

What are some behavioral anomalies with data structures that use reference counting in STL?

本文关键字:存在 异常 数据结构 引用 STL      更新时间:2023-10-16

Scott Meyer在《有效STL》中说,在决定使用哪个数据结构时,需要考虑的一件事是容器是否使用引用计数。他说,这种方法存在一些行为异常。

它们中的一些是什么?为什么像"string"answers"rope"这样的容器会有异常行为?

正如其他人所说,典型的例子是std::string。除了多线程程序中锁定的性能问题外,引用计数字符串也存在无线程问题。想象一下:

string s = "hello";
string t = s;                   // s and t share data
char &c = t[0];                 // copy made here, since t is non-const

问题是,如果该字符串是共享的,则非常常量operator[]必须复制该字符串,因为返回的引用稍后可以用来修改该字符串(您可以将其分配给非引用char,但operator[]不知道它的行为应该有任何不同)。另一方面,constoperator[]应避免复制,因为这将消除引用计数的所有好处(这意味着在实践中总是复制)。

const char &get_first(const string &s) {
    return s[0];                // no copy, s is const
}
string s = "hello";
string t = s;                   // s and t share data
const char &c1 = get_first(t);  // no copy made here
const char &c2 = t[0];          // copy made, since t is non-const
// c1 just got invalidated (in fact, it's pointing at s[0], not t[0]).
s[0] = 'X';
printf("%c, %cn", c1, c2);     // outputs "X, h"

正如你所看到的,这种区别是令人困惑的,可能会导致真正意想不到的行为。

下面是一篇关于写时复制语义及其对性能的影响的旧文章:http://www.gotw.ca/gotw/045.htm.

以下是将std::string更改为不包含在C++11标准中的参考的建议:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2534.html.这就是上面的例子所基于的。

作为一个示例,引用计数字符串,特别是具有"子部分"处理(具有起始/结束切片)的字符串可能发生的一个异常是"不幸锁定"。

假设您为文件的整个文本分配内存。然后解析文件并使用一些"slice()"、"left()"answers"mid()"或等效方法。您可能会锁定文件的整个字符串,而其中可能只有很小的一部分包含实际的文本数据(剩下的是已经解析过的数字、标点符号或其他什么)。因此,您最终可能使用了超过必要的内存,同时更容易控制峰值使用率。在这种情况下,如果您使用多线程并在不同的线程中密集使用一些字符串,可能会出现第二个问题:不必要的内存争用,字符串的引用计数可能会一直增加/减少,原子性可能会变为整数,从而减慢所有线程的速度。

不过,只要您知道应用程序中的潜在问题并防止它们(在这种情况下,只需通过复制字符串来"单独"创建字符串),就没有什么反对引用计数的。

通常,引用计数会遇到常见的多线程问题,如竞争条件、死锁或过度同步。

然后你会遇到上下文问题,这通常需要类似闭包的行为,即对象可能需要在它们明显超出范围后被捕获,但STL可以避免这种情况,我不是STL专家。

这里有一个讨论,讨论了与智能指针相关的各种巴洛克边缘情况:http://www.velocityreviews.com/forums/t689414-c-primer-4th-edition-reference-counting-smart-pointers.html

在第13项中,Meyers详细阐述了多线程和重计数字符串的问题。

这在很大程度上取决于std::string和锁定的确切实现以及使用模式。
如果在多线程环境中明显无害地使用std::string会因为隐藏的锁而导致延迟,则可能会成为问题。这样的锁和上下文切换在循环中的代价可能是巨大的。但它永远不应该造成僵局。
这不一定是个问题。这本书已有十多年的历史了。与此同时,线程实现也有所改进。例如,Linux Futex在大多数情况下表现得更加流畅。

另一点:(我不知道Meyers是否也讨论过…)
引用计数的std::string意味着它具有写时复制语义。这通常是一件好事。实际的副本会推迟到实际需要时才提供。但这也意味着复制品的价格必须在一个可能很难预测的点上支付。