2个线程如何共享同一个缓存行

时间:2013-01-17 03:18:08

标签: c++ c linux multithreading

我正在使用自定义网络协议库。该库基于TCP / IP构建,据称可用于高频消息传递。它是一个非阻塞库,使用回调作为与调用者集成的接口。

我不是表演专家,这就是我决定在这里提出这个问题的原因。自定义库带有特定约束,概述如下:

  

“Callee不应该在上下文中调用任何库的API   回调线程。如果他们试图这样做,线程会   挂起“

克服API限制的唯一方法是我启动另一个处理消息的线程并调用库来发送响应。库线程和进程线程将共享一个公共队列,该队列将受到互斥锁的保护,并使用wait_notify()调用来指示是否存在消息。

如果我每秒收到80k消息,那么我会让线程休眠并经常唤醒它们,执行线程上下文切换每秒80k次。

另外,由于有两个线程,它们不会在L1缓存中共享消息缓冲区。包含消息的缓存行首先由库的线程填充,然后逐出并拉入进程线程的核心L1缓存。我是否遗漏了某些内容,或者图书馆的设计是否可能不适用于高性能用例?

我的问题是:

  1. 我看到过警告,例如“不要在回调的上下文中使用此API,因为它可能会导致锁定。”跨越许多图书馆。导致此类设计约束的常见设计选择是什么?如果是多次调用锁的同一线程的简单问题,他们可以使用递归锁。这是一个可重入的问题,以及哪些挑战可能导致API所有者制作不可重入的API?

  2. 上述设计模型中是否有一种方法,即库线程和进程线程可以共享同一个内核,从而共享一个缓存行?

  3. volatile sig_atomic_t作为在两个线程之间共享数据的机制有多贵?

  4. 鉴于高频情况,在两个线程之间共享信息的轻量级方法是什么?

  5. 库和我的应用程序是基于C ++和Linux构建的。

2 个答案:

答案 0 :(得分:6)

  

2个线程如何共享同一个缓存行?

线程与缓存行无关。至少没有明确说明。可能出现的问题是在上下文切换和TLB失效时缓存刷新,但是给定了线程的相同虚拟地址映射,缓存通常应该忽略这些内容。

  

导致此类设计约束的常见设计选择是什么?

图书馆的实施者不想处理:

  1. 复杂的锁定方案。
  2. 可重入逻辑(即您调用'send()',库会使用on_error()回拨您,您再次从中调用send() - 这需要他们特别小心)。
  3. 我个人认为,在涉及高性能,特别是与网络相关的事情时,围绕回调设计API是一件非常糟糕的事情。虽然有时它会让用户和开发人员的生活变得更加简单(就简单编写代码而言)。唯一的例外可能是CPU中断处理,但这是一个不同的故事,你很难称之为API。

      

    如果是同一个线程调用锁多次的简单问题,他们可以使用递归锁。

    递归互斥体相对非常昂贵。关心运行时效率的人倾向于尽可能避免使用它们。

      

    上述设计模型中是否有一种方法,即库线程和进程线程可以共享同一个内核,从而共享一个缓存行?

    是。您必须将两个线程都固定到同一CPU内核。例如,使用sched_setaffinity()。但这也超出了单个程序 - 整个环境必须正确配置。例如,您可能想要考虑不允许操作系统在该核心上运行任何东西,但是您的两个线程(包括中断),并且不允许这两个线程迁移到不同的CPU。

      

    volatile sig_atomic_t作为在两个线程之间共享数据的机制有多贵?

    本身并不昂贵。但是,在多核环境中,您可能会缓存失效,停顿,增加的MESI流量等。鉴于两个线程都在同一个核心上并且没有任何入侵 - 唯一的惩罚是无法缓存变量,这是好的,因为它不应该被缓存(即编译器总是从内存中获取它,是缓存或主内存)。

      

    鉴于高频情况,在两个线程之间共享信息的轻量级方法是什么?

    从/向同一内存读写。可能没有任何系统调用,阻塞调用等。例如,对于英特尔架构,至少可以通过使用内存屏障来实现两个并发线程的环形缓冲区。为了做到这一点,你必须非常注重细节。但是,如果某些东西必须明确同步,那么原子指令就是下一个层次。 Haswell还附带了Transactional Memory,可用于低开销同步。之后没有什么是快的。

    另外,请查看Intel Architectures Developer's Manual, Chapter 11,关于内存缓存&控制。

答案 1 :(得分:1)

这里要记住的一件重要事情是,在处理网络应用程序时,更重要的性能指标是“每个任务的延迟”,而不是整个应用程序的原始cpu周期吞吐量。为此,线程消息队列往往是以最快的方式响应活动的一种非常好的方法。

在今天的服务器基础设施(甚至我的Core i3笔记本电脑)上,每秒80k消息接近于无关紧要的领域 - 特别是在L1缓存性能方面。如果线程正在做大量的工作,那么每次处理消息时期望CPU通过L1缓存刷新并且如果消息根本没有做很多工作就完全没有道理,那么它只是无关紧要,因为无论L1策略如何,它都可能不到CPU负载的1%。

按照该消息传递速率,我建议使用被动线程模型,例如。一个线程被唤醒以处理消息,然后又回到睡眠状态。这将为您提供最佳的延迟与性能模型。例如,它不是性能最有效的方法,但它最能快速响应网络请求(这通常是您在进行网络编程时所希望的)。

在今天的体系结构(2.8ghz,4+核心)上,我甚至不会开始担心原始性能,除非我预计每秒可处理100万个排队消息。即便如此,它仍然取决于预期消息的实际工作量。它预计不会做准备和发送一些数据包,然后1 mil肯定是保守的。

  

上述设计模型中是否有一种方法,即库线程和进程线程可以共享同一个内核,从而共享一个缓存行?

没有。我的意思是,确定如果你想要推出自己的操作系统。但是如果你想在多任务环境中运行并希望与其他任务共享CPU,那么“不”。将线程锁定到核心很可能会损害线程的平均响应时间,而不会提供更好的性能。 (并且任何性能提升都取决于系统专门用于您的软件,并且可能会在运行多个任务的系统上消失)

  

鉴于高频情况,在两个线程之间共享信息的轻量级方法是什么?

消息队列。 :) 认真。我并不是说听起来很傻,但那就是消息队列。它们在两个线程之间共享信息,它们通常都很轻松。如果你想减少上下文切换,只有在累积了一些消息后(或者在活动较少的情况下有一些超时时间),才会通知工作人员排空队列 - 但是厌倦会增加程序的响应时间/延迟。