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_sockpeers_contextpeers_context不会造成破坏。顺便说一句,更新peers_context不会很频繁,这就是为什么我需要避免请求-回复模型。

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

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

4

1 回答 1

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.
于 2012-04-19T19:27:23.657 回答