C++ 11 线程库笔记 mutex锁 原子操作
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++保证这些操作是原子操作)。对应于内置的数据类型,原子数据类型都有一份对应的类型,归纳出来如下: