多线程访问共享资源
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