【问题标题】:Thread-safe Settings线程安全设置
【发布时间】:2013-11-12 16:31:23
【问题描述】:

我正在编写一些设置类,可以从我的多线程应用程序中的任何地方访问。我会经常阅读这些设置(因此读取访问应该很快),但它们不会经常写入。

对于原始数据类型,它看起来像 boost::atomic 提供了我需要的东西,所以我想出了这样的东西:

class UInt16Setting
{
    private:
        boost::atomic<uint16_t> _Value;
    public:
        uint16_t getValue() const { return _Value.load(boost::memory_order_relaxed); }
        void setValue(uint16_t value) { _Value.store(value, boost::memory_order_relaxed); }
};

问题 1: 我不确定内存顺序。我认为在我的应用程序中我并不真正关心内存排序(是吗?)。我只是想确保getValue() 始终返回一个未损坏的值(旧值或新值)。那么我的内存排序设置是否正确?

问题 2: 这种使用boost::atomic 的方法是否推荐用于这种同步?还是有其他结构可以提供更好的读取性能?

我的应用程序中还需要一些更复杂的设置类型,例如std::stringboost::asio::ip::tcp::endpoints 的列表。我认为所有这些设置值都是不可变的。因此,一旦我使用setValue() 设置值,值本身(std::string 或端点列表本身)就不再改变。因此,我再次只想确保获得旧值或新值,而不是某些损坏的状态。

问题 3: 这种方法是否适用于 boost::atomic&lt;std::string&gt;?如果没有,有什么替代方案?

问题 4: 更复杂的设置类型(如端点列表)如何?你会推荐像boost::atomic&lt;boost::shared_ptr&lt;std::vector&lt;boost::asio::ip::tcp::endpoint&gt;&gt;&gt; 这样的东西吗?如果没有,什么会更好?

【问题讨论】:

  • 重新检查允许多个线程在需要时读取和写入这些设置的要求。线程调用 getBlahSetting() 并获得一个结果,然后在同一函数中再次调用它并获得 不同的结果,这是否有意义?我建议你设计一个硬同步点。
  • 设置只能代表用户进行更改,并且用户很少可以同时做超过一件事。就目前而言,作家不应该超过一位。因此,我将在没有任何同步的情况下访问(=读取)配置(因为它经常发生)。从修改线程中制作一个深拷贝,以应对配置被更改的罕见情况(同样,这不需要同步)。然后将单个seq_cst 存储到指向每个人都使用的配置对象的指针(这很昂贵,但您可以负担得起,因为它很少发生),然后就完成了。这可能需要...
  • ... 在其他线程获取更改之前的片刻,但这通常不是问题。他们最终会的。如果这有问题,你别无选择,只能使用锁(大概是读写器锁),然后性能会很糟糕。或者,线程可以保留配置状态的本地副本(或共享引用计数的副本),并在明确定义的时间通过任务队列明确通知更新配置。这消除了可能的种族。
  • 问题是,所有这些原子操作不太可能对您有很大帮助,例如,如果您以原子方式修改一些字符串和 TCP 端点,然后以原子方式修改另一个线程(或其中两个)使用错误的 TCP 端点等获取新的字符串值。如果配置的整体是荒谬的,那么是否每个都自动更新无关紧要。当然,除非这对你来说无关紧要。唯一合理安全的方法是使用读写器锁,或在定义的时间显式获取新配置(如通过指向完整的新配置集的新指针的“快照”)。
  • @Damon 我相信这(实际上是这些)应该是一个答案。

标签: c++ multithreading boost synchronization atomic


【解决方案1】:

Q1,如果您在读取原子后不尝试读取任何共享的非原子变量,则更正。内存屏障只同步对原子操作之间可能发生的非原子变量的访问

Q2 我不知道(但见下文)

Q3 应该可以工作(如果编译)。不过,

 atomic<string> 

可能不是无锁

Q4 应该可以工作,但同样,该实现不可能是无锁的(实现无锁 shared_ptr 是具有挑战性和专利挖掘的领域)。

因此,如果您的配置包含大小超过 1 个机器字的数据(CPU 原生原子通常适用于此),那么读写锁(正如 Damon 在 cmets 中所建议的)可能会更简单甚至更有效

[编辑]然而,

atomic<shared_ptr<TheWholeStructContainigAll> > 

即使是非无锁也可能有一些意义:这种方法最大限度地减少了需要多个连贯值的读取器的冲突概率,尽管编写器每次更改某些内容时都应该制作整个“参数表”的新副本。

【讨论】:

  • boost::atomic&lt;std::string&gt; 确实可以编译,但它不起作用。当我尝试加载它时,我得到一个调试断言。它说:“crt\src\dbgdel.cpp”(VC++ 2012)中的“_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)”。有时我只是遇到访问冲突。如果我用std::atomic 包装我的std::string,我可以加载该值,但是当我存储它时会遇到访问冲突...
【解决方案2】:

对于问题 1,答案是“取决于,但可能不是”。如果您真的只关心单个值不会出现乱码,那么是的,这很好,而且您也不关心内存顺序。
但是,通常这是一个错误的前提。

对于 234 的问题,是的,这会起作用,但它可能会对复杂的对象使用锁定,例如 @ 987654321@(内部,每次访问,您都不知道)。只有相当小的对象,大约只有一个或两个指针的大小,通常可以以无锁方式原子地访问/更改。这也取决于您的平台。

一个人是否成功地以原子方式更新一个或两个值是一个很大的区别。假设您有值leftright,它们界定了任务将在数组中进行某些处理的左右边界。假设它们分别是 50 和 100,然后您将它们分别更改为 101 和 150,每个原子都是原子的。因此,另一个线程获取从 50 到 101 的变化并开始进行计算,看到 101 > 100,完成并将结果写入文件。之后,您再次以原子方式更改输出文件的名称。
一切都是原子的(因此,比平常更昂贵),但没有一个是有用的。结果还是错了,也写错了文件。
在您的特定情况下,这可能不是问题,但通常是(并且您的要求将来可能会发生变化)。通常您真的希望完整的更改集是原子的。

也就是说,如果您有很多或复杂的(或既很多又复杂的)更新要做,您可能希望首先对整个配置使用一个大(读写器)锁,因为这比获取和释放 20 或 30 个锁或执行 50 或 100 个原子操作更有效。但是请注意,在任何情况下,锁定都会严重影响性能。

正如上面的 cmets 所指出的,我最好从修改配置的一个线程中对配置进行深层复制,并将消费者使用的引用(共享指针)的更新安排为正常任务。这种复制-修改-发布方法也有点类似于 MVCC 数据库的工作方式(这些也存在锁定会影响其性能的问题)。

修改副本断言只有读取器访问任何共享状态,因此读取器或单个写入器都不需要同步。读写速度很快。 交换配置集仅在明确定义的时间点发生,即保证集合处于完整、一致的状态,并且保证线程不会做其他事情,因此不会发生任何丑陋的意外。

一个典型的任务驱动应用程序看起来有点像这样(在类似 C++ 的伪代码中):

// consumer/worker thread(s)
for(;;)
{
    task = queue.pop();

    switch(task.code)
    {
        case EXIT:
            return;

        case SET_CONFIG:
            my_conf = task.data;
            break;

        default:
            task.func(task.data, &my_conf); // can read without sync
    }
}


// thread that interacts with user (also producer)
for(;;)
{
    input = get_input();

    if(input.action == QUIT)
    {
        queue.push(task(EXIT, 0, 0));
        for(auto threads : thread)
            thread.join();
        return 0;
    }
    else if(input.action == CHANGE_SETTINGS)
    {
        new_config = new config(config); // copy, readonly operation, no sync
        // assume we have operator[] overloaded
        new_config[...] = ...;           // I own this exclusively, no sync

        task t(SET_CONFIG, 0, shared_ptr<...>(input.data));
        queue.push(t);
    }
    else if(input.action() == ADD_TASK)
    {
        task t(RUN, input.func, input.data);
        queue.push(t);
    }
    ...
}

【讨论】:

  • 不幸的是,boost::atomic&lt;std::string&gt; 似乎不起作用。当我尝试加载它时,我得到一个调试断言。它说:“crt\src\dbgdel.cpp”(VC++ 2012)中的“_BLOCK_TYPE_IS_VALID(pHead->nBlockUse)”。有时我只是遇到访问冲突。如果我用std::atomic 包装我的std::string,我可以加载该值,但是在存储它时会遇到访问冲突...
【解决方案3】:

对于比指针更重要的东西,请使用互斥锁。 tbb(开源)库支持 reader-writer mutice 的概念,它允许多个同时读取器,请参阅documentation

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2010-10-29
    • 1970-01-01
    • 2010-10-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-09-28
    相关资源
    最近更新 更多