【问题标题】:sharing information among two sockets在两个套接字之间共享信息
【发布时间】:2012-04-19 18:01:00
【问题描述】:

我正在尝试实现一个基于 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 的更新不会很频繁,所以我需要避免使用请求-回复模式。

我对实现的初步考虑是,在主线程(监听线程)中维护 ctrl_sock,在另一个线程(工作线程)中维护data_sock 上的传输。但是,我发现在这种情况下很难同步。例如,如果我在peers_context 中使用互斥锁,那么每当工作线程锁定peers_context 时,当需要修改peers_context 时,侦听线程将无法再访问它,因为工作线程无休止地工作。另一方面,如果侦听器线程持有peers_context 并对其进行写入,则工作线程将无法读取peers_context 并终止。谁能给我一些建议?

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

【问题讨论】:

  • 为什么不使用 TCP?您可以与每个客户端建立基于流的连接并执行必要的通信,而无需担心同步。比 UDP 更简单、更可靠。
  • 听起来您只需要稍微复杂一点的锁定。显然你不能让工人无休止地持有锁。你需要加锁、发包、解锁
  • 是否可以/希望使用 select() 或 poll() 将其重新编码为状态机?这可能比线程设计执行得更好,并且避免了所有那些恼人的线程同步问题!
  • @Raam 这是我研究中的模拟工作,没有考虑TCP流量模型。传输的可靠性由应用层保证。
  • 嗨,@SimonElliott,使用 select() 看起来是个好主意。我会研究一下,谢谢指出。

标签: c multithreading sockets process


【解决方案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,也永远不会释放它。

Packer 发送者短暂锁定手柄,获取peers_context 的当前版本并解锁:

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

(实际上,如果引入内存屏障,您甚至可以取消锁定,因为指针解引用在 Linux 支持的所有平台上都是原子的,但我不推荐它,因为您必须开始深入研究内存障碍和其他低级的东西,C 和 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 的时间点)设置自己的标记。

【讨论】:

  • 嗨@Celada,衷心感谢您的友好回复。这个知识很有帮助。我有点困惑的一件事是在“魔术”部分,我怎么知道数据包发送方何时进入静止状态并让free_old_peers_context 线程知道呢?正如我所提到的,只要peers_context 中至少有一个对等点处于活动状态,数据包发送者就会继续发送数据包。 (一般我会遍历peers列表一次,然后给每个人发包,然后一遍遍地遍历列表,也许我应该在这里添加一个空闲点?)再次感谢。
  • 您必须安排您的应用程序处于静止状态。如果您使用事件循环(很可能,因为您受 IO 限制),那么它就在您调用 poll() 之前或之后。
  • 您是指在发送循环内部还是外部?问题是,无论是 select() 还是 poll() 都会阻塞发送者,这意味着,当监控套接字可读时,我只能发送一次。供您参考,这是我的一段代码link。 (我还没有开始应用你的读写器自旋锁。)
  • 您粘贴的代码片段似乎与在控制​​套接字上等待消息有关。我以为你有工作线程连续或频繁地发送数据包。这些是需要具有静止状态的那些。我建议在那些线程中poll()之前或之后。
  • 我不知道您的工作线程中是否需要或使用poll()。我想你可能有一个事件循环,它从生成它们的任何东西中提取数据包,或者一个等待data_sock 准备好发送的事件循环(POLLOUT),但如果这不是你的工作线程的设计方式,那就是没关系。但是你一个接一个地发送包裹。你必须在那里有一个循环。并且该循环是否没有静止点,在该点可以保证没有peer_context 结构被保持?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多