欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C++ 11 线程库笔记 mutex锁 原子操作

程序员文章站 2024-03-14 11:54:04
...

1、线程函数

 C++11的多线程必须包含< thread >头文件,同时,而且在原子操作中还引入了原子类的概念。
 一些相关函数
thread() 构造一个线程对象,没有关联任何线程函数,即没有启动任何线程。
thread[] 构造一个线程数组,没有关联任何线程函数,即没有启动任何线程。
thread(fn,args,…) 构造一个线程对象,并关联线程函数fn,args1,args2,…为线程函数的参数。
get_id() 获取线程id。
jionable() 线程是否还在执行,joinable代表的是一个正在执行中的线程。
jion() 该函数调用后会阻塞住线程,当该线程结束后,主线程继续执行。
detach() 在创建线程对象后马上调用,用于把被创建线程与线程对象分离开,分离的线程变为后台线程,创建的线程的"死活"就与主线程无关。
注意:
1、 线程是操作系统中的一个概念,线程对象可以关联一个线程,用来控制线程以及获取线程的状态。
2. 当创建一个线程对象后,没有提供线程函数,该对象实际没有对应任何线程。

int main()
{
	std::thread t1;
	cout << t1.get_id() << endl;
	return 0;
}

该线程id为0。
而在关联了一个线程后,可以输出该线程id。

void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
int main()
{
	std::thread t1(ThreadFunc,10);
	cout << t1.get_id() << endl;
	t1.join();
	return 0;
}

对线程数组初始化

#include <iostream>       // std::cout
#include <thread>         // std::thread, std::this_thread::sleep_for
#include <chrono>         // std::chrono::seconds
 
void pause_thread(int n) 
{
  std::this_thread::sleep_for (std::chrono::seconds(n));
  std::cout << "pause of " << n << " seconds ended\n";
}

int main() 
{
  std::thread threads[5];                         // default-constructed threads

  std::cout << "Spawning 5 threads...\n";
  for (int i=0; i<5; ++i)
    threads[i] = std::thread(pause_thread,i+1);   // move-assign threads

  std::cout << "Done spawning threads. Now waiting for them to join:\n";
  for (int i=0; i<5; ++i)
    threads[i].join();

  std::cout << "All threads joined!\n";

  return 0;
}

1.1 线程传参

 要想改变参数,要么以引用传参,要么以指针传参。引用传参必须是使用ref(a)传过去,这是因为线程也会存在一个线程栈,传参过去其实中间会产生一个临时拷贝tmp,随后再用tmp初始化参数列表。而如果直接使用thread t1(ThreadFunc1, a)传过去,随后使用void ThreadFunc1(int& x)接收,程序会报错,因为线程的执行是通过临时对象传递的,而临时对象是能引用的。而ref(a)是把引用变成指针存起来,用的时候再转成引用。

void ThreadFunc1(int& x)
{
	x += 10;
}
void ThreadFunc2(int* x)
{
	*x += 10;
}
void ThreadFunc(int a)
{
	cout << "Thread1" << a << endl;
}
int main()
{
	int a = 10;
	// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际引用的
	//是线程栈中的拷贝
	//thread t1(ThreadFunc1, a);不能直接传入a,程序会报错,两种方法,1、ref(a)  2、右值引用void ThreadFunc1(int&& x)
	//t1.join();
	//cout << a << endl;
	 //如果想要通过形参改变外部实参时,必须借助std::ref()函数
	thread t2(ThreadFunc1, ref(a));
	t2.join();
	cout << a << endl;
	// 地址的拷贝
	thread t3(ThreadFunc2, &a);
	t3.join();
	cout << a << endl;
	return 0;

 而如果我们使用右值引用,本来想的是因为是移动构造,在线程函数中改变的值也会改变主线程的参数,但实验结果是仍然不变。

void ThreadFunc(int&& a)
{
	a += 10;
}
int main()
{
	int a = 10;
	std::thread t1(ThreadFunc,10);
	cout << a << endl;  //10
	t1.join();
	return 0;
}

结果输出仍然是10,其原因是传入线程的不是a本身的地址,而是临时拷贝。

1.2 :如果是类成员函数作为线程参数时,必须将this作为线程函数参数。


class Test
{
public:
	void thread()
	{
		cout << "This is Test::show()" << endl;
	}
};
//注意:如果是类成员函数作为线程参数时,必须将this作为线程函数参数。
void main()
{
	Test t;

	thread th(&Test::thread, &t);   //this->show
	th.join();
	//th.join();
}

2、mutex锁

 如果我们需要保护一段代码的安全,可以对这段代码进行加锁。当然效率也会降低。

#include <thread>
#include <mutex>
int number = 0;
mutex mt;
int ThreadProc1()
{
	for (int i = 0; i < 100; i++)
	{
		mt.lock();
		++number;
		cout << "thread 1 :" << number << endl;
		mt.unlock();
	}
	return 0;
}
int ThreadProc2()
{
	for (int i = 0; i < 100; i++)
	{
		mt.lock();
		--number;
		cout << "thread 2 :" << number << endl;
		mt.unlock();
	}
	return 0;
}
int main()
{
	thread t1(ThreadProc1);
	thread t2(ThreadProc2);
	t1.join();
	t2.join();
	cout << "number:" << number << endl;
	system("pause");
	return 0;
}

3、原子操作

 由于加锁会极大的增加运用的开销,所以c++11增加了原子操作,可以理解为直接定义一个加锁的临界资源, 而在c++中原子操作就是这样的一种『小到不可分割的』操作。要使用原子操作我们需要引用c++11的一个新的头文件<atomic>.。
 增加了一个头文件,并且将 long sum = 0L; 修改成了 std::atomic_long sum {0L}; 注意不要写成std::atomic_long sum = 0L的形式,因为long类型是不可以隐式转换为std::atomic_long类型的。

#include <iostream>
using namespace std;
#include <thread>
#include <atomic>
atomic_long sum{ 0 };
void fun(size_t num)
{
	for (size_t i = 0; i < num; ++i)
		sum++; // 原子操作
}
int main()
{
	cout << "Before joining, sum = " << sum << std::endl;
	thread t1(fun, 1000000);
	thread t2(fun, 1000000);
	t1.join();
	t2.join();
	cout << "After joining, sum = " << sum << std::endl;
	return 0;
}

atomic模板中还实现了操作符的重载(atomic头文件),因此你可以像使用内置的数据类型那样使用原子数据类型(c++保证这些操作是原子操作)。对应于内置的数据类型,原子数据类型都有一份对应的类型,归纳出来如下:
 C++ 11 线程库笔记 mutex锁 原子操作