我应该使用 const 来使对象线程安全吗?

发布于 2024-12-04 11:34:55 字数 392 浏览 2 评论 0原文

我编写了一个类,其实例可以由多个线程访问。我使用了一个技巧来记住用户在使用对象之前必须锁定该对象。它涉及仅保留 const 实例。当需要读取或修改敏感数据时,其他类应该调用一个方法(该方法是 const,因此允许)来获取锁定对象的非 const 版本。实际上它返回一个代理对象,其中包含一个指向非常量对象的指针和一个scoped_lock,因此当超出范围时它会解锁该对象。代理对象还重载了operator->因此对对象的访问是透明的。

这样,通过访问未锁定的对象来射击自己的脚就更困难了(总是有 const_cast)。

“聪明的把戏”应该避免,而且无论如何这都很难闻。

这个设计真的很糟糕吗? 我还能或者应该做什么?

编辑: Getter 是强制锁定的非常量。

I wrote a class which instances may be accessed by several threads. I used a trick to remember users they have to lock the object before using it. It involves keeping only const instances. When in the need to read or modify sensitive data, other classes should call a method (which is const, thus allowed) to get a non-const version of the locked object. Actually it returns a proxy object containing a pointer to the non-const object and a scoped_lock, so it unlocks the object when going out of scope. The proxy object also overloads operator-> so the access to the object is transparent.

This way, shooting onself's foot by accessing unlocked objects is harder (there is always const_cast).

"Clever tricks" should be avoided, and this smells bad anyway.

Is this design really bad ?
What else can I or should I do ?

Edit: Getters are non-const to enforce locking.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

記柔刀 2024-12-11 11:34:55

基本问题:非常量引用可能存在于其他地方。如果安全地写入,并不意味着它可以安全地读取——您可能会看到中间状态。

此外,某些 const 方法可能(合法地)以线程不安全的方式修改隐藏的内部细节。

分析您实际对对象执行的操作并找到合适的同步模式

如果您聪明的容器确实对对象有足够的了解,可以通过代理控制它们的所有同步,那么请将这些对象设为私有内部类。

Basic problem: a non-const reference may exist elsewhere. If that gets written safely, it does not follow that it can be read safely -- you may look at an intermediate state.

Also, some const methods might (legitimately) modify hidden internal details in a thread-unsafe way.

Analyse what you're actually doing to the object and find an appropriate synchronisation mode.

If your clever container really does know enough about the objects to control all their synchronisation via proxies, then make those objects private inner classes.

心的憧憬 2024-12-11 11:34:55

这是很聪明的做法,但不幸的是注定会失败。

spraff 强调的问题是,您可以防止读取,但不能防止写入。

考虑以下顺序:

unsigned getAverageSalary(Employee const& e) {
  return e.paid() / e.hired_duration();
}

如果我们在两个函数调用之间递增 paid 会发生什么?我们得到一个不连贯的值。

问题是您的方案没有明确强制读取锁定。

考虑代理模式的替代方案:对象本身是一组数据,都是私有的。只有 Proxy 类(朋友)可以读/写其数据,并且在初始化 Proxy 时,它会自动获取锁(在对象的互斥锁上)。

class Data {
  friend class Proxy;
  Mutex _mutex;
  int _bar;
};

class Proxy {
public:
  Proxy(Data& data): _lock(data._mutex), _data(data) {}

  int bar() const { return _data._bar; }
  void bar(int b) { _data._bar = b; }

private:
  Proxy(Proxy const&) = delete; // disable copy

  Lock _lock;
  Data& _data;
};

This is clever, but unfortunately doomed to fail.

The problem, underlined by spraff, is that you protect against reads but not against writes.

Consider the following sequence:

unsigned getAverageSalary(Employee const& e) {
  return e.paid() / e.hired_duration();
}

What happens if we increment paid between the two function calls ? We get an incoherent value.

The problem is that your scheme does not explicitly enforce locking for reads.

Consider the alternative of a Proxy pattern: The object itself is a bundle of data, all privates. Only a Proxy class (friend) can read/write its data, and when initializing the Proxy it grabs the lock (on the mutex of the object) automatically.

class Data {
  friend class Proxy;
  Mutex _mutex;
  int _bar;
};

class Proxy {
public:
  Proxy(Data& data): _lock(data._mutex), _data(data) {}

  int bar() const { return _data._bar; }
  void bar(int b) { _data._bar = b; }

private:
  Proxy(Proxy const&) = delete; // disable copy

  Lock _lock;
  Data& _data;
};
生死何惧 2024-12-11 11:34:55

如果我想做你正在做的事情,我会做以下其中一项。

方法一:

shared_mutex m;  // somewhere outside the class

class A
{
private:
    int variable;
public:
    void lock() { m.lock(); }
    void unlock() { m.unlock(); }
    bool is_locked() { return m.is_locked(); }
    bool write_to_var(int newvalue)
    {
        if (!is_locked())
            return false;
        variable = newvalue;
        return true;
    }
    bool read_from_var(int *value)
    {
        if (!is_locked() || value == NULL)
            return false;
        *value = variable;
        return true;
    }
};

方法二:

shared_mutex m;  // somewhere outside the class

class A
{
private:
    int variable;
public:
    void write_to_var(int newvalue)
    {
        m.lock();
        variable = newvalue;
        m.unlock();
    }
    int read_from_var()
    {
        m.lock();
        int to_return = variable;
        m.unlock();
        return to_return;
    }
};

第一种方法效率更高(不是一直加锁-解锁),但是程序可能需要不断检查每次读写的输出,看是否成功。第二种方法自动处理锁定,因此程序员甚至不知道锁在那里。

注意:这不是用于复制粘贴的代码。它展示了一个概念并概述了它是如何完成的。请不要评论说您忘记在某处进行一些错误检查。

If I wanted to do what you are doing, I would do one of the following.

Method 1:

shared_mutex m;  // somewhere outside the class

class A
{
private:
    int variable;
public:
    void lock() { m.lock(); }
    void unlock() { m.unlock(); }
    bool is_locked() { return m.is_locked(); }
    bool write_to_var(int newvalue)
    {
        if (!is_locked())
            return false;
        variable = newvalue;
        return true;
    }
    bool read_from_var(int *value)
    {
        if (!is_locked() || value == NULL)
            return false;
        *value = variable;
        return true;
    }
};

Method 2:

shared_mutex m;  // somewhere outside the class

class A
{
private:
    int variable;
public:
    void write_to_var(int newvalue)
    {
        m.lock();
        variable = newvalue;
        m.unlock();
    }
    int read_from_var()
    {
        m.lock();
        int to_return = variable;
        m.unlock();
        return to_return;
    }
};

The first method is more efficient (not locking-unlocking all the time), however, the program may need to keep checking the output of every read and write to see if they were successful. The second method automatically handles the locking and so the programmer wouldn't even know the lock is there.

Note: This is not code for copy-paste. It shows a concept and sketches how it's done. Please don't comment saying you forgot some error checking somewhere.

趁微风不噪 2024-12-11 11:34:55

这听起来很像 Alexandrescu 的 挥发性想法。你不
使用 const 的实际语义,而是利用
类型系统使用它。在这方面,我更喜欢 Alexandrescu 的使用
volatile 的:const 具有非常明确且易于理解的语义,
颠覆它们肯定会给任何阅读的人带来困惑
或维护代码。 volatile 更合适,因为它没有
明确定义的语义,并且在大多数应用程序的上下文中,不是
用于其他任何事情。

您应该返回一个,而不是返回一个经典的代理对象
智能指针。您实际上可以使用 shared_ptr 来实现这一点,抓取
返回值之前的锁,并在删除器中释放它
(而不是删除对象);然而,我更担心的是,这
会引起读者的一些困惑,我可能会去
使用自定义智能指针(可能将 shared_ptr 与自定义
实现中的删除器)。 (根据你的描述,我怀疑
无论如何,这更接近您的想法。)

This sounds a lot like Alexandrescu's idea with volatile. You're not
using the actual semantics of const, but rather exploiting the way the
type system uses it. In this regard, I would prefer Alexandrescu's use
of volatile: const has very definite and well understood semantics,
and subverting them will definitely cause confusion for anyone reading
or maintaining the code. volatile is more appropriate, as it has no
well defined semantics, and in the context of most applications, is not
used for anything else.

And rather than returning a classical proxy object, you should return a
smart pointer. You could actually use shared_ptr for this, grabbing
the lock before returning the value, and releasing it in the deleter
(rather than deleting the object); I rather fear, however, that this
would cause some confusion amongst the readers, and I would probably go
with a custom smart pointer (probably using shared_ptr with the custom
deleter in the implementation). (From your description, I suspect that
this is closer to what you had in mind anyway.)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文