在应届生面试的时候,很多面试官都会问——“多线程如何共享资源”。在操作系统层面上可以给出若干关键词答案,但是在语言层面,这个问题考虑的就没有那么简单了。同时,很多人会将多线程数据共享和线程同步混淆。有关线程同步,我们会在接下来的章节里着重阐述。本文主要聚焦于保护共享数据,首先从加锁入手,进而扩展到加锁无法解决的问题,最后会给出一些其他保护方案。
参数入栈
一个存放参数的栈数据结构,相同函数的参数必须要在栈中相连,我们来实现这个功能,看下面代码:
#include <stack>
#include <iostream>
class MutexTest
{
public:
MutexTest() : m_charStack() { }
~MutexTest() { }
void Push(int n, char c)
{
for (int i = 0; i < n; ++i)
{
m_charStack.push(c);
std::cout << c;
}
std::cout << std::endl;
}
private:
std::stack<char> m_charStack;
};
int main()
{
MutexTest test;
std::thread mutexTestThread1(&MutexTest::Push, &test, 10, 'a');
std::thread mutexTestThread2(&MutexTest::Push, &test, 10, 'b');
mutexTestThread1.join();
mutexTestThread2.join();
std::cout << std::this_thread::get_id() << std::endl;
return 0;
}
------------------------------------------------
aaaababababababab
bbb
5420
请按任意键继续. . .
结果不确定!!!!!!!!!!!!!!!!
上面这段代码的执行结果是不确定的,这是因为我们无法预测线程的执行顺序,多个线程共享同一个数据栈存在竞态条件(Race Condition)。所以我们可能得到下面的执行结果,所有的参数都是交叉在一起的,这不是我们想要的结果。
aabbbbbbbaaaaaaaabbb
竞态条件
是多线程编程的噩梦,为什么会出现竞态条件可以自行百度,我们主要是为了解决这个问题。让最终执行的结果为:
aaaaaaaaaa
bbbbbbbbbb
参数入栈保护
std::mutex
是C++11提供的数据加锁类,C++中通过实例化 std::mutex 创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。
#include <iostream>
#include <mutex>
#include <thread>
#include <stack>
using namespace std;
class MutexTest
{
public:
MutexTest() : m_mutex(), m_charStack() { }
~MutexTest() { }
void Push(int n, char c)
{
m_mutex.lock();
for (int i = 0; i < n; ++i)
{
m_charStack.push(c);
std::cout << c;
}
std::cout << std::endl;
m_mutex.unlock();
}
private:
std::mutex m_mutex;
std::stack<char> m_charStack;
};
int main()
{
MutexTest test;
std::thread mutexTestThread1(&MutexTest::Push, &test, 10, 'a');
std::thread mutexTestThread2(&MutexTest::Push, &test, 10, 'b');
mutexTestThread1.join();
mutexTestThread2.join();
std::cout << std::this_thread::get_id() << std::endl;
return 0;
}
-------------------------------------------
aaaaaaaaaa
bbbbbbbbbb
6472
请按任意键继续. . .
这段代码和上面的不同点就是使用std::mutex,在访问m_charStack之前上锁,其他线程就必须要等待解锁后才能访问m_charStack。如果我们忘记解锁,那么m_charStack就再也无法被访问了,所以有必要用RAII类std::lock_guard
进行封装——构造时上锁,析构时解锁。
void MutexTest::Push(int n, char c)
{
std::lock_guard<std::mutex> lg(m_mutex);
for (int i = 0; i < n; ++i)
{
m_charStack.push(c);
std::cout << c;
}
std::cout << std::endl;
}
C++还提供了std::unique_lock
锁,相对于std::lock_guard
,该锁提供了更好地上锁和解锁灵活性控制。std::unique_lock
以独占所有权的方式来管理mutex对象的上锁和解锁操作。我们来看看其用法:
#include <iostream>
#include <mutex>
#include <thread>
#include <stack>
using namespace std;
// unique_lock constructor example
std::mutex foo, bar;
void task_a() {
std::lock(foo, bar); // simultaneous lock (prevents deadlock)
std::unique_lock<std::mutex> lck1(foo, std::adopt_lock);
std::unique_lock<std::mutex> lck2(bar, std::adopt_lock);
std::cout << "task a\n";
// (unlocked automatically on destruction of lck1 and lck2)
}
void task_b() {
// foo.lock(); bar.lock(); // replaced by:
std::unique_lock<std::mutex> lck1, lck2;
lck1 = std::unique_lock<std::mutex>(bar, std::defer_lock);
lck2 = std::unique_lock<std::mutex>(foo, std::defer_lock);
std::lock(lck1, lck2); // simultaneous lock (prevents deadlock)
std::cout << "task b\n";
// (unlocked automatically on destruction of lck1 and lck2)
}
int main()
{
std::thread th1(task_a);
std::thread th2(task_b);
th1.join();
th2.join();
return 0;
}
----------------------------------------------------
task a
task b
请按任意键继续. . .
现在我们终于得到了我们想要的结果,可惜在很多时候加锁并不是解决数据共享的万能药。下一节,我们将会涉及到一些加锁无法解决的数据共享问题。
std::mutex
定义于头文件 | ||
class mutex; | (C++11 起) | |
mutex
类是能用于保护共享数据免受从多个线程同时访问的同步原语。
mutex
提供排他性非递归所有权语义:
- 调用方线程从它成功调用
lock
或try_lock
开始,到它调用unlock
为止占有mutex
。 - 线程占有
mutex
时,所有其他线程若试图要求mutex
的所有权,则将阻塞(对于lock
的调用)或收到 false 返回值(对于try_lock
). - 调用方线程在调用
lock
或try_lock
前必须不占有mutex
。
若 mutex
在仍为任何线程所占有时即被销毁,或在占有 mutex
时线程终止,则行为未定义。 mutex
类满足互斥 (Mutex) 和标准布局类型 (StandardLayoutType) 的全部要求。
std::mutex
既不可复制亦不可移动。
成员类型
成员类型 | 定义 |
native_handle_type (可选) | 实现定义 |
成员函数
构造互斥 (公开成员函数) | |
销毁互斥 (公开成员函数) | |
operator= [被删除] | 不可复制赋值 (公开成员函数) |
锁定 | |
锁定互斥,若互斥不可用则阻塞 (公开成员函数) | |
尝试锁定互斥,若互斥不可用则返回 (公开成员函数) | |
解锁互斥 (公开成员函数) | |
原生句柄 | |
返回底层实现定义的原生句柄 (公开成员函数) |
注意
通常不直接使用 std::mutex
: std::unique_lock 、 std::lock_guard 或 std::scoped_lock (C++17 起)以更加异常安全的方式管理锁定。
示例
此示例展示 mutex
能如何用于在保护共享于二个线程间的 std::map 。
#include <iostream>
#include <map>
#include <string>
#include <chrono>
#include <thread>
#include <mutex>
std::map<std::string, std::string> g_pages;
std::mutex g_pages_mutex;
void save_page(const std::string &url)
{
// 模拟长页面读取
std::this_thread::sleep_for(std::chrono::seconds(2));
std::string result = "fake content";
std::lock_guard<std::mutex> guard(g_pages_mutex);
g_pages[url] = result;
}
int main()
{
std::thread t1(save_page, "http://foo");
std::thread t2(save_page, "http://bar");
t1.join();
t2.join();
// 现在访问g_pages是安全的,因为线程t1/t2生命周期已结束
for (const auto &pair : g_pages) {
std::cout << pair.first << " => " << pair.second << '\n';
}
}
------------------------------------------------------
http://bar => fake content
http://foo => fake content
请按任意键继续. . .
引用来源: