6

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

对于原始数据类型,它看起来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::string例如boost::asio::ip::tcp::endpoints 列表。我认为所有这些设置值都是不可变的。因此,一旦我使用 设置值setValue(),值本身(std::string或端点列表本身)就不再改变。因此,我只想确保获得旧值或新值,而不是某些损坏的状态。

问题 3:这种方法适用于boost::atomic<std::string>吗?如果不是,有什么替代方案?

问题 4:更复杂的设置类型,如端点列表?你会推荐类似的东西boost::atomic<boost::shared_ptr<std::vector<boost::asio::ip::tcp::endpoint>>>吗?如果没有,什么会更好?

4

3 回答 3

2

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

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

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

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

正如上面的评论所指出的,我最好从修改配置的一个线程中对配置进行深层复制,并将消费者使用的引用(共享指针)的更新安排为正常任务。这种复制-修改-发布方法也有点类似于 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);
    }
    ...
}
于 2013-11-01T16:09:20.663 回答
2

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

于 2013-11-01T22:44:22.293 回答
2

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

Q2 我不知道(但见下文)

Q3 应该工作(如果编译)。然而,

 atomic<string> 

可能不是无锁的

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

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

[编辑]然而,

atomic<shared_ptr<TheWholeStructContainigAll> > 

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

于 2013-11-01T14:12:38.870 回答