目录
一,线程库简介
二,线程库简单使用
2.1 传函数指针
编辑 2.2 传lamdba表达式
2.3 简单综合运用
2.4 线程函数参数
三,线程安全问题
3.1 为什么会有这个问题?
3.2 锁
3.2.1 互斥锁
3.2.2 递归锁
3.3 原子操作
3.3.1 原子操作变量
3.3.2 atomic模板
五,lock_guard和unique_lock
5.1 可能发生的情况
5.2 lock_guard
5.3 unique_lock
六,两个线程交替打印,一个打印奇数,一个打印偶数
一,线程库简介
在Linux中我们可以使用用户层的线程接口来实现线程操作,但是Linux下的接口无法在Windows下使用,因为Linux支持了POSIX线程标准,但是Windows没有支持,它搞了一套属于它自己的线程标准。所以在C++11之前,涉及到多线程问题的代码可移植性比较差
所以C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类得概念。
C++,Linux和Windows下都可以支持多线程程序 -- 条件编译
#ifdef _WIN32
CreateThread...
#else
pthread_create...
#endif
下面是C++中常用的线程函数的介绍:
①thread():构造一个线程对象,没有关联任何线程函数,即没有启动任何线程
②thread(fn, args1, args2...):构造一个线程对象,并使其关联函数fn,args是fn线程函数的参数
③get_id:获取线程id
④join():类似malloc或new之后需要手动free或者delete,join被调用后会阻塞主先,当该线程结束后,主线程继续执行
⑤detach():一般在创建线程对象后马上调用,用于把创建出来的线程与线程对象分离开,分离后的线程变为后台线程,在这之后后台线程就与主线程无关
二,线程库简单使用
线程函数一般情况下可以按照三种方式提供:①函数指针 ②lamdba表达式 ③函数对象
2.1 传函数指针
先来个最简单的线程使用,如下代码:
void Func(int n, int num)
{
for (int i = 0; i < n; i++)
{
cout <<num<<":" << i << endl;
}
cout << endl;
}
void main()
{
thread t1(Func, 10, 1);
thread t2(Func, 20, 2);
t1.join();
t2.join();
}
注:由于是多线程同时运行,所以打印结果会比较乱,属于正常情况!
2.2 传lamdba表达式
我们还可以直接用lamdba表达式传给线程,如下代码:
void main()
{
int n1, n2;
cin >> n1 >> n2;
thread t1([n1](int num)
{
for (int i = 0; i < n1; i++)
{
cout << num << ":" << i << endl;
}
cout << endl;
}, 1);
thread t2([n1](int num)
{
for (int i = 0; i < n1; i++)
{
cout << num << ":" << i << endl;
}
cout << endl;
}, 2);
t1.join();
t2.join();
}
2.3 简单综合运用
但是单独用lamdba表达式传过去的话,代码会重复,所以也可以用循环把多个线程放进数组里,如下代码:
void main()
{
int m;
cin >> m;
//要求m个线程分别打印n
vector<thread> vthds(m);//把线程作为一个对象放到容器里去
size_t n;
for (size_t i = 0; i<m; i++)
{
size_t n = 10;
vthds[i] = thread([i, n]() {
for (int j = 0; j < n; j++)
{
cout << this_thread::get_id() << ":" << j << endl; //用this_thread命名空间打印线程id
this_thread::sleep_for(chrono::seconds(1)); //每打印一次休眠一秒
}
cout << endl;
});
//cout << vthds[i].get_id() << endl;
}
for (auto& t : vthds) //库线程禁掉了拷贝赋值,所以这里用引用
{
t.join();
}
}
2.4 线程函数参数
线程函数的参数是以值拷贝的方式拷贝到线程栈空间中的,因此,即使线程参数为引用类型,在线程中修改后也不能修改外部实参,因为其实际引用的是线程栈中的拷贝,而不是外部实参
void ThreadFunc1(int& x)
{
x += 10;
}
void ThreadFunc2(int* x)
{
*x += 10;
}
int main()
{
int a = 10;
// 在线程函数中对a修改,不会影响外部实参,因为:线程函数参数虽然是引用方式,但其实际
引用的是线程栈中的拷贝
thread t1(ThreadFunc1, a);
t1.join();
cout << a << endl;
// 如果想要通过形参改变外部实参时,必须借助std::ref()函数
thread t2(ThreadFunc1, std::ref(a);
t2.join();
cout << a << endl;
// 地址的拷贝
thread t3(ThreadFunc2, &a);
t3.join();
cout << a << endl;
return 0;
}
三,线程安全问题
3.1 为什么会有这个问题?
先看下面代码:
unsigned 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, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}
从上面的运行结果可以看出,两个线程都对一个全局变量进行相加时,结果与我们预期的不同。
多线程最主要的问题就是访问临界资源带来的线程安全问题,如果所有临界资源都是只读的,那就是线程安全的,因为数据不会被修改,但是当一个或多个线程要修改临界资源时,如果没有对临界资源给予相关的保护措施,比如++,那么就是线程不安全的,因为我们说一条汇编语句是原子的,但是++等操作经过编译器编译后会编程三条汇编语句,那么++操作就不是原子的,具体的底层细节我们到linux系统编程部分再讲。
3.2 锁
关于锁的概念我们到Linux系统编程再进行讲解,这里只展示用法:
3.2.1 互斥锁
list<int> lt;
int x = 0;
mutex mtx;
void Func2(int n)
{
//并行
for (int i = 0; i < n; i++)
{
mtx.lock();
++x;
lt.push_back(x);
mtx.unlock();
cout << i << endl;
cout << i << endl;
cout << i << endl;
}
//串行 --> 如果我们只是++,没有push_back等其他操作时,可以看出并行并不比串行快,并行会有大量的加锁和解锁,而且由于计算太快了,还有切换线程的切换上下文的消耗
//但是当我们++后再push_back插到链表后,还是并行快,而且随着多临界资源的访问消耗变大时,并行的优势更大
/*mtx1.lock();
for (int i = 0; i < n; i++)
{
++x;
lt.push_back(x);
cout << i << endl;
cout << i << endl;
cout << i << endl;
}
mtx1.unlock();*/
}
void main()
{
int n = 2000000;
size_t begin = clock();
thread t1(Func2, 10000); //每个线程都有自己独自的栈,这个栈在共享区中,由库提供
thread t2(Func2, 20000);
t1.join();
t2.join();
size_t end = clock();
cout << (end - begin) << endl;
cout << x << endl;
}
上面的有点复杂,我们直接传lamdba
void main()
{
int n = 20000;
int x = 0;
mutex mtx1;
size_t begin = clock();
thread t1([&, n](){
mtx.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx1.unlock();
});
thread t2([&, n]() {
mtx1.lock();
for (int i = 0; i < n; i++)
{
++x;
}
mtx1.unlock();
});
t1.join();
t2.join();
size_t end = clock();
cout << (end - begin) << endl;
cout << x << endl;
}
3.2.2 递归锁
recursive_mutex mtx2;
void Func3(int n)
{
if (n == 0)
return;
mtx2.lock();
++x;
Func3(n - 1);
mtx2.unlock(); //当普通锁的解锁在这里定义的时候会造成死锁,递归锁解决在递归场景中的死锁问题
}
void main()
{
thread t1(Func3, 1000);
thread t2(Func3, 1000);
t1.join();
t2.join();
cout << x << endl;
}
3.3 原子操作
虽然加锁可以解决线程安全问题,但是加锁有一个缺陷就是:只要一个线程在对sum++,其他线程就必须阻塞等待,一旦线程很多的情况下就会影响总体的效率,而且如果控制不好容易造成死锁问题。
因此C++11引入了原子操作,也叫无锁操作,需要用到头文件#include<atomic>
(该操作也是用到了一个叫做CAS的同步原语,就是当我写一个数的时候,如果这个数已经改变过了,那我就不写了,如果每改变就写)
3.3.1 原子操作变量
#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;
}
3.3.2 atomic模板
atmoic<T> t; //声明一个类型为T的原子类型变量t
注意:原子类型通常属于“资源型”数据,多个线程只能访问单个原子类型的拷贝,因此在C++11中,原子类型只能从其模板参数中进行改造,不允许原子类型进行拷贝构造,移动构造和operator=等,所以为了防止意外,标准库直接将atomic模板类中的拷贝构造,移动构造,赋值运算符重载全部删除了
#include <atomic>
int main()
{
atomic<int> a1(0);
//atomic<int> a2(a1); // 编译失败
atomic<int> a2(0);
//a2 = a1; // 编译失败
return 0;
}
下面是atomic模板类的演示代码:
void Func5(int n)
{
cout << x << endl;
}
void main()
{
int n = 200;
//atomic<int> x = 0; 三种写法都一样,都是去调用构造函数
//atomic<int> x = {0};
atomic<int> x{ 0 };
thread t1([&, n]() {
for (int i = 0; i < n; i++)
{
++x;
}
});
thread t2([&, n]() {
for (int i = 0; i < n; i++)
{
++x;
}
});
Func5(x.load());
t1.join();
t2.join();
cout << x << endl;
}
五,lock_guard和unique_lock
5.1 可能发生的情况
在多线程中,如果想要保证临界资源的安全性,可以将其设置为原子类型避免死锁,也可以通过加锁保证一段代码的安全性。但是还有一种情况,那就是如果在加锁和解锁中间抛异常了,那么代码会直接跳到捕获的地方导致无法执行unlock来释放锁。因此,C++11采用RAII的方式对锁进行了封装,即lock_guard和unique_lock
5.2 lock_guard
template<class Lock>
class LockGuard
{
public:
LockGuard(Lock& lk)
:_lk(lk)
{
_lk.lock();
}
~LockGuard()
{
_lk.unlock();
}
private:
Lock& _lk; //三类成员必须在初始化列表初始化:引用,const和没有默认构造的成员变量
};
void Func4(int n)
{
for (int i = 0; i < n; i++)
{
try
{
//把锁交给对象,构造函数时加锁,析构函数解锁 --> RAII
//mtx.lock();
//LockGuard<mutex> lock(mtx); // --> 我们自己实现的
lock_guard<mutex> lock(mtx); // --> 库里的
++x;
//...抛异常
if (rand() % 3 == 0)
{
throw exception("抛异常");
}
mtx.unlock();
}
catch (const exception& e)
{
cout << e.what() << endl;
}
}
}
void main()
{
thread t1(Func4, 100);
thread t2(Func4, 100);
t1.join();
t2.join();
}
通过上述代码可以看到,lock_guard类模板通过RAII的方式,对其管理的互斥量进行了封装,在需要加锁的地方生成一个lock_guard,调用构造函数自动上锁,然后出作用域时通过析构函数自动解锁
5.3 unique_lock
lock_guard的缺陷:太单一,用户没办法对其进行控制,因此C++11还提供了unique_lock.
unique_lock与lock_guard类似,也是采用RAII进行封装,但是unique_lock对象需要传一个mutex对象作为参数,对传入的锁进行上锁和解锁操作。而且它还提供了更多的成员函数:
①上锁/解锁操作:lock,try_lock,try_lock_for,try_lock_until和unlock
②修改操作:移动赋值,交换(swap:与另一个unique_lock对象交换所管理的互斥量),释放(release:返回它所管理的互斥量的指针,释放所有权)
③获取属性:owns_lock(返回当前对象是否上了锁),operator bool(),mutex(返回当前unique_lock所管理互斥量的指针)
六,两个线程交替打印,一个打印奇数,一个打印偶数
//C++也支持条件变量(不是线程安全的,所以要配合锁使用)
//题目:两个线程交替打印,一个线程打印奇数,一个打印偶数
//condition_variable::wait 当前进程会进行阻塞,直到被唤醒,和linux一样,在阻塞之前会进行解锁,防止死锁问题,然后唤醒后重新申请锁
//文档中的 wait_for和wait_until表示等待的时间,notify_one和notify_all表示唤醒一个线程和唤醒所有线程
#include<condition_variable>
void main39()
{
condition_variable cv;
int n = 100;
int x = 1;
//如何做到t1和t2不管谁抢到锁都保证t1先运行,t2阻塞 --> t1不等待,t2有等待,这样t1获得了锁,t2就等着,t2获得了锁但是wait,释放锁,t1获得锁
//如何防止一个线程不断申请锁释放锁,不断运行?( t1打印的时候,释放锁并且通知t2,t2重新获得锁,但是t1没停下来又等待了,t1又获得了锁) --> t1, if(x%2 == 0) cv.wait(lock); t2,if (x % 2 != 0) cv.wait(lock);
//为什么要防止一个线程连续打印? --> 假设t1,先获取到锁,t2后获取到锁,t2就阻塞在锁上面 --> t1打印奇数,++x,x变成偶数 --> t1 notift唤醒,但是没有线程wait(因为t1有锁,t2没有锁所以在阻塞等待) --> t1解锁,t1时间片到了,切出去了
// --> t2获取到锁,打印,notify,但是没有线程等待,t2再出作用域,解锁 --> t2的时间片充裕会比t1先获得锁,所以如果没有条件控制,就会导致t2连续打印
thread t1([&, n]() {
while(1)
{
unique_lock<mutex> lock(mtx);
if (x >= 100)
break;
//if(x%2 == 0) //奇数
// cv.wait(lock);
cv.wait(lock, [&x]() {return x % 2 != 0; });
cout << this_thread::get_id() << ":" << x << endl;
x++;
cv.notify_one();
}
});
thread t2([&, n]() {
while(1)
{
unique_lock<mutex> lock(mtx);
if (x > 100)
break;
//if (x % 2 != 0) //偶数
// cv.wait(lock);
cv.wait(lock, [&x](){return x % 2 == 0; });
cout << this_thread::get_id() << ":" << x << endl;
++x;
cv.notify_one();
}
});
t1.join();
t2.join();
}
unsigned 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, 10000000);
thread t2(fun, 10000000);
t1.join();
t2.join();
cout << "After joining,sum = " << sum << std::endl;
return 0;
}