我正在尝试“学习更多”和“从函数式编程中吸取教训”以及不变性对并发性有益的想法等。
作为一个思考练习,我想象一个简单的游戏,Mario-esq类型的角色可以跑来跑去和向他射击的敌人......
然后我试图想象这是使用不可变对象在功能上编写的。
这引发了一些令我困惑的问题(成为势在必行的OO程序员)。
1)如果我的小家伙在x10位置,y100向右移动1个单位我是否只是使用他的旧值重新实例化他的x位置+1(例如x11,y100)?
2)(如果我的第一个假设是正确的) 如果我的输入线程移动小家伙1个单位和我的敌人AI线程射击小家伙和敌人-ai线程在输入线程之前解析然后我的家伙将失去健康,然后在输入线程解析,获得它回来并向右移动.. 。
这是否意味着即使具有不变性,我也无法解雇我的线程? 当我得到两个线程操作的结果时,我是否需要发送我的线程来执行他们的操作然后新的()同步的小家伙?还是有简单的“功能性”解决方案?
这是一个与我日常面对的问题略有不同的线程问题。 通常我必须决定我是否关心线程解决的顺序。在上述情况下,我在技术上并不关心他是先受伤还是先移动。但我确实关心实例化期间的竞争条件会导致一个线程数据完全丢失。
3)(再次,如果我的第一个假设是正确的)不断实例化一个对象的新实例(例如马里奥家伙)有一个可怕的开销,使其成为一个非常严重/重要的设计决策?
修改 对于这个额外的编辑感到抱歉,我不是关于跟进问题的良好做法......
4)如果不变性是我应该努力的事情,甚至跳过实例化已经改变的对象的新版本的箍...并且如果我每次移动时实例化我的人(只有不同的位置)不要我有完全相同的问题,如果他是可变的吗?在某个时刻引用他的东西实际上是在看旧值吗?我越深入了解我的头脑越多,因为生成具有不同值的相同事物的新版本似乎是可变性的,通过黑客攻击。 :¬我想我的问题是:应该这项工作怎么样?如何改变他的立场会有什么好处呢?
for(ever)//simplified game-loop update or "tick" method
{
if(Keyboard.IsDown(Key.Right)
guy = new Guy(guy){location = new Point(guy.Location.x +1, guy.Location.y)};
}
同样令人困惑的是:上面的代码意味着那个人是可变的!(即使他的属性不是)
4.5)对于一个完全不变的人来说,这完全可能吗?
谢谢,
学家
答案 0 :(得分:3)
关于你的观点的几点评论:
1)是的,也许吧。为了减少开销,实际设计可能最终会在这些实例之间共享很多状态。例如,也许你的小家伙有一个“装备”结构也是不可改变的。新副本和旧副本可以安全地引用相同的“设备”结构,因为它是不可变的;所以你只需要复制一个引用,而不是整个引用。这是一个共同的优势,你只能感谢不变性 - 如果“设备”是可变的,你就无法共享参考,因为如果它改变了,你的“旧”版本也会改变。
2)在游戏中,这个问题最实用的解决方案可能是拥有一个全局“时钟”并且在时钟周期内进行一次这样的处理。请注意,如果您没有以功能样式编写它,您的确切方案仍然是一个问题:假设H0是时间T的健康状况。如果您将H0传递给在时间T做出健康决定的函数,则您采取了在时间T + 1受到伤害,然后在时间T + 5返回该功能,它可能根据您当前的健康状况作出了错误的决定。
3)在鼓励函数式编程的语言中,对象实例化通常尽可能便宜。我知道在JVM上,在堆上创建小对象的速度非常快,在任何实际情况下都很少考虑性能问题,在C#中我从来没有遇到过这样的问题。
答案 1 :(得分:2)
1)如果我的小家伙在位置 x10,y100向右移动1个单位 用旧的方式重新解释他 + x位置的+1值 (例如x11,y100)?
嗯,不一定。你可以一次实例化那个人,并在比赛期间改变它的位置。您可以使用代理对此进行建模。这个人是代理人,AI也是如此,渲染线程也是如此,用户也是如此。
当AI射击该人时,当用户按下发送另一条消息的箭头键等时,它会向其发送消息。
let guyAgent (guy, position, health) =
let messages = receiveMessages()
let (newPosition, newHealth) = process(messages)
sendMessage(renderer, (guy, newPosition, newHealth))
guyAgent (guy, newPosition, newHealth)
“Everything”现在是不可变的(实际上,代理人的dipatch队列确实有一些可变状态)。
4)如果不变性是我的话 应该努力甚至跳跃 实例化新版本的箍 已经改变的对象......如果我 每次他移动时都会实例化我的家伙 (只有不同的位置)不要 我和我有完全相同的问题 如果他是可变的吗?
嗯,是的。循环使用可变值并使用不可变值重复进行是等效的。
编辑:
答案 2 :(得分:0)
如果在当前堆栈框架之外的全局系统状态中的所有内容都是不可变的,除非为另一个线程提供对堆栈中某些内容的引用(非常危险),否则将无法使用一个线程做任何事情来互相影响。你可能会发射并忘记,或者根本就不打扰首先发射,效果也是一样的。
假设全局状态的某些部分是可变的,那么一个有用的模式是:
Do Latch a mutable reference to an immutable object Generate a new object based upon the latched reference Loop While CompareExchange fails.
如果仍然指向旧的,则比较交换应更新对新的可变引用。如果没有并发访问,这可以避免锁定的开销,但是如果许多线程尝试更新同一个对象并且从锁定的对象生成新实例很慢,则可能比锁定更糟糕。这种方法的一个优点是没有死锁的危险,但在某些情况下可能会发生liveLock。
答案 3 :(得分:0)
解决这类问题的另一个功能方法是退后一步,将国家的想法与小家伙的想法分开。
你的州将包括你的小家伙的位置,以及你的坏人的位置和它的射击,然后你有一些功能,可以采取一些或所有的状态,并做一些事情,如生成下一个状态并绘制屏幕。
当你想要并行化的东西相互依赖时,你所谈论的时间问题是真正的问题,这些问题不会神奇地消失,尽管这些解决方案在不同的语言中可能或多或少都很方便。 / p>
已经提出了一些建议,并且有各种各样的并发解决方案。中央时钟和代理可以工作,软件事务存储器,互斥锁或CSP(转换风格通道),以及其他可能。最好的方法取决于问题的具体情况,并在一定程度上取决于个人品味。
至于头部旋转,尽量不要过于关注事物是否在变化。不变性的关键不在于事情不会发生变化,而是你可以创建纯粹的功能,这样你的程序就更容易推理了。
例如,OO程序可能具有迭代场景中所有对象的绘图功能,并要求它们全部绘制自己,其中功能程序可能具有采用状态并绘制帧的功能。
最终结果将是同一个场景,但逻辑和状态的组织方式却截然不同。
对于其中一个人来说,当你在这里拥有所有数据,在一个大的输入块中,以及那里的所有绘图逻辑中,封装在一些函数中时,我会发现它更容易处理。有一些非常明确的架构获胜 - 使用这种结构,序列化,测试和交换前端变得更加容易。
答案 4 :(得分:-1)
并非程序中的所有内容都应该是不可变的。玩家的位置是你期望变化的东西。他的名字,也许不是。
不变性很好,但是您应该重新考虑使用更多并发解决方案的方法而不是简单的“不可变”的所有方法。考虑一下这个
线程AI会获得您职位的副本 你向左移动三个单位。 AI会根据你的旧位置射击你,命中......不应该发生!
此外,大多数游戏都是在“游戏滴答”中完成的 - 没有太多的多线程正在进行!