多线程访问共享资源

Posted

技术标签:

【中文标题】多线程访问共享资源【英文标题】:Multiple threads access shared resources 【发布时间】:2017-04-29 15:51:15 【问题描述】:

我目前正在研究一个粒子系统,它使用一个线程,首先更新粒子,然后绘制。粒子存储在std::vector 中。我想将更新功能移至单独的线程以提高系统性能。然而,这意味着当更新线程和绘制线程同时访问std::vector 时我会遇到问题。我的更新函数改变所有粒子的位置和颜色,并且几乎总是调整std::vector的大小。

单线程方法:

std::vector<Particle> particles;
void tick() //tick would be called from main update loop

    //slow as must wait for update to draw
    updateParticles();
    drawParticles();

多线程:

std::vector<Particle> particles;
//quicker as no longer need to wait to draw and update
//crashes when both threads access the same data, or update resizes vector
void updateThread()

    updateParticles();

void drawThread()

    drawParticles();

为了解决这个问题,我使用std::mutex 进行了调查,但在实践中,由于大量粒子,线程的持续锁定意味着性能没有提高。我也调查了std::atomic,但是,粒子和std::vector 都不能简单地复制,因此也不能使用它。

使用互斥锁的多线程:

注意:我使用的是 SDL 互斥锁,据我所知,原理是一样的。

SDL_mutex mutex = SDL_CreateMutex();
SDL_cond canDraw = SDL_CreateCond();
SDL_cond canUpdate = SDL_CreateCond();
std::vector<Particle> particles;
//locking the threads leads to the same problems as before, 
//now each thread must wait for the other one
void updateThread()

    SDL_LockMutex(lock);
    while(!canUpdate)
    
        SDL_CondWait(canUpdate, lock);
    
    updateParticles();
    SDL_UnlockMutex(lock);
    SDL_CondSignal(canDraw);

void drawThread()

    SDL_LockMutex(lock);
    while(!canDraw)
    
        SDL_CondWait(canDraw, lock);
    
    drawParticles();
    SDL_UnlockMutex(lock);
    SDL_CondSignal(canUpdate);

我想知道是否还有其他方法可以实现多线程方法?从本质上防止两个线程同时访问相同的数据,而不必让每个线程等待另一个。我曾考虑过制作要从中绘制的向量的本地副本,但这似乎效率低下,并且如果更新线程在复制向量时更改了向量,可能会遇到同样的问题?

【问题讨论】:

您还应该考虑利用数据并行性是否是更合适的方法。对于所有这些粒子,更新和绘制操作不会有那么大的不同,不是吗? 它们几乎是相同的功能,你能解释一下你的意思吗?我不熟悉数据并行的概念。 更新粒子可能发生在该向量上的某个循环中,让一个线程在前半部分工作,另一个线程在后半部分工作。泛化更多线程。 有趣,我喜欢这个想法,但如果一个线程调整矢量大小,它会起作用吗?这将如何影响其他线程? 不,那是行不通的。尽管您可以这样处理: 1. 计算所有粒子的更新函数,从而为每个粒子生成一个新/更新粒子的集合。 2. 将这些集合展平为更新的单个集合/向量。第 1 步是微不足道的并行化,第 2 步有点棘手。但是假设计算更新函数远比复制/移动粒子对象复杂得多,仍然应该得到可接受的加速。 【参考方案1】:

我会使用更精细的锁定策略。我不会在vector 中存储particle 对象,而是存储指向不同对象的指针。

struct lockedParticle particle* containedParticle; SDL_mutex lockingObject; ;

updateParticles() 中,我将尝试使用SDL_TryLockMutex() 获取单个锁定对象 - 如果我无法获得对互斥体的控制,我会将指向此特定lockedParticle 实例的指针添加到另一个向量,然后重试更新它们。

我会在drawParticles() 中遵循类似的策略。这依赖于这样一个事实,即绘制顺序对于粒子来说并不重要,这通常是这种情况。

【讨论】:

首先,感谢您的回复。您假设我的绘制顺序无关紧要是正确的,但是如果我使用 alpha 混合进行绘制,那么这可能会成为一个问题。需要明确的是,您是否建议我在 updateParticles(); 中声明一个局部向量并将指针推送到其上锁定的粒子,然后更新此局部向量?如果在我尝试绘制局部向量时开始了新的更新周期,这不会再次遇到问题吗? 没有。我建议您的全局可访问向量包含指向新结构的指针。新结构包含一个单独的互斥锁,用于锁定单个粒子。因此,无论是绘制还是更新,每个粒子都使用相同的单独锁。当迭代这个向量时(在任一方法中),您维护一个单独的对象向量,您必须在方法循环结束时重新访问,以获取无法锁定的对象。 另外,您可以考虑使用无锁策略,其中 update 方法以原子方式更新整个粒子对象。为此,您不能使用 vector。 好的,感谢您解决这个问题,但我相信我的观点仍然成立。假设一个粒子无法锁定,所以它被添加到新向量中,然后在函数结束时,当我尝试重新访问它们时,如果新的更新周期已经开始,那么这些粒子可能仍然无法锁定?跨度> 是的,在这种情况下,我不会更新它们并将这些粒子留到下一个循环 - 屏幕上有 10,000 个粒子,这可能不是问题。有 3 个或 4 个就可以了。我个人会接受我的替代建议,即无锁列表。这将允许始终对所有粒子进行一致的只读访问,并且多个线程可以一次一致地更新。如果您只需要从单线程进行写访问,则实现会变得更加简单(因为您不需要执行 Compare And Exchange 调用),您只需将新粒子交换到您的链表中。【参考方案2】:

如果数据一致性不是问题,您可以通过将向量封装在自定义类中并仅在单个读/写操作上设置互斥锁来避免阻塞整个向量,例如:

struct SharedVector

    // ...
    std::vector<Particle> vec;

    void push( const& Particle particle )
    
       SDL_LockMutex(lock);
       vec.push_back(particle);
       SDL_UnlockMutex(lock);
    

//...
SharedVector particles;

那么当然需要修改updateParticles()drawParticles() 以使用新类型而不是std::vector

编辑: 您可以通过在updateParticles()drawParticles() 方法中使用互斥体来避免创建新结构,例如

void updateParticles()

    //... get Particle particle object
    SDL_LockMutex(lock);
    particles.push_back(particle);
    SDL_UnlockMutex(lock);

drawParticles() 也应该这样做。

【讨论】:

您的 push 函数将是向矢量添加新粒子的解决方案,但我的问题也是粒子的位置可以在访问绘图时更改,我需要添加新函数以使用互斥锁以相同方式访问位置? 是的,绝对是,在多线程环境中,您需要保护对向量的所有访问操作。【参考方案3】:

如果向量一直在变化,你可以使用两个向量。 drawParticles 将拥有自己的副本,updateParticles 将写入另一个副本。完成这两个功能后,将updateParticles 使用的向量交换、复制或移动到drawParticles 使用的向量。 (updateParticles 可以从 drawParticles 使用的相同向量读取当前粒子位置,因此您不需要创建一个完整的新副本。)无需锁定。

【讨论】:

这会使我的内存使用量翻倍?因此,如果我理解您的答案,那么这将成为速度与内存的问题。另外,我知道如果不进行实际测试很难判断,但是您认为这会提高我的表现吗?我不确定复制向量对性能有多大影响。 @nitronoid 它可能会增加您的内存使用量,但根据您已经对updateParticles 中的向量所做的更改,它可能不会那么糟糕。速度改进是您必须测试的(配置文件),因为这在很大程度上取决于许多因素。锁,尤其是在频繁使用时,可能会成为瓶颈。这具有相当容易实现和测试的优点,而不会忘记某个地方的锁。

以上是关于多线程访问共享资源的主要内容,如果未能解决你的问题,请参考以下文章

多线程同步与并发访问共享资源工具—LockMonitorMutexSemaphore

多线程访问共享对象和数据的方式

iOS 多线程之线程安全

c++多线程问题

多线程一共就俩问题:1.线程安全(访问共享数据) 2.线程通信(wait(),notify())

多线程篇五:多个线程访问共享对象和数据的方式