在多线程服务器应用程序中,我使用类型Client
来表示客户端。 Client
的性质非常可变:客户端发送UDP心跳消息以便在服务器上注册,该消息还可能包含一些实时数据(想想传感器)。我需要跟踪许多事情,例如最后一次心跳的时间戳和源地址,实时数据等。结果是一个包含许多状态的相当大的结构。每个客户端都有一个客户端ID,我使用包含在HashMap
中的MVar
来存储客户端,因此查找非常简单快捷。
type ID = ByteString
type ClientMap = MVar (HashMap ID Client)
ClientMap
的“全局”值可供每个线程使用。它与许多其他全局值一起存储在ReaderT
转换器中。
Client
本身是一个很大的不可变结构,使用严格的字段来防止空间泄漏:
data Client = Client
{
_c_id :: !ID
, _c_timestamp :: !POSIXTime
, _c_addr :: !SockAddr
, _c_load :: !Int
...
}
makeLenses ''Client
根据Parallel and Concurrent Programming in Haskell,在Concurrent Haskell中的通用设计模式中使用可变包装器中的不可变数据结构。收到心跳消息后,处理该消息的线程将构造一个新的Client
,锁定MVar
的{{1}},将HashMap
插入Client
将新的HashMap
放在HashMap
中。代码基本上是:
MVar
这种方法运行良好,但随着客户数量的增长(我们现在有数万个客户端),出现了几个问题:
modifyMVar hashmap_mvar (\hm ->
let c = Client id ...
in return $! M.insert id c hm)
的访问争用。ClientMap
中包含的大型不可变结构将使垃圾收集器非常繁忙。现在,为了减少全局MVar
的争用,我尝试为每个客户端在hashmap_mvar
中包含Client
的可变字段,例如:
MVar
这似乎降低了争用级别(因为现在我只需要更新每个data ClientState = ClientState
{
_c_timestamp :: !POSIXTime
, _c_addr :: !SockAddr
, _c_load :: !Int
...
}
makeLenses ''ClientState
data Client = Client
{
c_id :: !ID
, c_state :: MVar CameraState
}
中的MVar
,粒度更精细),但程序的内存占用率仍然很高。我也尝试过UNPACK的一些字段,但这没有帮助。
有什么建议吗? STM会解决争用问题吗?我应该使用包含在Client
中的不可变数据结构之外的可变数据结构吗?
另见Updating a Big State Fast in Haskell。
编辑:
正如Nikita Volkov指出的那样,在典型的基于TCP的服务器 - 客户端应用程序中,共享地图闻起来像是糟糕的设计。但是,在我的情况下,系统是基于UDP的,这意味着没有“连接”这样的东西。服务器使用单个线程从所有客户端接收UDP消息,解析它们并相应地执行动作,例如,更新客户端数据。另一个线程定期读取地图,检查心跳的时间戳,并删除那些在过去5分钟内没有发送心跳的人,比如说。好像共享地图是不可避免的?无论如何,我知道首先使用UDP是一个糟糕的设计选择,但我仍然想知道如何通过UDP改善我的情况。
答案 0 :(得分:2)
首先,为什么你需要共享地图呢?你真的需要与任何东西分享客户的私人状态吗?如果不是(这是客户端 - 服务器应用程序的典型情况),那么您可以简单地在没有任何共享映射的情况下四处走动。
实际上,a "remotion" library包含所有客户端 - 服务器通信,只需使用自定义协议扩展服务即可创建服务。你应该看看。
其次,在某个实体的字段上使用多个MVar
总是潜在的竞争条件错误。当您需要以原子方式更新多个内容时,您应该使用STM。我不确定您的应用中是否存在这种情况,但您应该知道这一点。
第三,
客户端经常发送心跳消息(大约每30秒一次),导致ClientMap的访问争用
似乎只是最近发布的"stm-containers" library Map
的工作。有关库的介绍,请参阅this blog post。您将能够使用此功能返回不可变Client
模型。