I am familiar with many of the mechanisms and idioms surrounding concurrency in Java. Where I am confused is with a simple concept: concurrent access of different members of the same object.
我熟悉Java中关于并发的许多机制和习惯用法。我困惑的地方是一个简单的概念:同一对象的不同成员的并发访问。
I have a set of variables which can be accessed by two threads, in this case concerning graphical information within a game engine. I need to be able to modify the position of an object in one thread and read it off in another. The standard approach to this problem is to write the following code:
我有一组可以由两个线程访问的变量,在这种情况下涉及游戏引擎中的图形信息。我需要能够在一个线程中修改对象的位置并在另一个线程中读取它。解决此问题的标准方法是编写以下代码:
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;
}
}
However, I'm writing a real-time game engine, not a 20 questions application. I need things to work fast, especially when I access and modify them as often as I do the position of a graphical asset. I want to remove the synchronized overhead. Even better, I'd like to remove the function call overhead altogether.
但是,我正在编写一个实时游戏引擎,而不是一个20个问题的应用程序。我需要快速工作,特别是当我访问和修改它们时,就像我处理图形资产的位置一样。我想删除同步开销。更好的是,我想完全删除函数调用开销。
private int xpos;
private int bufxpos;
...
public void finalize()
{
bufxpos = xpos;
...
}
Using locks, I can make the threads wait on each other, and then call finalize() while the object is neither being accessed nor modified. After this quick buffering step, both threads are free to act on the object, with one modifying/accessing xpos and one accessing bufxpos.
使用锁,我可以使线程彼此等待,然后调用finalize(),同时既不访问也不修改对象。在这个快速缓冲步骤之后,两个线程都可以*地操作对象,一个修改/访问xpos,一个访问bufxpos。
I have already had success using a similar method where the information was copied in to a second object, and each thread acted on a separate object. However, both members are still part of the same object in the above code, and some funny things begin to happen when both my threads access the object concurrently, even when acting on different members. Unpredictable behaviour, phantom graphical objects, random errors in screen position, etc. To verify that this was indeed a concurrency issue, I ran the code for both threads in a single thread, where it executed flawlessly.
我已经成功使用了一种类似的方法,其中信息被复制到第二个对象,每个线程作用于一个单独的对象。但是,两个成员仍然是上述代码中同一个对象的一部分,当我的线程同时访问对象时,即使在对不同的成员进行操作时,也会发生一些有趣的事情。不可预知的行为,虚幻的图形对象,屏幕位置的随机错误等。为了验证这确实是一个并发问题,我在一个线程中运行了两个线程的代码,在那里它执行完美。
I want performance above all else, and I am considering buffering the critical data in to separate objects. Are my errors caused by concurrent access of the same objects? Is there a better solution for concurrency?
我希望性能高于一切,我正在考虑将关键数据缓冲到单独的对象中。我的错误是由并发访问相同对象引起的吗?是否有更好的并发解决方案?
EDIT: If you are doubting my valuation of performance, I should give you more context. My engine is written for Android, and I use it to draw hundreds or thousands of graphic assets. I have a single-threaded solution working, but I have seen a near doubling in performance since implementing the multi-threaded solution, despite the phantom concurrency issues and occasional uncaught exceptions.
编辑:如果你怀疑我的表现估值,我应该给你更多的背景。我的引擎是为Android编写的,我使用它来绘制数百或数千个图形资源。我有一个单线程解决方案正在运行,但是自从实现多线程解决方案以来,我已经看到性能几乎翻了一番,尽管存在幻像并发问题和偶尔的未捕获异常。
EDIT: Thanks for the fantastic discussion about multi-threading performance. In the end, I was able to solve the problem by buffering the data while the worker threads were dormant, and then allowing them each their own set of data within the object to operate on.
编辑:感谢关于多线程性能的精彩讨论。最后,我能够通过在工作线程处于休眠状态时缓冲数据来解决问题,然后允许它们在对象内的每个数据集进行操作。
3 个解决方案
#1
4
If you are dealing with just individual primitives, such as AtomicInteger
, which has operations like compareAndSet
, are great. They are non-blocking and you can get a good deal of atomicity, and fall back to blocking locks when needed.
如果您只处理单个基元,例如AtomicInteger,它具有compareAndSet等操作,那就太棒了。它们是非阻塞的,你可以获得很大的原子性,并在需要时回退到阻塞锁。
For atomically setting accessing variables or objects, you can leverage non-blocking locks, falling back to traditional locks.
对于原子设置访问变量或对象,您可以利用非阻塞锁定,回退到传统锁定。
However, the simplest step forward from where you are in your code is to use synchronized
but not with the implicit this
object, but with several different member objects, one per partition of members that need atomic access: synchronized(partition_2) { /* ... */ }
, synchronized(partition_1) { /* ... */ }
, etc. where you have members private Object partition1;
, private Object partition2;
etc.
但是,您在代码中最简单的步骤是使用synchronized但不使用隐式此对象,但使用几个不同的成员对象,每个需要原子访问的成员分区一个:synchronized(partition_2){/ *。 .. * /},synchronized(partition_1){/ * ... * /}等,其中你有成员私有对象partition1 ;,私有对象partition2;等等
However, if the members cannot be partitioned, then each operation must acquire more than one lock. If so, use the Lock
object linked earlier, but make sure that all operation acquires the locks it needs in some universal order, otherwise your code might deadlock.
但是,如果无法对成员进行分区,则每个操作必须获取多个锁。如果是这样,请使用之前链接的Lock对象,但要确保所有操作都以某种通用顺序获取所需的锁,否则您的代码可能会死锁。
Update: Perhaps it is genuinely not possible to increase the performance if even volatile
presents an unacceptable hit to performance. The fundamental underlying aspect, which you cannot work around, is that mutual exclusion necessarily implies a tradeoff with the substantial benefits of a memory hierarchy, i. e. caches. The fastest per-processor-core memory cache cannot hold variables that you are synchronizing. Processor registers are arguably the fastest "cache" and even if the processor is sophisticated enough to keep the closest caches consistent, it still precludes keeping values in registers. Hopefully this helps you see that it is a fundamental block to performance and there is no magic wand.
更新:如果即使不稳定性对性能造成不可接受的打击,也许真的不可能提高性能。你无法解决的基本方面是,互斥必然意味着与内存层次结构的实质性好处进行权衡,即。即缓存。最快的每处理器核心内存缓存无法保存您正在同步的变量。处理器寄存器可以说是最快的“缓存”,即使处理器足够复杂以保持最接近的缓存一致,它仍然不能将值保存在寄存器中。希望这可以帮助您了解它是性能的基本障碍,并且没有魔杖。
In case of mobile platforms, the platform is deliberately designed against letting arbitrary apps run as fast as possible, because of battery life concerns. It is not a priority to let any one app exhaust battery in a couple of hours.
在移动平台的情况下,该平台是故意设计的,以防止任意应用程序尽可能快地运行,因为电池寿命问题。让任何一个应用程序在几个小时内耗尽电池并不是一个优先事项。
Given the first factor, the best thing to do would be redesign your app so that it doesn't need as much mutual exclusion -- consider tracking x-pos inconsistently except if two objects come close to each other say within a 10x10 box. So you have locking on a coarse grid of 10x10 boxes and as long an object is within it you track position inconsistently. Not sure if that applies or makes sense for your app, but it is just an example to convey the spirit of an algorithm redesign rather than search for a faster synchronization method.
考虑到第一个因素,最好的办法是重新设计你的应用程序,这样它就不需要那么多的互斥 - 考虑跟踪x-pos不一致,除非两个对象在10x10的盒子里相互靠近。所以你已经锁定了10x10盒子的粗网格,只要一个物体在它内部,你就会不一致地跟踪位置。不确定这是否对您的应用程序适用或有意义,但它只是传达算法重新设计的精神而不是搜索更快的同步方法的示例。
#2
2
I don't think that I get exactly what you mean, but generally
我不认为我得到你的意思,但一般而言
Is there a better solution for concurrency?
是否有更好的并发解决方案?
Yes, there is:
就在这里:
- prefer Java Lock API over the intrinsic built-in lock.
- 更喜欢Java Lock API而不是内在的内置锁。
- think of using non-blocking constructs provided in atomic API such as AtomicInteger for better performance.
- 考虑使用原子API中提供的非阻塞构造(如AtomicInteger)以获得更好的性能。
#3
1
I think synchronization or any kind of locking can be avoided here with using an immutable object for inter-thread communication. Let's say the message to be sent looks like this:
我认为使用不可变对象进行线程间通信可以避免同步或任何类型的锁定。假设要发送的消息如下所示:
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; }
}
Then somewhere in the writer thread:
然后在作者线程中的某个地方:
sharedObject.message = new ImmutableMessage(1);
The reader thread:
读者帖子:
ImmutableMessage message = sharedObject.message;
int xPos = message.getXPos();
The shared object (public field for the shake of simplicity):
共享对象(简单摇动的公共字段):
public class SharedObject {
public volatile ImmutableMessage message;
}
I guess things change rapidly in a real-time game engine which might end up creating a lot of ImmutableMessage
object which in the end may degrade the performance, but may be it is balanced by the non-locking nature of this solution.
我猜在实时游戏引擎中事情会发生迅速变化,最终可能会创建大量的ImmutableMessage对象,最终可能会降低性能,但可能会被此解决方案的非锁定性质所平衡。
Finally, if you have one free hour for this topic, I think it's worth to watch this video about the Java Memory Model by Angelika Langer.
最后,如果你有一个空闲时间来讨论这个话题,我认为观看Angelika Langer关于Java内存模型的视频是值得的。
#1
4
If you are dealing with just individual primitives, such as AtomicInteger
, which has operations like compareAndSet
, are great. They are non-blocking and you can get a good deal of atomicity, and fall back to blocking locks when needed.
如果您只处理单个基元,例如AtomicInteger,它具有compareAndSet等操作,那就太棒了。它们是非阻塞的,你可以获得很大的原子性,并在需要时回退到阻塞锁。
For atomically setting accessing variables or objects, you can leverage non-blocking locks, falling back to traditional locks.
对于原子设置访问变量或对象,您可以利用非阻塞锁定,回退到传统锁定。
However, the simplest step forward from where you are in your code is to use synchronized
but not with the implicit this
object, but with several different member objects, one per partition of members that need atomic access: synchronized(partition_2) { /* ... */ }
, synchronized(partition_1) { /* ... */ }
, etc. where you have members private Object partition1;
, private Object partition2;
etc.
但是,您在代码中最简单的步骤是使用synchronized但不使用隐式此对象,但使用几个不同的成员对象,每个需要原子访问的成员分区一个:synchronized(partition_2){/ *。 .. * /},synchronized(partition_1){/ * ... * /}等,其中你有成员私有对象partition1 ;,私有对象partition2;等等
However, if the members cannot be partitioned, then each operation must acquire more than one lock. If so, use the Lock
object linked earlier, but make sure that all operation acquires the locks it needs in some universal order, otherwise your code might deadlock.
但是,如果无法对成员进行分区,则每个操作必须获取多个锁。如果是这样,请使用之前链接的Lock对象,但要确保所有操作都以某种通用顺序获取所需的锁,否则您的代码可能会死锁。
Update: Perhaps it is genuinely not possible to increase the performance if even volatile
presents an unacceptable hit to performance. The fundamental underlying aspect, which you cannot work around, is that mutual exclusion necessarily implies a tradeoff with the substantial benefits of a memory hierarchy, i. e. caches. The fastest per-processor-core memory cache cannot hold variables that you are synchronizing. Processor registers are arguably the fastest "cache" and even if the processor is sophisticated enough to keep the closest caches consistent, it still precludes keeping values in registers. Hopefully this helps you see that it is a fundamental block to performance and there is no magic wand.
更新:如果即使不稳定性对性能造成不可接受的打击,也许真的不可能提高性能。你无法解决的基本方面是,互斥必然意味着与内存层次结构的实质性好处进行权衡,即。即缓存。最快的每处理器核心内存缓存无法保存您正在同步的变量。处理器寄存器可以说是最快的“缓存”,即使处理器足够复杂以保持最接近的缓存一致,它仍然不能将值保存在寄存器中。希望这可以帮助您了解它是性能的基本障碍,并且没有魔杖。
In case of mobile platforms, the platform is deliberately designed against letting arbitrary apps run as fast as possible, because of battery life concerns. It is not a priority to let any one app exhaust battery in a couple of hours.
在移动平台的情况下,该平台是故意设计的,以防止任意应用程序尽可能快地运行,因为电池寿命问题。让任何一个应用程序在几个小时内耗尽电池并不是一个优先事项。
Given the first factor, the best thing to do would be redesign your app so that it doesn't need as much mutual exclusion -- consider tracking x-pos inconsistently except if two objects come close to each other say within a 10x10 box. So you have locking on a coarse grid of 10x10 boxes and as long an object is within it you track position inconsistently. Not sure if that applies or makes sense for your app, but it is just an example to convey the spirit of an algorithm redesign rather than search for a faster synchronization method.
考虑到第一个因素,最好的办法是重新设计你的应用程序,这样它就不需要那么多的互斥 - 考虑跟踪x-pos不一致,除非两个对象在10x10的盒子里相互靠近。所以你已经锁定了10x10盒子的粗网格,只要一个物体在它内部,你就会不一致地跟踪位置。不确定这是否对您的应用程序适用或有意义,但它只是传达算法重新设计的精神而不是搜索更快的同步方法的示例。
#2
2
I don't think that I get exactly what you mean, but generally
我不认为我得到你的意思,但一般而言
Is there a better solution for concurrency?
是否有更好的并发解决方案?
Yes, there is:
就在这里:
- prefer Java Lock API over the intrinsic built-in lock.
- 更喜欢Java Lock API而不是内在的内置锁。
- think of using non-blocking constructs provided in atomic API such as AtomicInteger for better performance.
- 考虑使用原子API中提供的非阻塞构造(如AtomicInteger)以获得更好的性能。
#3
1
I think synchronization or any kind of locking can be avoided here with using an immutable object for inter-thread communication. Let's say the message to be sent looks like this:
我认为使用不可变对象进行线程间通信可以避免同步或任何类型的锁定。假设要发送的消息如下所示:
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; }
}
Then somewhere in the writer thread:
然后在作者线程中的某个地方:
sharedObject.message = new ImmutableMessage(1);
The reader thread:
读者帖子:
ImmutableMessage message = sharedObject.message;
int xPos = message.getXPos();
The shared object (public field for the shake of simplicity):
共享对象(简单摇动的公共字段):
public class SharedObject {
public volatile ImmutableMessage message;
}
I guess things change rapidly in a real-time game engine which might end up creating a lot of ImmutableMessage
object which in the end may degrade the performance, but may be it is balanced by the non-locking nature of this solution.
我猜在实时游戏引擎中事情会发生迅速变化,最终可能会创建大量的ImmutableMessage对象,最终可能会降低性能,但可能会被此解决方案的非锁定性质所平衡。
Finally, if you have one free hour for this topic, I think it's worth to watch this video about the Java Memory Model by Angelika Langer.
最后,如果你有一个空闲时间来讨论这个话题,我认为观看Angelika Langer关于Java内存模型的视频是值得的。