使用c ++ 11 atomics编写(旋转)线程障碍

时间:2011-11-13 22:29:06

标签: c++ multithreading gcc c++11

我正在尝试熟悉c ++ 11原子,所以我尝试为线程编写一个屏障类(在有人抱怨不使用现有类之前:这更多是为了学习/自我改进,而不是由于任何实际需要)。我的班级基本上看起来如下:

class barrier
{
private:
    std::atomic<int> counter[2];
    std::atomic<int> lock[2];
    std::atomic<int> cur_idx;
    int thread_count;
public:
    //constructors...
    bool wait();
};

所有成员都被初始化为零,除了thread_count,它保存适当的计数。 我已将wait函数实现为

int idx  = cur_idx.load();
if(lock[idx].load() == 0)
{
    lock[idx].store(1);
}
int val = counter[idx].fetch_add(1);
if(val >= thread_count - 1)
{
    counter[idx].store(0);
    cur_idx.fetch_xor(1);
    lock[idx].store(0);
    return true;
}
while(lock[idx].load() == 1);
return false;

然而,当尝试使用两个线程(thread_count是2)时,第一个线程进入等待循环就好了,但第二个线程没有解锁障碍(似乎它甚至没有转到int val = counter[idx].fetch_add(1);,但我对此不太确定。但是当我使用volatile int代替std::atomic<int>并将wait写为{g}时使用gcc atomic-intrinsics如下:

int idx = cur_idx;
if(lock[idx] == 0)
{
    __sync_val_compare_and_swap(&lock[idx], 0, 1);
}
int val = __sync_fetch_and_add(&counter[idx], 1);
if(val >= thread_count - 1)
{
    __sync_synchronize();
    counter[idx] = 0;
    cur_idx ^= 1;
    __sync_synchronize();
    lock[idx] = 0;
    __sync_synchronize();
    return true;
}
while(lock[idx] == 1);
return false;

它运作得很好。根据我的理解,两个版本之间不应该有任何根本的区别(如果第二个应该不太可能工作的话,那就更多了)。那么以下哪种情况适用?

  1. 我很幸运,第二次实现,我的算法是废话
  2. 我没有完全理解std::atomic并且第一个变体存在问题(但不是第二个)
  3. 它应该可行,但c ++ 11库的实验性实现并不像我希望的那样成熟
  4. 对于记录我使用32位mingw和gcc 4.6.1

    调用代码如下所示:

    spin_barrier b(2);
    std::thread t([&b]()->void
    {
        std::this_thread::sleep_for(std::chrono::duration<double>(0.1));
        b.wait();
    });
    b.wait();
    t.join();
    

    由于mingw没有<thread>标题,我使用自编写的版本,基本上包含了适当的pthread函数(在某人问之前:是的,它没有屏障,所以它不应该是包装问题) 任何见解都将不胜感激。

    编辑:解释算法以使其更清晰:

    • thread_count是等待屏障的线程数(因此,如果thread_count线程在屏障中,则所有线程都可以离开屏障)。
    • 当第一个(或任何)线程进入屏障时,
    • lock设置为1。
    • counter计算屏障内有多少个线程,并为每个线程以原子方式递增一次
    • if counter>=thread_count所有线程都在屏障内,因此计数器和锁定重置为零
    • 否则线程等待lock变为零
    • 在下一次使用障碍时,使用不同的变量(counterlock),确保线程仍在等待第一次使用障碍时没有问题(例如,它们已被抢占)当屏障被抬起时)

    EDIT2: 我现在已经在linux下使用gcc 4.5.1对它进行了测试,其中两个版本似乎工作得很好,这似乎指向了mingw的std::atomic的问题,但我仍然没有完全相信,因为我看到了<atomic>标题重新表示大多数函数只是调用适当的gcc-atomic意味着两个版本之间确实不应该有区别

7 个答案:

答案 0 :(得分:23)

我不知道这是否会有所帮助,但Herb Sutter实现并发队列的以下片段使用了基于原子的自旋锁:

std::atomic<bool> consumerLock;

{   // the critical section
    while (consumerLock.exchange(true)) { }  // this is the spinlock

    // do something useful

    consumerLock = false;  // unlock
}

事实上,标准为这种结构提供了一种特制的类型,需要进行无锁操作std::atomic_flag。有了这个,关键部分看起来像这样:

std::atomic_flag consumerLock;

{
    // critical section

    while (consumerLock.test_and_set()) { /* spin */ }

    // do stuff

    consumerLock.clear();
}

(如果您愿意,可以使用获取和释放内存排序。)

答案 1 :(得分:5)

看起来不必要复杂。尝试这个更简单的版本(好吧,我还没有测试过,我只是在它上面思考:))):

#include <atomic>

class spinning_barrier
{
public:
    spinning_barrier (unsigned int n) : n_ (n), nwait_ (0), step_(0) {}

    bool wait ()
    {
        unsigned int step = step_.load ();

        if (nwait_.fetch_add (1) == n_ - 1)
        {
            /* OK, last thread to come.  */
            nwait_.store (0); // XXX: maybe can use relaxed ordering here ??
            step_.fetch_add (1);
            return true;
        }
        else
        {
            /* Run in circles and scream like a little girl.  */
            while (step_.load () == step)
                ;
            return false;
        }
    }

protected:
    /* Number of synchronized threads. */
    const unsigned int n_;

    /* Number of threads currently spinning.  */
    std::atomic<unsigned int> nwait_;

    /* Number of barrier syncronizations completed so far, 
     * it's OK to wrap.  */
    std::atomic<unsigned int> step_;
};

修改 @Grizzy,我在你的第一个(C ++ 11)版本中找不到任何错误,而且我也运行它,就像有两个线程的一亿个同步一样,它就完成了。我在双插槽/四核GNU / Linux机器上运行它,所以我更倾向于怀疑你的选择3. - 库(或者更确切地说,它的端口到win32)还不够成熟。 / p>

答案 2 :(得分:5)

以下是本书C++ Concurrency in Action: Practical Multithreading中的优雅解决方案。

struct bar_t {
    unsigned const count;
    std::atomic<unsigned> spaces;
    std::atomic<unsigned> generation;
    bar_t(unsigned count_) :
        count(count_), spaces(count_), generation(0)
    {}
    void wait() {
        unsigned const my_generation = generation;
        if (!--spaces) {
            spaces = count;
            ++generation;
        } else {
            while(generation == my_generation);
        }
    }
};

答案 3 :(得分:4)

这是我的一个简单版本:

// spinning_mutex.hpp
#include <atomic>


class spinning_mutex
{
private:
    std::atomic<bool> lockVal;
public:
    spinning_mutex() : lockVal(false) { };

    void lock()
    {
        while(lockVal.exchange(true) );
    } 

    void unlock()
    {
        lockVal.store(false);
    }

    bool is_locked()
    {
        return lockVal.load();
    }
};

用法:(来自std::lock_guard示例)

#include <thread>
#include <mutex>
#include "spinning_mutex.hpp"

int g_i = 0;
spinning_mutex g_i_mutex;  // protects g_i

void safe_increment()
{
    std::lock_guard<spinning_mutex> lock(g_i_mutex);
    ++g_i;

    // g_i_mutex is automatically released when lock
    // goes out of scope
}

int main()
{
    std::thread t1(safe_increment);
    std::thread t2(safe_increment);

    t1.join();
    t2.join();
}

答案 4 :(得分:2)

我知道这个帖子有点旧,但是因为它仍然是第一个使用c ++ 11搜索线程障碍时的google结果,所以我想提出一个解决方案来摆脱繁忙的等待使用std::condition_variable。 基本上它是寒冷的解决方案,但它使用whilestd::conditional_variable.wait()而不是std::conditional_variable.notify_all()循环。在我的测试中它似乎工作正常。

#include <atomic>
#include <condition_variable>
#include <mutex>


class SpinningBarrier
{
    public:
        SpinningBarrier (unsigned int threadCount) :
            threadCnt(threadCount),
            step(0),
            waitCnt(0)
        {}

        bool wait()
        {
            if(waitCnt.fetch_add(1) >= threadCnt - 1)
            {
                std::lock_guard<std::mutex> lock(mutex);
                step += 1;

                condVar.notify_all();
                waitCnt.store(0);
                return true;
            }
            else
            {
                std::unique_lock<std::mutex> lock(mutex);
                unsigned char s = step;

                condVar.wait(lock, [&]{return step == s;});
                return false;
            }
        }
    private:
        const unsigned int threadCnt;
        unsigned char step;

        std::atomic<unsigned int> waitCnt;
        std::condition_variable condVar;
        std::mutex mutex;
};

答案 5 :(得分:2)

为什么不使用std :: atomic_flag(来自C ++ 11)?

http://en.cppreference.com/w/cpp/atomic/atomic_flag

  

std :: atomic_flag是一种原子布尔类型。与所有专业不同   std :: atomic,它保证是无锁的。

以下是我编写旋转线程障碍类的方法:

#ifndef SPINLOCK_H
#define SPINLOCK_H

#include <atomic>
#include <thread>

class SpinLock
{
public:

    inline SpinLock() :
        m_lock(ATOMIC_FLAG_INIT)
    {
    }

    inline SpinLock(const SpinLock &) :
        m_lock(ATOMIC_FLAG_INIT)
    {
    }

    inline SpinLock &operator=(const SpinLock &)
    {
        return *this;
    }

    inline void lock()
    {
        while (true)
        {
            for (int32_t i = 0; i < 10000; ++i)
            {
                if (!m_lock.test_and_set(std::memory_order_acquire))
                {
                    return;
                }
            }

            std::this_thread::yield();  // A great idea that you don't see in many spinlock examples
        }
    }

    inline bool try_lock()
    {
        return !m_lock.test_and_set(std::memory_order_acquire);
    }

    inline void unlock()
    {
        m_lock.clear(std::memory_order_release);
    }

private:

    std::atomic_flag m_lock;
};

#endif

答案 6 :(得分:1)

直接从文档中被盗

spinlock.h

#include <atomic>

using namespace std;

/* Fast userspace spinlock */
class spinlock {
public:
    spinlock(std::atomic_flag& flag) : flag(flag) {
        while (flag.test_and_set(std::memory_order_acquire)) ;
    };
    ~spinlock() {
        flag.clear(std::memory_order_release);
    };
private:
    std::atomic_flag& flag; 
};

usage.cpp

#include "spinlock.h"

atomic_flag kartuliga = ATOMIC_FLAG_INIT;

void mutually_exclusive_function()
{
    spinlock lock(kartuliga);
    /* your shared-resource-using code here */
}