【问题标题】:Is it safe to modify data of pointer in vector from another thread?从另一个线程修改向量中指针的数据是否安全?
【发布时间】:2011-06-10 20:20:32
【问题描述】:

似乎一切正常,但我不确定这是否是最好的方法。

基本上我有一个异步检索数据的对象。这个对象有一个指针向量,这些指针在主线程上分配和取消分配。使用 boost 函数,进程结果回调与此向量中的指针之一绑定。当它触发时,它将在某个任意线程上运行并修改指针的数据。

现在,如果异步检索对象收到更多请求,我在推入向量和擦除的部分周围有关键部分,但我想知道在修改指针数据的回调中是否需要某种保护也是。

希望这个精简的伪代码能让事情更清楚:

class CAsyncRetriever
{
    // typedefs of boost functions

    class DataObject
    {
         // methods and members
    };

public:
    // Start single asynch retrieve with completion callback
    void Start(SomeArgs)
    {
        SetupRetrieve(SomeArgs);
        LaunchRetrieves();
    }

protected:
    void SetupRetrieve(SomeArgs)
    {
            // ...

        { // scope for data lock
            boost::lock_guard<boost::mutex> lock(m_dataMutex);
            m_inProgress.push_back(SmartPtr<DataObject>(new DataObject)));
            m_callback = boost::bind(&CAsyncRetriever::ProcessResults, this, _1, m_inProgress.back());
        }

            // ...
    }

    void ProcessResults(DataObject* data)
    {
                // CALLED ON ANOTHER THREAD ... IS THIS SAFE?
        data->m_SomeMember.SomeMethod();
                data->m_SomeOtherMember = SomeStuff;
    }

    void Cleanup()
    {
                // ...

        { // scope for data lock
            boost::lock_guard<boost::mutex> lock(m_dataMutex);
            while(!m_inProgress.empty() && m_inProgress.front()->IsComplete())
                m_inProgress.erase(m_inProgress.begin());
        }

                // ...
         }

private:
    std::vector<SmartPtr<DataObject>> m_inProgress;
    boost::mutex m_dataMutex;
        // other members
};

编辑:这是 ProccessResults 回调的实际代码(加上 cmets 为您带来好处)

    void ProcessResults(CRetrieveResults* pRetrieveResults, CRetData* data)
        {
// pRetrieveResults is delayed binding that server passes in when invoking callback in thread pool
// data is raw pointer to ref counted object in vector of main thread (the DataObject* in question)

                // if there was an error set the code on the atomic int in object
            data->m_nErrorCode.Store_Release(pRetrieveResults->GetErrorCode());

                // generic iterator of results bindings for generic sotrage class item
            TPackedDataIterator<GenItem::CBind> dataItr(&pRetrieveResults->m_DataIter);
                // namespace function which will iterate results and initialize generic storage
            GenericStorage::InitializeItems<GenItem>(&data->m_items, dataItr, pRetrieveResults->m_nTotalResultsFound); // this is potentially time consuming depending on the amount of results and amount of columns that were bound in storage class definition (i.e.about 8 seconds for a million equipment items in release)
                // atomic uint32_t that is incremented when kicking off async retrieve
            m_nStarted.Decrement(); // this one is done processing

                // boost function completion callback bound to interface that requested results
            data->m_complete(data->m_items);
        }

【问题讨论】:

  • 如果您期望得到合理的答案,您需要添加更多信息。目前缺少重要的部分,特别是 SomeMethodIsComplete 是什么以及如何实现以及如何引发 complete 标志。如果处理函数的最后一行是赋值,除非是用户定义类型,并且它被锁定,并且将IsComplete设置为true,答案是否定的,它不安全。但我的猜测是,那个处理函数只是一个骨架。
  • 是的,这是一个骨架,我只是想展示它调用方法并在其中分配成员。在这一点上,我想我会没事的,只要每个指针每个回调只有一个线程的语义仍然存在。增量异步检索引入了太多延迟,所以我还不担心这种实现。
  • 这种方法本质上没有错,但魔鬼在细节中。根据实际操作是什么以及如何对 shared 数据执行同步(我的猜测是共享数据只是IsComplete 检查的标志),那么它可能是正确的。它是最好的方法还是可以简化是一个不同的问题,但如果没有上下文,就无法判断。可能很重要的事情:SmartPtr 线程的实现是否安全?那里很有可能出现竞争状况。
  • 顺便说一句,ProcessResults 应该是static,如果不是static,你是共享​​> this 指针,然后你还必须添加为每个成员对象锁定ProcessResults
  • 你可能是对的......但是我想我会停止在完成回调中传递异步检索器,因为它并不是真正需要的,然后唯一使用的其他成员是原子的,所以这将是很好。

标签: c++ boost stl thread-safety


【解决方案1】:

就目前而言,Cleanup 代码似乎可以破坏一个对象,该对象正在对其进行ProcessResults 的回调。当您在回调中取消引用指针时,这将导致问题。

我的建议是您扩展 m_dataMutex 的语义以包含回调,但如果回调是长时间运行的,或者可以在 SetupRetrieve 内内联发生(有时确实会发生这种情况 - 尽管在这里您声明了回调在不同的线程上,在这种情况下你没问题)那么事情就更复杂了。目前m_dataMutex 对它是否控制对向量的访问,或它的内容,或两者兼而有之感到有些困惑。明确其范围后,可以增强 ProcessResults 以验证锁内有效负载的有效性。

【讨论】:

  • 这就是 IsComplete 方法检查的内容,不用担心......我想锁定回调,但是当服务器完成异步任务时,任意线程在任意时间调用这些回调。锁定单个互斥体会同步并因此造成瓶颈,而目前的回调可以并行完成。
  • @AJG85 - 因为你的回调得到一个原始指针,你必须在使用它之前验证它仍然有效。您能否将智能指针转换为引用计数类型,并仅锁定向量以检查它是否正常并获得对对象的安全引用?然后你的大部分回调逻辑仍然可以并行进行。另一个问题是回调中对象的操作是否会干扰任何其他线程的逻辑,如果是这样,这也是一个问题。
  • 我忽略了空检查和大部分看起来不相关的代码。 “SmartPtr”也有不同的名称,但它是我们制作的与 STL 容器兼容的引用计数对象(可能有一天会逐步淘汰它以支持 C++0x)......有一个弱指针友元类 I可能可以用来获取引用,但更改的生命周期需要持续超出回调和线程的范围。在触发完成回调时,在进程结果回调结束之前,其他任何东西都无法看到或使用私有对象。
【解决方案2】:

不,这不安全。

ProcessResults 对通过DataObject 传递给它的数据结构进行操作。这表明您在不同线程之间共享了状态,如果两个线程同时对数据结构进行操作,您可能会遇到一些麻烦。

【讨论】:

  • 每次检索只触发一个回调。所以任何时候只有一个线程访问指针。如果您发出 5 个请求,它将分配 5 个对象,进行 5 个绑定,将它们启动,将调用 5 个其他线程上的 5 个回调来完成肮脏的工作,然后在下次您发出更多请求时清理它们完成了。
  • 此外,“完成”由回调触发、完成处理以及随后在执行结束时触发完整的回调来确定。
  • 啊,在回调中完成的分配有点“暗示”你正在使用DataObject 传递 another 结构,以便回调方法可以处理它。您是否会用有关回调方法语义的更多细节来更新问题,即它的工作原理是什么以及它是否在线程之间共享?
  • 它在线程之间调用,但我的理解是每个线程都有自己的堆栈,所以只要线程之间没有共享任何内容就可以了......我将发布真正的回调作为编辑如果你喜欢。
【解决方案3】:

更新指针应该是一个原子操作,但您可以使用InterlockedExchangePointer(在 Windows 中)来确定。不确定 Linux 的等价物是什么。

唯一的考虑是如果一个线程正在使用一个过时的指针。另一个线程是否删除了原始指针指向的对象?如果是这样,你肯定有问题。

【讨论】:

  • 它没有更新指针,ProcessResults 操作传递给它的数据结构。
  • @Khaled:它是一个指针,但它确实调用方法并更改指针指向的对象的成员。
  • 所以我需要使用原子指针吗?我不需要交换或更改指针指向的内容,只需使用它即可。此外,另一个线程不会删除任何内容,因为释放在另一个线程上创建的内容听起来像是一周中的任何一天的坏消息。
  • @AJG85:我的意思是:“问题不在于更新指针”,看看我之前评论的措辞,我承认这很令人困惑。无论如何,真正的问题似乎在回调函数中。
猜你喜欢
  • 1970-01-01
  • 2011-04-24
  • 1970-01-01
  • 1970-01-01
  • 2019-02-12
  • 1970-01-01
  • 1970-01-01
  • 2021-08-04
  • 1970-01-01
相关资源
最近更新 更多