使用线程不安全的静态变量锁定嵌套函数

Posted

技术标签:

【中文标题】使用线程不安全的静态变量锁定嵌套函数【英文标题】:Locking nested function with thread-unsafe static variable 【发布时间】:2020-01-05 17:20:05 【问题描述】:

我有一个函数memoize,它读写一个静态的std::map,例如:

int memoize(int i)

static std::map<int,int> map;
const auto iterator = memoizer.find(i);
if (iterator == memoizer.end())
  memoizer[i]=i+1;
else
  return iterator->second;

memoize 被其他函数调用,而其他函数又被其他函数调用,而这些函数又被main 中的函数调用。现在如果在main 我有类似的东西

#pragma omp parallel for
for (int i=0; i<1000; i+++)
  f(i); \\function calling memoize

for (int i=0; i<1000; i+++)
  g(i); \\function calling memoize

然后我在第一个循环中遇到问题,因为std::map 不是线程安全的。我试图找出一种仅在使用 openmp 时才锁定静态地图的方法(因此仅适用于第一个循环)。我宁愿避免重写所有函数来获取额外的omp_lock_t 参数。

实现这一目标的最佳方法是什么?希望使用尽可能少的宏。

【问题讨论】:

考虑拥有两个独立的函数:一个是线程安全的,一个不是。 @freakish 有没有线程安全的替代std::map?以及如何从内部检测我是否在 openmp 区域中? 我的建议是不检测任何东西。您编写了两个函数,由调用者决定使用哪一个。在您的情况下,最简单的线程安全是锁定静态std::mutex。不过有更高效的并发地图,你必须谷歌它。 @freakish 调用者不直接调用这些函数,而是其他函数调用函数......调用这两个替代方案。因此标题中的“嵌套” 如果内存化值得花时间与多线程同步,那么如果单个线程锁定互斥锁(即使不需要)也可能没问题。 【参考方案1】:

解决您的问题的一个非常简单的方法是使用critical OpenMP directive 保护代码的读取和更新部分。当然,为了减少与其他critical 发生不必要的交互/冲突的风险,您的代码中可能已经有其他地方,您应该给它一个名称以清楚地识别它。如果您的 OpenMP 实现允许它(即标准的版本足够高),并且如果您有相应的知识,您可以add a hint 了解您期望从线程之间的更新中获得多少争用。

然后,我建议你检查一下这个简单的实现是否给你足够的性能,这意味着critical 指令的影响是否在性能方面不是太多。如果您对性能感到满意,请保持原样。如果没有,那么回来提出一个更准确的问题,我们会看看能做些什么。

对于简单的解决方案,代码可能如下所示(此处提示预计会出现高争用,这只是为您提供的示例,而不是我的真实期望):

int memoize( int i ) 
    static std::map<int,int> memoizer;
    bool found = false;
    int value;
    #pragma omp critical( memoize ) hint( omp_sync_hint_contended )
    
        const auto it = memoizer.find( i );
        if ( it != memoizer.end() ) 
           value = it->second;
           found = true;
        
    
    if ( !found ) 
        // here, we didn't find i in memoizer, so we compute it
        value = compute_actual_value_for_memoizer( i );
        #pragma omp critical( memoize ) hint( omp_sync_hint_contended )
        memoizer[i] = value;
    
    return value;

【讨论】:

您是否还有数据竞争,因为一个线程可能正在调用find 而另一个线程正在插入? OP 还指出 i+1 代表实际的时间密集型任务,所以我想这不应该是关键部分的一部分。如何解决这个问题可能很明显,但我想如果你展示如何解决这个问题,答案会更清楚。 确实在映射中的读取和写入之间存在竞争条件。 ATM,读取部分需要保护,写入部分可能会在访问以从另一个线程读取时对其进行修改。然而,即使读取受到保护,线程之间仍然会存在一些竞争,以便首先创建第 i 个条目。这是一个真正的问题吗,很难说...我会修复代码 我不担心可能会浪费时间进行多次计算的竞争条件。我担心未定义的行为。至少从 C++ 标准的角度来看,调用 findoperator[] 未同步是一种数据竞争并导致未定义的行为。我对 OpenMP 了解得不够多,不知道它是否提出了任何其他要求。 同样如此。这就是为什么我什至没有尝试避免给定值的地图可能的多次计算和更新,因为它们很快就会消失。但是,既然读取也受到保护,对性能的影响可能会变得相当高......由 OP 看到。如果需要,我可以考虑使用原子的 2 级解决方案,这样成本会更低 转念一想,我意识到对iterator-&gt;second() 甚至memoizer.end() 的访问也需要受到保护。所以我以一种我认为现在是防弹的方式重写了代码。但它是否有效是另一个问题。

以上是关于使用线程不安全的静态变量锁定嵌套函数的主要内容,如果未能解决你的问题,请参考以下文章

可重入和线程安全

线程安全和静态函数

如果不修改静态类变量,非同步静态方法是不是线程安全?

线程安全与可重入

PHP 源码学习之线程安全

[转]如何写出线程安全的类和函数