首页 > 编程知识 正文

Linux多线程命令,linux多线程pthread

时间:2023-05-04 12:23:21 阅读:156756 作者:1042

威武的发夹(giantchen_AT_gmail_DOT_com ) ) ) ) ) ) )威武的发夹) ) ) 65 )

2012-01-28

我在《Linux 多线程服务端编程:使用 muduo C++ 网络库》第1.9节《关于shared_ptr的线程安全》中写道。

[shared_ptr]引用计数本身是安全的,未锁定,但不会读写对象。 shared_ptr有两个数据成员,因此无法原子执行读取/写入操作。 根据文档(http://www.boost.org/doc/libs/release/libs/smart _ ptr/shared _ ptr.htm # threadsafety )

可以通过多个线程同时读取一个shared_ptr对象实体(文档示例1 );

两个shared_ptr对象实体可以在两个线程上同时写入。 (例2 ),“析构函数”是写入操作。

如果要从多个线程读写同一shared_ptr对象,则必须将其锁定。 (例3~5 )。

请注意,以上内容是shared_ptr对象本身的线程安全级别,而不是受管理对象的线程安全级别。

后文(p.18 )介绍了如何有效地解锁。 本文具体分析了为什么“因为shared_ptr有两个数据成员,并且读/写操作不可原子化”需要锁定才能通过多线程读写同一个shared_ptr对象。 这个我似乎有人对明显的结论抱有疑问,那将会导致灾难性的后果。 该文以boost:shared_ptr为例,可能与std:shared_ptr略有不同。

shared_ptr的数据结构

shared_ptr是引用计数(reference counting )智能指针,大多数实现采用在堆(heap )上放置计数值(count )的方法(理论上还有使用循环链接列表的其他方法) 具体来说,shared_ptr包含两个成员:指向Foo的指针ptr和指向堆上的ref_count对象的ref_count指针。 ref_count指针类型不一定是原始指针,也可能是class类型,但不影响这里的讨论。 ref_count对象有多个成员。 具体的数据结构如图1所示。 其中deleter和allocator是可选的。

图1:shared_ptr的数据结构。

为了简化和强调重点,后一句中只画use_count :

以上为shared_ptrx(newfoo ); 对应的内存数据结构。

shared_ptr y=x; 那么,对应的数据结构如下。

但是,y=x与两个成员的复制有关,这两个阶段的复制不会同时发生(原子)。

中间步骤1,复制ptr指针:

在中间步骤2中,复制ref_count指针并对引用计数加1。

步骤1和步骤2的优先级与实现相关,因此在步骤2中没有描绘y.ptr的指向),我看到的都是先1后2。

因为y=x有两个步骤,所以如果没有mutex保护,多线程就有race condition。

多线程未受保护的读/写shared_ptr可能发生的race condition

考虑一下简单的场景吧。 三个shared_ptr对象x、g、n:shared_ptrg(newfoo ); //线程之间共享的shared_ptr

shared_ptr x; //线程a的局部变量

Shared_ptrn(newfoo ); //线程b的局部变量

一开始,我会注意每件事。

线程a执行x=g; 也就是说,read g )按以下方式完成了步骤1,但还没有到达步骤2。 此时,切换到了b线程。

同时编程b执行g=n; (即write G ),两个步骤一起完成了。

第一步:

步骤2 :

这是Foo1对象被丢弃,x.ptr成为了空的悬挂指针!

最后返回线程a,完成步骤2。

多线程在没有保护的情况下读写g,导致了“x是空指针”的结果。 多线程必须锁定才能读写同一个shared_ptr。

当然,race condition不仅如此,还可能与其他线程纠缠在一起,导致其他错误。

请考虑shared_ptr的operator=如果实现先复制ref_count(step2),然后再复制ptr (step1),那么step1)有什么r

ace condition?

杂项

shared_ptr 作为 unordered_map 的 key

直到 Boost 1.47.0 发布之前,unordered_set<:shared_ptr> > 虽然可以编译通过,但是其 hash_value 是 shared_ptr 隐式转换为 bool 的结果。也就是说,如果不自定义hash函数,那么 unordered_{set/map} 会退化为链表。https://svn.boost.org/trac/boost/ticket/5216

Boost 1.51 在 boost/functional/hash/extensions.hpp 中增加了有关重载,现在只要包含这个头文件就能安全高效地使用 unordered_set<:shared_ptr> 了。

这也是 muduo 的 examples/idleconnection 示例要自己定义 hash_value(const boost::shared_ptr& x) 函数的原因(书第 7.10.2 节,p.255)。因为 Debian 6 Squeeze、Ubuntu 10.04 LTS 里的 boost 版本都有这个 bug。

为什么图 1 中的 ref_count 也有指向 Foo 的指针?

shared_ptr sp(new Foo) 在构造 sp 的时候捕获了 Foo 的析构行为。实际上 shared_ptr.ptr 和 ref_count.ptr 可以是不同的类型(只要它们之间存在隐式转换),这是 shared_ptr 的一大功能。分 3 点来说:

1. 无需虚析构;假设 Bar 是 Foo 的基类,但是 Bar 和 Foo 都没有虚析构。

shared_ptr sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr sp2 = sp1; // 可以赋值,自动向上转型(up-cast)

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为其 ref_count 记住了 Foo 的实际类型。

2. shared_ptr 可以指向并安全地管理(析构或防止析构)任何对象;muduo::net::Channel class 的 tie() 函数就使用了这一特性,防止对象过早析构,见书 7.15.3 节。

shared_ptr sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr sp2 = sp1; // 可以赋值,Foo* 向 void* 自动转型

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,不会出现 delete void* 的情况,因为 delete 的是 ref_count.ptr,不是 sp2.ptr。

3. 多继承。假设 Bar 是 Foo 的多个基类之一,那么:

shared_ptr sp1(new Foo);

shared_ptr sp2 = sp1; // 这时 sp1.ptr 和 sp2.ptr 可能指向不同的地址,因为 Bar subobject 在 Foo object 中的 offset 可能不为0。

sp1.reset(); // 此时 Foo 对象的引用计数降为 1

但是 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为 delete 的不是 Bar*,而是原来的 Foo*。换句话说,sp2.ptr 和 ref_count.ptr 可能具有不同的值(当然它们的类型也不同)。

为什么要尽量使用 make_shared()?

为了节省一次内存分配,原来 shared_ptr x(new Foo); 需要为 Foo 和 ref_count 各分配一次内存,现在用 make_shared() 的话,可以一次分配一块足够大的内存,供 Foo 和 ref_count 对象容身。数据结构是:

不过 Foo 的构造函数参数要传给 make_shared(),后者再传给 Foo::Foo(),这只有在 C++11 里通过 perfect forwarding 才能完美解决。

(.完.)

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。