C++11多线程(三):atomic与atomic_flag的使用与原理分析

        C++11提供了线程的原子操作,这些在boost库中也有,在多个线程时原子操作保证了数据的安全性。C++11的原子操作主要是atomic和atomic_flag,使用时需要包含如下头文件:

#include

一.  atomic类的使用

         template<>  struct atomic<_ITYPE> , 模板类,生成一个T类型的原子对象,并提供了一些原子操作方法。

        例如用两个线程对数0加两百万次,如果不用原子操作,互斥锁等操作,那么和一定不是二百万。代码如下:

#include  
#include 
using  namespace  std;
 
int N = 0;

void  ThreadFun()
{
	for (int i = 0; i < 1000000; ++i)
	{
		++N; //线程并发导致 加操作 重叠,不是原子操作,因此肯定少于2000000
	}
}

int main()
{
	//每个线程100万次+1
	thread  t1(ThreadFun);
	thread  t2(ThreadFun);

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

	cout << N << endl;//应该是2000000
	system("pause");
	return 0;
}

 结果如下:

 使用原子操作就可以解决加错得问题,只要添加头文件,原子对象声明即可,代码修改如下,

#include  
#include //原子操作头文件
#include 
using  namespace  std;
 
atomic  N = 0;//用atomic保证对N的操作的原子性

  现在的结果:

 用互斥量,临界区等,也可以解决多线程加法错误问题,但是用锁会牺牲性能,原子操作是比较折中的方式,可以写代码测试。

 

二. atomic_flag的使用

        atomic_flag 一种简单的原子布尔类型,只支持两种操作,test-and-set 和 clear。

       std::atomic_flag 构造函数如下:

atomic_flag() noexcept = default;
atomic_flag (const atomic_flag&T) = delete;

      std::atomic_flag 只有默认构造函数,拷贝构造函数已被禁用,因此不能从其他的 std::atomic_flag 对象构造一个新的 std::atomic_flag 对象。

       如果在初始化时没有明确使用 ATOMIC_FLAG_INIT初始化,那么新创建的 std::atomic_flag 对象的状态是未指定的(unspecified)(既没有被 set 也没有被 clear。)另外,atomic_flag不能被拷贝,也不能 move 赋值。

       ATOMIC_FLAG_INIT: 如果某个 std::atomic_flag 对象使用该宏初始化,那么可以保证该 std::atomic_flag 对象在创建时处于 clear 状态。

      下面先看一个简单的例子,main() 函数中创建了 10 个线程进行计数,率先完成计数任务的线程输出自己的 ID,后续完成计数任务的线程不会输出自身 ID:

#include               
#include                 
#include                 
#include                 

std::atomic ready(false);    
std::atomic_flag winner = ATOMIC_FLAG_INIT;    // always set when checked

void count1m(int id)
{
	while (!ready) 
	{
		std::this_thread::yield();
	} // 等待主线程中设置 ready 为 true.

	for (int i = 0; i < 1000000; ++i) 
	{
		// 计数.
	} 

	// 如果某个线程率先执行完上面的计数过程,则输出自己的 ID.
	// 此后其他线程执行 test_and_set 是 if 语句判断为 false,
	// 因此不会输出自身 ID.
	if (!winner.test_and_set()) 
	{
		std::cout << "thread #" << id << " won!\n";
	}
}

int main()
{
	std::vector threads;
	std::cout << "spawning 10 threads that count to 1 million...\n";
	for (int i = 1; i <= 10; ++i)
		threads.push_back(std::thread(count1m, i));

	ready = true;

	for (auto & th : threads)
		th.join();

	system("pause");
	return 0;
}

测试结果如下:

test_and_set 介绍

       std::atomic_flag 的 test_and_set 函数原型如下:

bool test_and_set (memory_order sync = memory_order_seq_cst) volatile noexcept;
bool test_and_set (memory_order sync = memory_order_seq_cst) noexcept;

     test_and_set() 函数检查 std::atomic_flag 标志,如果 std::atomic_flag 之前没有被设置过,则设置 std::atomic_flag 的标志,并返回先前该 std::atomic_flag 对象是否被设置过,如果之前 std::atomic_flag 对象已被设置,则返回 true,否则返回 false。

test-and-set 操作是原子的(因此 test-and-set 是原子 read-modify-write (RMW)操作)。

test_and_set 可以指定 Memory Order(后续的文章会详细介绍 C++11 的 Memory Order,此处为了完整性列出 test_and_set 参数 sync 的取值),取值如下:

 

Memory Order 值 Memory Order 类型
memory_order_relaxed Relaxed
memory_order_consume Consume
memory_order_acquire Acquire
memory_order_release Release
memory_order_acq_rel Acquire/Release
memory_order_seq_cst Sequentially consistent

 一个简单的例子:

#include                 
#include                
#include                
#include                
#include                 

std::atomic_flag lock_stream = ATOMIC_FLAG_INIT;
std::stringstream stream;

void append_number(int x)
{
	while (lock_stream.test_and_set()) 
	{
	}

	stream << "thread #" << x << '\n';
	lock_stream.clear();
}

int main()
{
	std::vector  threads;
	for (int i = 1; i <= 10; ++i)
		threads.push_back(std::thread(append_number, i));

	for (auto & th : threads)
		th.join();

	std::cout << stream.str() << std::endl;

	system("pause");
	return 0;
}

结果如下

C++11多线程(三):atomic与atomic_flag的使用与原理分析_第1张图片

atomic_flag::clear() 介绍

           清除 std::atomic_flag 对象的标志位,即设置 atomic_flag 的值为 false。clear 函数原型如下:

void clear (memory_order sync = memory_order_seq_cst) volatile noexcept;
void clear (memory_order sync = memory_order_seq_cst) noexcept;

          清除 std::atomic_flag 标志使得下一次调用 std::atomic_flag::test_and_set 返回 false。

         std::atomic_flag::clear() 可以指定 Memory Order(后续的文章会详细介绍 C++11 的 Memory Order,此处为了完整性列出 clear 参数 sync 的取值),取值如下:

Memory Order 值 Memory Order 类型
memory_order_relaxed Relaxed
memory_order_consume Consume
memory_order_acquire Acquire
memory_order_release Release
memory_order_acq_rel Acquire/Release
memory_order_seq_cst Sequentially consistent

        结合 std::atomic_flag::test_and_set() 和 std::atomic_flag::clear(),std::atomic_flag 对象可以当作一个简单的自旋锁使用,请看下例:

#include 
#include 
#include 
#include 

std::atomic_flag lock = ATOMIC_FLAG_INIT;

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             ; // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

int main()
{
    std::vector v;
    for (int n = 0; n < 10; ++n) {
        v.emplace_back(f, n);
    }
    for (auto& t : v) {
        t.join();
    }
}

        在上面的程序中,std::atomic_flag 对象 lock 的上锁操作可以理解为 lock.test_and_set(std::memory_order_acquire); (此处指定了 Memory Order,更多有关 Memory Order 的概念,我会在后续的文章中介绍),解锁操作相当与 lock.clear(std::memory_order_release)。

        在上锁的时候,如果 lock.test_and_set 返回 false,则表示上锁成功(此时 while 不会进入自旋状态),因为此前 lock 的标志位为 false(即没有线程对 lock 进行上锁操作),但调用 test_and_set 后 lock 的标志位为 true,说明某一线程已经成功获得了 lock 锁。

        如果在该线程解锁(即调用 lock.clear(std::memory_order_release)) 之前,另外一个线程也调用 lock.test_and_set(std::memory_order_acquire) 试图获得锁,则 test_and_set(std::memory_order_acquire) 返回 true,则 while 进入自旋状态。如果获得锁的线程解锁(即调用了 lock.clear(std::memory_order_release))之后,某个线程试图调用 lock.test_and_set(std::memory_order_acquire) 并且返回 false,则 while 不会进入自旋,此时表明该线程成功地获得了锁。

        按照上面的分析,我们知道在某种情况下 std::atomic_flag 对象可以当作一个简单的自旋锁使用。

你可能感兴趣的:(C++多线程与线程池)