2013-11-01 73 views
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::endpoint s的列表。我认为所有这些设置值都是不变的。因此,一旦我使用setValue()设置了该值,则该值本身(std::string或端点本身列表)不再更改。所以我只是想确保我得到旧值或新值,但不是一些损坏的状态。

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

问题4:更复杂的设置类型如端点列表如何?你会推荐类似于boost::atomic<boost::shared_ptr<std::vector<boost::asio::ip::tcp::endpoint>>>吗?如果不是,那会更好吗?

+1

重新检查允许多个线程在需要时读取和写入这些设置的要求。一个线程调用getBlahSetting()并获得一个结果是否有意义,并且在同一个函数中,稍后再调用它并得到一个_different result_?我建议你在硬同步点设计。 – David

+3

设置只能代表用户更改,用户很少能同时执行多个操作。就目前而言,不应该有一个以上的作家。因此,我会访问(=读取)配置,而没有任何同步(因为它经常发生)。在配置发生改变的罕见情况下,从修改线程进行深层复制(再次,这不需要同步)。然后对指向每个人都使用的配置对象的指针执行一个'seq_cst'存储(这很昂贵,但是您可以承担,因为它很少发生),并且完成。它可能需要... – Damon

+0

...在其他线程接受更改之前的一段时间,但这通常不是问题。他们最终会。如果这是一个问题*,你别无选择,只能使用锁(可能是读写器锁),然后性能会很差。或者,线程可以保留配置状态的本地副本(或共享一个ref-counting副本),并在明确的时间通知_explicitly_通过任务队列更新配置。这消除了可能的比赛。 – Damon

回答

2

Q1,如果在读取原子之后没有尝试读取任何共享的非原子变量,请更正。记忆障碍,只有同步访问,可能原子操作之间发生

Q2我不知道(见下文)非原子变量

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

atomic<string> 

可能不锁免费

Q4应该工作,但,再次实现是不可能lockfree(实施lockfree shared_ptr的是具有挑战性和专利雷区场)。

所以也许读者,作家锁(达蒙建议在评论)可以更简单,更有效的,如果你的配置包括有规模超过1个字机(为CPU原生原子能通常工作)

数据[编辑]然而,

atomic<shared_ptr<TheWholeStructContainigAll> > 

可能有一定的道理,即使是非无锁:这种方法最大限度地减少碰撞的概率为需要多于一个连贯的价值的读者,虽然作者应该使整个的新副本“参数表“每次改变一些东西。

+0

'boost :: atomic '编译但不起作用。当我尝试加载它时,我得到一个调试断言。它在“crt \ src \ dbgdel.cpp”(VC++ 2012)中说:“_BLOCK_TYPE_IS_VALID(pHead-> nBlockUse)”。有时我只是遇到访问冲突。如果我用'std :: atomic'包装我的'std :: string',我可以加载这个值,但是当我将它存储时,会出现访问冲突... –

2

对于问题,答案是 “依靠,但可能不会”。如果你真的只关心单个值是不是乱码,那么是的,这很好,你也不关心记忆顺序。
虽然这通常是一个错误的前提。

如有问题,,并是的,这会工作,但它很可能会使用锁定复杂的物体,如string(内部,每次访问,而不让你知道)。通常只能以无锁方式原子地访问/更改大致为一个或两个指针大小的相当小的对象。这也取决于你的平台。

是否成功更新一个或两个原子值是一个很大的区别。假设您的值为leftright,它将界定任务在阵列中执行某些处理的左侧和右侧边界。假设它们分别是50和100,并且你将它们分别改变为101和150,每个原子。所以另一个线程从50到101变化,并开始计算,看到101> 100,完成并将结果写入文件。之后,您再次以原子方式更改输出文件的名称。
一切都是原子的(因此比正常情况下更昂贵),但没有一个是有用的。结果仍然是错误的,并被写入错误的文件中。
这在您的特定情况下可能不是问题,但通常情况下(并且您的要求可能会在未来发生变化)。通常你真的想要完成集的变化是原子。也就是说,如果你有很多或者复杂的更新(或者更复杂的更新),那么你可能想要在整个配置中使用一个大的(读写器)锁定因为这比获取和释放20或30个锁或者执行50或100个原子操作更有效。但请注意,无论如何,锁定会严重影响性能。

正如上面的评论中指出的那样,我最好从修改配置的一个线程中创建配置的深层副本,并将消费者使用的引用(共享指针)的更新安排为常规任务。这种copy-modify-publish方法与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); 
    } 
    ... 
} 
+0

不幸的是,它看起来像'boost :: atomic 'doesn'工作。当我尝试加载它时,我得到一个调试断言。它在“crt \ src \ dbgdel.cpp”(VC++ 2012)中说:“_BLOCK_TYPE_IS_VALID(pHead-> nBlockUse)”。有时我只是遇到访问冲突。如果我用'std :: atomic'封装我的'std :: string',我可以加载这个值,但是当我存储它的时候,我得到一个访问冲突... –

2

对于什么比一个指针较大幅度的,使用互斥。 tbb(opensource)库支持读写器模块的概念,它允许多个同时读取器,请参阅documentation