Enforcing locking with C++ nonmovable types

栏目: IT技术 · 发布时间: 4年前

内容简介:Let's say you have a struct with some variable protected by a mutex like this:struct UnsafeData {int x;

Let's say you have a struct with some variable protected by a mutex like this:

struct UnsafeData {

int x;

std::mutex ;

};

You should only be able to change x when the mutex is being held. A typical solution is to make x private and then create a method like this:

void UnsafeData::set_x(int newx) {

// WHOOPS, forgot to lock mutex here.

x = newx;

}

It is a common mistake that when code is changed, someone, somewhere forgots to add a lock guard. The problem is even bigger if the variable is a full object or a handle that you would like to "pass out" to the caller so they can use it outside the body of the struct. This caller also needs to release the lock when it's done.

This brings up an interesting question: can we implement a scheme which only permits safe accesses to the variables in a way that the users can not circumvent [0] and which has zero performance penalty compared to writing optimal lock/unlock function calls by hand and which uses only standard C++?

Initial approaches

The first idea would be to do something like:

int& get_x(std::lock_guard &lock);

This does not work because the lifetimes of the lock and the int reference are not enforced to be the same. It is entirely possible to drop the lock but keep the reference and then use x without the lock by accident.

A second approach would be something like:

struct PointerAndLock {

int *x;

std::lock_guard lock;

};

PointerAndLock get_x();

This is better, but does not work. Lock objects are special and they can't be copied or moved so for this to work the lock object must be stored in the heap, meaning a call to new . You could pass that in as an out-param but those are icky. That would also be problematic in that the caller creates the object uninitialised, meaning that x points to garbage values (or nullptr). Murpy's law states that sooner or later one of those gets used incorrectly. We'd want to make these cases impossible by construction.

The implementation

It turns out that this has not been possible to do until C++ added the concept of guaranteed copy elision. It means that it is possible to return objects from functions via neither copy or a move. It's as if they were automatically created in the scope of the calling function. If you are interested in how that works, googling for "return slot" should get you the information you need.  With this the actual implementation is not particularly complex. First we have the data struct:

struct Data {

friend struct Proxy;

Proxy get_x();

private:

int x;

mutable std::mutex m;

};

This struct only holds the data. It does not manipulate it in any way. Every data member is private, so the struct itself and its Proxy friend can poke them directly. All accesses go via the Proxy struct, whose implementation is this:

struct Proxy {

int &x;

explicit Proxy(Data &input) : x(input.x), l(input.m) {}

Proxy(const Proxy &) = delete;

Proxy(Proxy &&) = delete;

Proxy& operator=(const Proxy&) = delete;

Proxy& operator=(Proxy &&) = delete;

private:

std::lock_guard l;

};

This struct is not copyable or movable. Once created the only things you can do with it are to access x and to destroy the entire object. Thanks to guaranteed copy elision, you can return it from a function, which is exactly what we need.

The creating function is simply:

Proxy Data::get_x() {

return Proxy(*this);

}

Using the result feels nice and natural:

void set_x(Data &d) {

// d.x = 3 does not compile

auto p = d.get_x();

p.x = 3;

}

This has all the requirements we need. Callers can only access data entities when they are holding the mutex [1]. They do not and in deed can not release the mutex accidentally because it is marked private. The lifetime of the variable is tied to the life time of the lock, they both vanish at the exact same time. It is not possible to create half initialised or stale  Proxy objects, they are always valid. Even better, the compiler produces assembly that is identical to the manual version, as can be seen via this handy godbolt link .

[0] Unless they manually reinterpret cast objects to char pointers and poke their internals by hand. There is no way to prevent this.

[1] Unless they manually create a pointer to the underlying int and stash it somewhere. There is no way to prevent this.


以上所述就是小编给大家介绍的《Enforcing locking with C++ nonmovable types》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

信息学奥林匹克教程·提高篇

信息学奥林匹克教程·提高篇

吴耀斌 / 湖南师范大学出版社 / 2003-1 / 24.00元

《信息学奥林匹克教程》(提高篇)既有各个算法设计基本思路的讲解及对求解问题的分析,注重了算法引导分析与不同算法的比较,又给出了具体的编程思路与参考程序,程序采用信息学竞赛流行的Turbo Pascal7.0语言编写,并注重结构化与可读性。一起来看看 《信息学奥林匹克教程·提高篇》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具