2012-04-19 109 views
0

我试图实现一个基于UDP的服务器,它维护两个插座,一个用于控制(ctrl_sock),另一个用于数据传输(data_sock)。事情是,ctrl_sock总是上行链路,data_sock是下行链路。也就是说,客户端将通过ctrl_sock请求数据传输/停止,数据将通过data_sock发送给他们。在两个插座之间共享信息

现在的问题是,由于模型没有连接,服务器将不得不维护已注册客户信息的列表(我称之为peers_context),以便它可以“盲目地”向他们推送数据,直到他们询问停止。在此盲传期间,客户端可能会异步通过ctrl_sock向服务器发送控制消息。除了初始请求和停止之外,这些信息也可以是例如文件部分的偏好。因此,peers_context必须异步更新。然而,通过data_sock的传输依赖于这个peers_context结构,因此引起了ctrl_sockdata_sock之间的同步问题。我的问题是,我能做些什么来安全地维护这两个袜子和peers_context结构,这样异步更新peers_context不会造成严重破坏。顺便说一下,peers_context的更新不会很频繁,这就是为什么我需要避免请求 - 回复模型。

我最初考虑的实现是在主线程(listener线程)中维护ctrl_sock,并且在另一个线程(工作线程)中维护通过data_sock进行的传输。但是,我发现在这种情况下很难同步。例如,如果我在peers_context中使用互斥锁,每当工作线程锁定peers_context时,由于工作线程无休止地工作,侦听线程在它需要修改peers_context时不再有权访问它。另一方面,如果侦听器线程持有peers_context并写入它,工作线程将无法读取peers_context并终止。有人可以给我一些建议吗?

顺便说一下,这个实现是在C语言的Linux环境下完成的。只有监听线程偶尔需要修改peers_context,工作线程只需要读取。衷心感谢!

+0

为什么不使用TCP?您可以与每个客户端建立基于流的连接,并执行必要的通信,而不用担心同步它。比UDP更容易,更可靠。 – Raam 2012-04-19 18:06:42

+0

听起来像你只需要稍微复杂的锁定。显然你不能让工人无休止地锁住。您需要锁定,发送数据包,解锁 – TJD 2012-04-19 18:10:04

+2

使用select()或poll()将这个状态机重新编码为状态机是否可能/合意?这可能比一个线程化设计更好,并且避免了所有这些刺激的线程同步问题! – 2012-04-19 18:19:53

回答

1

如果您的peers_context存在强烈争用,那么您需要缩短关键部分。你谈到了使用互斥锁。我假设你已经考虑改变为读者+作家锁,并拒绝它,因为你不想让不断读者饿死作家。这个怎么样?

做一个非常小的结构,是一个间接引用peers_context这样的:

struct peers_context_handle { 
    pthread_mutex_t ref_lock; 
    struct peers_context *the_actual_context; 
    pthread_mutex_t write_lock; 
}; 

数据包的发送者(读者)和控制要求处理器(作家)总是通过这种间接访问peers_mutex

假设:数据包发送者从不修改peers_context,也没有释放它。

帕克发件人简要锁手柄,获得peers_context的当前版本和解锁:

pthread_mutex_lock(&(handle->ref_lock)); 
peers_context = handle->the_actual_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

(在实践中,你甚至可以用锁做的路程,如果你介绍记忆障碍,因为指针解引用是对Linux支持的所有平台的原子,但我不会推荐它,因为你将不得不开始钻研记忆障碍和其他低层次的东西,也不ç也不POSIX保证它无论如何都会正常工作。)

请求处理器不更新peers_context,他们复制并共享完全取代它。这就是他们如何保持他们的关键部分小。他们使用write_lock序列化更新,但更新很少,所以这不是问题。

pthread_mutex_lock(&(handle->write_lock)); 

/* Short CS to get the old version */ 
pthread_mutex_lock(&(handle->ref_lock)); 
old_peers_context = handle->the_actual_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

new_peers_context = allocate_new_structure(); 
*new_peers_context = *old_peers_context; 

/* Now make the changes that are requested */ 
new_peers_context->foo = 42; 
new_peers_context->bar = 52; 

/* Short CS to replace the context */ 
pthread_mutex_lock(&(handle->ref_lock)); 
handle->the_actual_context = new_peers_context; 
pthread_mutex_unlock(&(handle->ref_lock)); 

pthread_mutex_unlock(&(handle->write_lock)); 

magic(old_peers_context); 

这有什么用?这是最后一行代码中的魔力。您必须释放peers_context的旧副本以避免内存泄漏,但您无法这样做,因为可能有数据包发件人仍在使用该副本。

该解决方案与在Linux内核中使用的RCU类似。你必须等待所有的数据包发送者线程进入静态状态。我要离开这个实施作为练习你:-)但这里有准则:

  • magic()函数添加old_peers_context所以要被释放队列(这必须由一个互斥体的保护)。
  • 一个专用线程释放这个名单在一个循环:
    1. 它锁定到待释放名单
    2. 它获得的指针列表
    3. 它取代了清单,一个新的空单
    4. 它解锁,以待释放名单
    5. 它清除与每个工作线程
    6. 它等待所有标记再次设置
    7. 它释放每个相关的标记在其先前获得的待释放列表副本中的项目
  • 同时,每个工作线程在其事件循环中的空闲点处设置其自己的标记(即,当它不忙于发送任何数据包或持有任何peer_contexts时。
+0

Hi @Celada,真诚地感谢您的回复,这些知识相当有帮助,我有点困惑的一件事是在“魔术”部分,我怎么知道包发送者何时进入静态状态并让'free_old_peers_context'线程知道它?正如我所提到的,只要在peers_context中至少有一个对等点处于活动状态,包发送者就会继续发送包(通常我会遍历对等列表一次,然后将数据包发送给每个对象,然后遍历列表,也许我应该在这里添加一个空闲点?)再次感谢。 – 2012-04-20 13:26:36

+0

您必须安排您的应用程序拥有一个静止的状态如果你使用了一个事件循环(很可能,因为你是IO绑定的),那么在你调用poll()之前或之后它就是正确的。 – Celada 2012-04-20 15:28:38

+0

你的意思是发送循环内部还是外部?事情是,select()或poll()将阻止发送者,这意味着,只能在监听套接字可读时发送一次。为了您的信息,这里是我的一段代码[链接](http://pastebin.com/XB73mLUF)。 (我还没有开始应用您的读写器自旋锁。) – 2012-04-20 17:10:42