我熟悉Java中并发并发的许多机制和习语。我所混淆的是一个简单的概念:同一对象的不同成员的并发访问。
我有一组变量可以通过两个线程访问,在本例中是关于游戏引擎中的图形信息。我需要能够修改一个对象在一个线程中的位置,并在另一个线程中读取它。解决这个问题的标准方法是编写以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| private int xpos;
private object xposAccess;
public int getXpos() {
int result;
synchronized (xposAccess) {
result = xpos;
}
return result;
}
public void setXpos(int xpos) {
synchronized (xposAccess) {
this.xpos = xpos;
}
} |
但是,我正在编写一个实时的游戏引擎,而不是一个20个问题的应用程序。我需要快速工作,尤其是当我像处理图形资产的位置那样频繁地访问和修改它们时。我想去掉同步开销。更好的是,我希望完全消除函数调用开销。
1 2 3 4 5 6 7 8 9
| private int xpos;
private int bufxpos;
...
public void finalize()
{
bufxpos = xpos;
...
} |
号
使用锁,我可以让线程彼此等待,然后在既不访问也不修改对象的情况下调用Finalize()。在这个快速缓冲步骤之后,两个线程都可以自由地对对象进行操作,一个线程修改/访问xpos,另一个线程访问bufxpos。
我已经成功地使用了类似的方法,将信息复制到第二个对象中,并且每个线程都在一个单独的对象上运行。但是,在上面的代码中,两个成员仍然是同一对象的一部分,当两个线程同时访问对象时,甚至当对不同的成员执行操作时,也会发生一些有趣的事情。不可预测的行为、虚幻的图形对象、屏幕位置的随机错误等。为了验证这确实是一个并发问题,我在一个线程中为两个线程运行了代码,在这个线程中它可以完美地执行。
我最需要的是性能,我正在考虑将关键数据缓冲到单独的对象中。我的错误是由同一对象的并发访问引起的吗?有没有更好的并发解决方案?
编辑:如果你怀疑我对业绩的评价,我应该给你更多的背景。我的引擎是为Android编写的,我使用它来绘制成百上千的图形资产。我有一个单线程解决方案可以工作,但是自从实现多线程解决方案以来,我看到性能几乎翻了一番,尽管存在幻影并发问题和偶尔出现的未捕获异常。
编辑:感谢您对多线程性能的精彩讨论。最后,我能够通过在工作线程处于休眠状态时缓冲数据来解决这个问题,然后允许它们在对象中各自操作自己的数据集。
- 如果你让xpos和bufxpos易变怎么办?
- 当对象既没有被访问也没有被修改时,调用finalize()是什么意思?不使用同步机制是不可能的。
- @nosid我认为他的finalize方法不是用来重写object finalize()。op,您可能需要更好的名称,因为在所有对象实例上都有一个名为finalize()的方法。这种方法很特别,你真的不应该去碰它。
- 如果编写器线程使用必要的数据构造一个不可变的对象,然后将其发布到读线程,该怎么办?据我所知,不可变对象可以在不同步的情况下使用。
- 为什么您仍然需要在读访问中进行同步?如果只有一个线程正在写入,则其他线程可以在不进行任何同步的情况下读取。唯一的问题是他们看到一些毫秒级的旧数据。(这类似于卡托纳的评论)
- @卡托娜,这是我成功使用的确切方法。问题是需要一个复杂的机制来增加性能开销。
- @Christian我的成语可能是关的,但我的观点是,出于性能原因,同步块是我试图避免的一部分。
- 我不太明白你为什么不简单地让xpos易变,去掉xposAccess和synchronized块…
- @Assylias同步块是我的教科书方法示例,其中第二位代码是我的实际实现。不幸的是,对于一个游戏引擎,波动性增加了一个很大的性能开销。
- @Bostonwalker易失性读取与x86上的正常读取一样有效-易失性写入速度可能慢50倍,但我们现在讨论的是,在最近的PC上,最大差异为50纳秒,如果您以每秒10秒的帧为目标,这不应该是个问题。这不太可能成为瓶颈…如果这真的是一个问题,那么另一种选择是在单个线程环境中执行特定的操作(如果正确执行,这会很有效)。
- 我应该补充一点,易变也可以防止一些JIT优化,这可能是一个"隐藏"的成本
- @但是我不是说x86,最近的个人电脑,或者每秒10秒的帧。我说的是在大脑皮层或手臂芯片上每秒画60次的数千个物体。我曾经成功地通过在一个线程中完成这些任务而获得了良好的性能,但并不是我想要的性能。如果您忽略了幻影并发性问题,那么这个方法是非常优越的,因为Android虚拟机提供了我两倍多的核心,而且我已经看到了性能几乎翻倍。
- @波士顿沃克:你说你在安卓上做这个。Android的用户界面是单线程的。你到底想做什么?你认为你需要解决的问题是什么?
- 你应该更具体地说明你想要达到的目标和你的环境。
- @FalmariI已经通过在UI线程上缓冲数据成功地实现了两个工作线程。但是,通过添加第三个线程,dalvik机器更可能允许应用程序使用第二个处理器内核,因此在大多数情况下(35->55 fps)性能会得到极大的提高。
- @实际上,我更愿意保持这个问题的一般性,以避免对多线程的价值等不相关的讨论,并允许这个问题对更多的人更有用。这确实是一个Java问题,它超越了Android。
如果您只处理单个原语,如AtomicInteger,它的操作类似于compareAndSet,那么它是非常好的。它们是非阻塞的,您可以获得大量原子性,并在需要时返回到阻塞锁。
对于原子性地设置访问变量或对象,可以利用非阻塞锁,返回到传统锁。
但是,从代码中的位置向前迈出的最简单的一步是使用synchronized,但不是使用隐式this对象,而是使用几个不同的成员对象,每个需要原子访问的成员分区一个:synchronized(partition_2) { /* ... */ }、synchronized(partition_1) { /* ... */ }等,其中有private Object partition1;、private Object partition2;等成员。
但是,如果无法对成员进行分区,则每个操作必须获取多个锁。如果是这样,请使用前面链接的Lock对象,但请确保所有操作都以某种通用顺序获取所需的锁,否则您的代码可能会死锁。
更新:如果volatile对性能造成了不可接受的影响,那么可能无法提高性能。基本的基础方面,你不能解决,是互斥必然意味着一个折衷与内存层次结构的实质利益,即缓存。每处理器最快的核心内存缓存无法保存正在同步的变量。处理器寄存器可以说是最快的"缓存",即使处理器足够复杂以保持最近的缓存一致,它仍然排除在寄存器中保留值。希望这能帮助你看到它是一个基本的性能块,没有魔杖。
在移动平台的情况下,出于电池寿命的考虑,该平台专门针对让任意应用程序尽可能快地运行而设计。让任何一个应用程序在几个小时内耗尽电池并不是一个优先事项。
考虑到第一个因素,最好的办法就是重新设计你的应用程序,这样它就不需要太多的互斥——考虑不一致地跟踪x-pos,除非两个物体靠近,比如说在一个10x10盒子里。因此,您锁定了一个10x10框的粗网格,只要其中有一个对象,就可以不一致地跟踪位置。不确定这是否适用于您的应用程序,也不一定对您的应用程序有意义,但它只是一个示例,用来传达算法重新设计的精神,而不是寻找更快的同步方法。
- 你的回答是对许多可能的解决方案的一个很好的总结,为此我反对你。不过,如果您能了解一下我最初的两个问题,即并发性故障的原因,什么是安全方法,以及解决方案的性能(必须每秒处理10000-100K读写),我将不胜感激。
- 锁拆分是一个很好的主意,但在实践中会引起大量上下文切换,这使得它不是一个有效的解决方案。
- @波士顿沃克,我不明白你的问题"最后定稿"是怎么回事。finalize通常与垃圾收集有关,我不清楚与您的互斥计划有什么联系。我仍然会更新性能问题的答案。
我不明白你的意思,但一般来说
Is there a better solution for concurrency?
是的,有:
- 相对于内部内置的锁更喜欢Java锁API。
- 考虑使用原子API中提供的非阻塞结构(如atomicinteger)以获得更好的性能。
- 我已经使用Java锁API来控制高级线程时序。这确实是一个很好的建议,但当每帧使用数百或数千次时会变得很棘手。
- 至于原子变量,我对它们的性能成本知之甚少。我想它会比我目前的解决方案高,但是如果我错了,请纠正我的。
- 不,使用AtomigInteger比使用int或Integer以及通过synchronized关键字包围所有代码路径更有效,请注意,您的自原子整数不使用任何类型的锁。
- 同步块的性能可能不太好,但我很清楚这一点,在这种情况下,我不打算实现同步块。我更感兴趣的是atomicinteger如何将性能与缓冲数据进行比较。
- atomicinteger本质上是一个包装器,它使用附加的cas操作来包装volatile int。
- @使用易失性ints的assylias已经带来了巨大的性能成本,我正试图避免这一点。
我认为使用不可变对象进行线程间通信可以避免同步或任何类型的锁定。假设要发送的消息如下所示:
1 2 3 4 5 6 7 8
| public final class ImmutableMessage {
private final int xPos;
// ... other fields with adhering the rules of immutability
public ImmutableObject(int xPos /* arguments */) { ... }
public int getXPos() { return xPos; }
} |
然后在编写器线程中的某个地方:
1
| sharedObject.message = new ImmutableMessage(1); |
读卡器线程:
1 2
| ImmutableMessage message = sharedObject.message;
int xPos = message.getXPos(); |
共享对象(用于简化的公共字段):
1 2 3 4
| public class SharedObject {
public volatile ImmutableMessage message;
} |
我想,在一个实时游戏引擎中,情况会迅速变化,最终可能会产生大量的ImmutableMessage对象,这最终可能会降低性能,但可能会被该解决方案的非锁定性质所平衡。
最后,如果你有一个小时来讨论这个话题,我认为值得看看Angelika Langer的Java内存模型。
- @nosid,即使这只是访问器方法中字段的简单赋值/读取?
- @没有好的地方,我应该提到顺序一致性在这个应用程序中很重要。
- @nosid用volatile修改了它,至少是正确的,不一定是高性能的,因为volatile表示的隐式记忆障碍。
- 我看了上述视频的前25分钟。我认为这是误导。你应该找汉斯·博姆的视频。他谈到了C++内存模型,它是固定Java内存模型的起源。
- NOSID我认为Java内存模型比C++更"古老",请参阅下面的问题