【问题标题】:Sharing generic data between threads在线程之间共享通用数据
【发布时间】:2023-03-25 17:22:01
【问题描述】:

我需要为诸如客户端服务器交互之类的东西创建一个库。数据请求并不频繁,而且大多只有几个字节(整数、长整数)。对用户定义结构的请求很少。我已经查看了一些类似的问题(thesethese),但没有找到我在这里所问问题的答案,因为我主要担心数据是通用的。

我在设计中遇到的问题是线程之间的数据共享: 我的库有 2 个线程(pthreads),其中一个是 msg 处理程序(事件循环)。 msg 处理程序循环从服务器获取响应并需要将数据传递给另一个线程。数据是通用的 - 可以是用户定义的 struct 或 int 或 long。此外,可能会多次收到相同类型的响应(每个响应都是唯一标识的),这意味着每个数据类型的单个全局变量都不会这样做。另一个线程正在等待条件变量,并将被 msg 处理程序线程唤醒。条件变量不在布尔变量上,而是检查 msg 处理程序是否已将请求的信息添加到某个“已知位置”。那么,如何实现线程间的数据共享呢?

我考虑了以下一些方法:

  1. 全局通用列表 - 带有 void *data...处理程序线程在接收到服务器的响应时分配内存并向其他线程发出信号。第一个线程唤醒并遍历列表以寻找特定的 id。找到后,使用数据,然后释放内存。

    无需定义不同的 msg 类型(用于 int、struct、long 等),但 walk 会比较长。

  2. 全局列表数组 - 每种数据类型的列表头指针数组。分配和免费同上。除了,数组是按数据类型(枚举)索引的。

    walk 相对较短,但需要为每个列表定义不同的 msg_types。

  3. 我还关心动态分配像 int 这样会导致碎片的小数据。所以尝试分配一个没有动态分配的数组,但在语义上没有成功。 (找不到足够复杂的例子)

    如果这是正确的方法,我会在这方面投入更多时间。 (举个例子会有所帮助!)

  4. 用于生成每个类型的 struct/msg 的宏...刚刚决定这是一个矫枉过正(在示例中看起来非常复杂)

  5. 共享内存 - 使用 char * 缓冲区进行分配似乎太尴尬了..

在这种情况下,最好的设计是什么。我希望这是可以理解的。

【问题讨论】:

  • 您是否考虑过使用管道从“消息处理程序”线程到“其他”线程进行通信?
  • 是的,但是根据我对管道的了解(仅基于 [beej.us/guide/bgipc/output/html/multipage/pipes.html] 并且没有实际经验),我认为它是将数据写入文件,这意味着我需要转换我的结构更基本的东西?另外,我上面没有提到的是第一个线程可能不止一个。即不止一个读者线程。不确定这对管道是否有影响。
  • 管道允许您从一侧写入,然后从另一侧(fifo)读取。它最适合一位作家和一位读者。但是,有些实现类似于您的实现,每个(第一个线程)使用一个管道,(另一个)线程将监视和服务(即:读取)所有管道。
  • 请注意,您的问题与设计相关,通常在 programmers.stackexchange.com 而不是 stackoverflow 上解决。

标签: c multithreading


【解决方案1】:

这是一个生产者-消费者的场景,我的偏好是实现一个阻塞队列。

事件处理程序将消息推送到队列中。工作线程在队列为空时阻塞,当有东西要处理时唤醒。

消息可以被分配和释放,但从空闲列表中提取它们通常会更好。消息只是类型的联合,具有共同的类型字段和 id。漂亮又简单。

扩展到多个工作线程和/或新消息类型很简单。

您可以轻松找到以您选择的语言为此类阻塞队列预先编写的代码。

[您的约束可能有些地方不适合这种方法,但如果是这样,请编辑您的问题以明确说明。]


在 C++ 中,可变长度阻塞队列有三种基本策略。假设一条消息是不同大小的不同类型的并集。

  1. 队列中的消息大小固定,并包含指向消息的指针。每条消息都根据需要分配和释放。
  2. 队列中的消息大小固定,并包含指向消息的指针。消息保存在池(空闲列表/数组)中以避免分配开销。
  3. 队列中的消息大小可变。它们由推送到队列中的字节流组成,长度可以知道要弹出多少。

您可以通过搜索“阻塞队列可变长度 C++”或类似内容轻松找到示例代码和完整的工作示例。

【讨论】:

  • 我上面提到的通用列表不是类似于事件队列吗?是的,我可以使用阻塞队列而不是 condvar(我选择 condvar 的原因不明),但问题是队列实现。我考虑过“工会”,但分配给工会的大小不是其最大成员的大小吗?这意味着我每次都会分配 60 个字节,即使我当前的数据是 4 字节 int
  • 我的重点是让您考虑阻塞队列。其余的是实现细节,可以通过网络搜索轻松找到。请参阅编辑了解一些起点。
【解决方案2】:

我的库有 2 个线程

数据请求不频繁

这些是决定性因素:这意味着您不会遇到与吞吐量相关的问题,例如内存分配压力,并且您不需要复杂的信号系统。

  1. 您可以使用简单的列表来存储您的项目,并根据需要使用 malloc/free 结构
  2. 只有两个线程,即 1 个生产者/1 个消费者设置,这意味着您可以简单地在列表中使用保护单元来有效地在消费者和生产者之间分配使用量,而无需互斥。

但是,如果您认为这些因素在未来可能会发生变化,您需要确保访问您的列表数据结构的代码与其他代码(即,制作一个 API)有很好的区别,以便为您留出空间稍后修改该列表的用法(比如通过包含互斥锁或包含消息键索引)。

关于数据结构:你需要你的代码知道它必须处理什么类型的数据,这意味着被传递的数据必须包含一个类型标签。最简单的方法是创建一个你必须处理的所有数据类型的联合类型,并在它前面加上一个标记数据的枚举值:

typedef enum {
   TYPE1,
   TYPE2,
   TYPE3
   // etc.
} data_tag;

typedef union {
   struct type1 {/* .. */};
   struct type2 {/* .. */};
   struct type3 {/* .. */};
   // etc.
} data_content;


typedef struct {
   data_tag tag;
   data_content value;
} message;

然后,在您的代码中,只需在标签字段上使用 switch 来区分用例。

内存处理的一个可能解决方案是使用所谓的memory pools 而不是简单的malloc 方案。基本上,您预先分配了每个data_content 子类型的许多实例,并围绕它们构建您的消息。

typedef struct {
   data_tag tag;
   data_content *value;
};

如果你很小心(即你正确地构造你的值,并适当地打开它们),你实际上指向一个单一的数据类型而不是一个联合这一事实应该不重要,并且应该让你避免混乱你的具有多种类型转换的代码。

对于较小的内存压力,您可能不需要使其过于复杂,但您可以添加一种机制来扩展这些池,以防需求旺盛,将它们实现为结构数组的列表。

如果您真的担心代码复杂性,请考虑使用支持内置内存回收的语言(例如,Erlang、OCaml、Go、Python 等)。此外,许多函数式语言(如 OCaml)都支持所谓的algebraic datatypes,从而可以轻松地在该语言中实现上述消息类型。

【讨论】:

  • 是的。我目前使用的方法是类似的——一个列表。但是,我不确定为通用数据设计列表的方式,以及分配和释放这些数据的碎片问题。
  • 感谢您的意见。我对联合的唯一担心是即使当前数据为 4 个字节,我也会为每条消息分配(在内存池的情况下使用)60 个字节。是不是可以忽略的小事?
  • 我的意思是你可以在消息中使用分配的struct 的指针(来自内存池)而不是联合。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-11-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-05
  • 1970-01-01
相关资源
最近更新 更多