大量关键部分有啥问题吗?
Posted
技术标签:
【中文标题】大量关键部分有啥问题吗?【英文标题】:Any issues with large numbers of critical sections?大量关键部分有什么问题吗? 【发布时间】:2009-11-03 10:38:05 【问题描述】:我有大量的结构,像这样:
typedef struct
int a;
int b;
int c;
etc...
data_type;
data_type data[100000];
我有一堆单独的线程,每个线程都想修改 data[] 中的元素。我需要确保没有线程尝试同时访问相同的数据元素。准确地说:一个线程执行数据[475].a = 3;另一个线程执行 data[475].b = 7;不允许同时执行数据[475].a = 3;而另一个线程执行 data[476].a = 7;被允许。该程序对速度高度至关重要。我的计划是为每个数据元素创建一个单独的关键部分,如下所示:
typedef struct
CRITICAL_SECTION critsec;
int a;
int b;
int c;
etc...
data_type;
从某种意义上说,我想这一切都应该工作,我应该没有真正的问题,但在多线程编程方面没有太多经验,我只是对有这么多关键部分感到有点不安。我想知道他们的数量是否会造成某种低效率。我还想知道其他一些多线程技术是否会更快?我应该放松一下并继续执行 A 计划吗?
【问题讨论】:
注意:变量数组可能会在线程之间引入错误的共享。您应该真正将您的结构对齐到 CACHELINE 倍数。 听起来很有趣——但我不太明白你在说什么。您能否对此进行扩展或将我指向一篇文章? 如果我在谷歌中输入 ["csheline multiples" threads] 我得到零点击:-( 那是因为您应该复制粘贴该术语,这样可以节省您的打字错误。 “CACHELINE multiples”获得超过 131.000 次点击。 好点...我猜你在谈论这种现象:ddj.com/go-parallel/article/… 【参考方案1】:有了这么多对象,它们的大部分关键部分都将被解锁,几乎不会发生争用。如您所知(其他评论),如果关键部分是无主的,则不需要内核模式转换。这使得关键部分在这种情况下很有效。
唯一的其他考虑因素是您是否希望将关键部分放在对象中或另一个数组中。引用的局部性是将关键部分放在对象内的一个很好的理由。当您进入临界区时,整个高速缓存行(例如 16 或 32 字节)将在内存中。通过一些填充,您可以确保每个对象都从缓存行开始。因此,一旦进入其临界区,该对象将(部分)在缓存中。
【讨论】:
【参考方案2】:你的计划值得一试,但我想你会发现 Windows 不乐意创建那么多关键部分。每个 CS 都包含一些内核句柄,您正在使用宝贵的内核空间。我认为,根据您的 Windows 版本,您将耗尽句柄内存并且 InitializeCriticalSection() 或其他一些函数将开始失败。
你可能想要做的是有一个可用的 CS 池,并在你的结构中存储一个指向“正在使用”的 CS 的指针。但这很快就会变得棘手,您将需要使用原子操作来设置/清除 CS 指针(以原子地将数组条目标记为“正在使用”)。可能还需要一些引用计数等...
变得复杂。
所以先试试你的方法,看看会发生什么。我们曾经遇到过类似的情况,我们不得不去游泳池,但也许从那以后情况发生了变化。
【讨论】:
“每个 CS 都包含一些内核句柄”你确定吗?其他答案暗示他们没有。您确定您没有与互斥锁混淆吗? 我可以。但我认为这个想法是 CSes,IIRC 在没有争用时避免内核(即,只需一个原子操作来获得无主 CS 的所有权),但如果存在 i> 争用,那么你需要进入内核。我已经有一段时间没有阅读 SysInternals 或类似的东西了,但我认为其中还有很多其他的东西——簿记,几乎是调试级别的东西。 CS 包含一个事件。 “当线程第一次尝试获取临界区时,操作系统会默默地创建其中一个,但被另一个已经拥有它的线程阻塞。”这样,随着您使用应用程序的时间足够长,创建的事件数量会逐渐增加。 msdn.microsoft.com/en-us/magazine/cc164040.aspx【参考方案3】:根据 data_type 结构中的数据成员类型(还取决于您要对这些成员执行的操作),您可能可以放弃使用单独的同步对象,而改用 Interlocked 函数。
在您的示例代码中,所有数据成员都是整数,并且所有操作都是赋值(并且可能是读取),因此您可以使用 InterlockedExchange() 以原子方式设置值,并使用 InterlockedCompareExchange() 以原子方式读取值。
如果您需要使用非整数数据成员类型,或者如果您需要执行更复杂的操作,或者如果您需要一次协调对多个操作的原子访问(例如,读取数据[1]. a 然后写入数据[1].b),那么您将不得不使用同步对象,例如 CRITICAL_SECTION。
如果您必须使用同步对象,我建议您考虑将数据集划分为子集,并为每个子集使用一个同步对象。例如,您可以考虑为数据数组中的每个跨度 1000 个元素使用一个 CRITICAL_SECTION。
【讨论】:
【参考方案4】:您也可以考虑使用 MUTEX。 这是很好的方法。 每个客户端都可以通过互斥锁(互斥)自行保留资源。
这比较常见,一些库也支持线程。 阅读boost::thread,它是mutexes
采用您的方法:
data_type data[100000];
我害怕堆栈溢出,除非你在堆上分配它。
编辑:
Boost::MUTEX uses win32 Critical Sections
【讨论】:
我认为互斥锁应该慢得多? 临界区被实现为具有固定自旋计数的自旋锁,之后它会抓取一个互斥锁。 我认为这是个好主意,因为 boost::mutex 在 windows 上使用了 CriticalSections。 术语混淆。在 Microsoft Windows 上下文中(我们可以从标签中推断出),互斥锁是跨进程内核级同步对象,而关键部分是进程内的,通常不需要内核模式切换。 boost::mutex 更接近 CompSci 的互斥概念,它根本不处理进程。 是的 - 尝试用计算机科学术语与某人交谈是相当痛苦的,因为他们所了解的只是他们在 MSDN 上阅读的内容。【参考方案5】:正如其他人指出的那样,是的,存在一个问题,它被称为太细粒度的锁定。它会浪费资源,即使机会很小,当事情发生时,您也会开始创建大量的支持原语和数据得到一个偶尔的,比平时更长的时间或其他,争吵。另外,您正在浪费资源,因为它实际上并不是一个微不足道的数据结构,例如在 VM impls 中......
如果我没记错的话,从那时起,您在 Win32 上出现 SEH 异常的几率会更高,或者只是更高的内存使用率。分区和池化它们可能是要走的路,但它是一个更复杂的实现。对其他事物进行分区(re:action)并期待一些短暂的争用是另一种处理方式。
无论如何,这都是你现在拥有的资源管理的问题。
【讨论】:
以上是关于大量关键部分有啥问题吗?的主要内容,如果未能解决你的问题,请参考以下文章