进行多线程编程,如果多个线程需要对同一块内存进行操作,比如:同时读、同时写、同时读写对于后两种情况来说,如果不做任何的人为干涉就会出现各种各样的错误数据。这是因为线程在运行的时候需要先得到CPU时间片,时间片用完之后需要放弃已获得的CPU资源,就这样线程频繁地在就绪态和运行态之间切换,更复杂一点还可以在就绪态、运行态、挂起态之间切换,这样就会导致线程的执行顺序并不是有序的,而是随机的混乱的,就如同下图中的这个例子一样,理想很丰满现实却很残酷。
解决多线程数据混乱的方案就是进行线程同步,最常用的就是互斥锁,在C++11中一共提供了四种互斥锁:
互斥锁在有些资料中也被称之为互斥量,二者是一个东西。
不论是在C还是C++中,进行线程同步的处理流程基本上是一致的,C++的mutex类提供了相关的API函数:
lock()函数**用于给临界区加锁,并且只能有一个线程获得锁的所有权font>,它有阻塞线程的作用**,函数原型如下:
void lock();
独占互斥锁对象有两种状态:锁定和未锁定。如果互斥锁是打开的,调用lock()函数的线程会得到互斥锁的所有权,并将其上锁,其它线程再调用该函数的时候由于得不到互斥锁的所有权,就会被lock()函数阻塞。当拥有互斥锁所有权的线程将互斥锁解锁,此时被lock()阻塞的线程解除阻塞,抢到互斥锁所有权的线程加锁并继续运行,没抢到互斥锁所有权的线程继续阻塞。
除了使用lock()还可以使用try_lock()获取互斥锁的所有权并对互斥锁加锁,函数原型如下:
bool try_lock();
二者的区别在于try_lock()不会阻塞线程,lock()会阻塞线程:
try_lock
尝试锁定互斥锁,如果互斥锁已经被其他线程锁定,则返回 false
,否则返回 true
。这可以用于避免线程阻塞,可以在锁定前进行检查。
#include <mutex>
std::mutex myMutex;
void myFunction() {
if (myMutex.try_lock()) {
// 访问共享资源的代码
myMutex.unlock();
} else {
// 互斥锁被其他线程锁定,执行备选方案
}
}
当互斥锁被锁定之后可以通过unlock()进行解锁,但是需要注意的是只有拥有互斥锁所有权的线程也就是对互斥锁上锁的线程才能将其解锁,其它线程是没有权限做这件事情的。该函数的函数原型如下:
void unlock();
通过介绍以上三个函数,使用互斥锁进行线程同步的大致思路差不多就能搞清楚了,主要分为以下几步:
线程同步的目的是让多线程按照顺序依次执行临界区代码,这样做线程对共享资源的访问就从并行访问变为了线性访问,访问效率降低了,但是保证了数据的正确性。
当线程对互斥锁对象加锁,并且执行完临界区代码之后,一定要使用这个线程对互斥锁解锁,否则最终会造成线程的死锁。死锁之后当前应用程序中的所有线程都会被阻塞,并且阻塞无法解除,应用程序也无法继续运行。
举个栗子,我们让两个线程共同操作同一个全局变量,二者交替数数,将数值存储到这个全局变量里边并打印出来。
#include <iostream>
using namespace std;
#include <mutex>
#include <thread>
#include <chrono>
// 创建全局锁对象
mutex mt1;
int num = 0;
void test01() {
while (true) {
mt1.lock();
if (num == 100) {
mt1.unlock(); // 没有这行代码 程序是无法结束的
return;
}
cout << this_thread::get_id() << ": num = " << ++num << endl;
mt1.unlock();
this_thread::sleep_for(chrono::milliseconds(10));
}
}
int main() {
thread t1(test01);
thread t2(test01);
t1.join();
t2.join();
return 0;
}
在上面的示例程序中,两个子线程执行的任务的一样的(其实也可以不一样,不同的任务中也可以对共享资源进行读写操作),在任务函数中把与全局变量相关的代码加了锁,两个线程只能顺序访问这部分代码(如果不进行线程同步打印出的数据是混乱且无序的)。另外需要强调一点:
std::lock_guard
是 C++ 标准库提供的一个 RAII(Resource Acquisition Is Initialization)风格的锁管理类。它用于在特定范围内自动锁定和解锁互斥锁,确保在离开范围时释放锁。
就是说我们不用手动解锁了, lock_guard会帮助我们自己解锁
#include <iostream>
#include <mutex>
#include <thread>
std::mutex myMutex;
int sharedData = 0;
void myFunction() {
std::lock_guard<std::mutex> lock(myMutex);
// 在这个作用域内,myMutex 已经被锁定
sharedData++; // 对共享数据的互斥访问
} // 在这个作用域结束时,lock_guard 析构,myMutex 自动解锁
上面的示例就可以这样写:
#include <iostream>
using namespace std;
#include <mutex>
#include <thread>
#include <chrono>
// 创建全局锁对象
mutex mt1;
int num = 0;
void test01() {
while (true) {
lock_guard<mutex> lock(mt1);
if (num == 100) {
return;
}
cout << this_thread::get_id() << ": num = " << ++num << endl;
this_thread::sleep_for(chrono::milliseconds(10));
}
}
int main() {
thread t1(test01);
thread t2(test01);
t1.join();
t2.join();
return 0;
}
? 通过修改发现代码被精简了,而且不用担心因为忘记解锁而造成程序的死锁,但是**这种方式也有弊端,在上面的示例程序中整个循环的体都被当做了临界区,多个线程是线性的执行临界区代码的,因此临界区越大程序效率越低,**还是需要根据实际情况选择最优的解决方案。
? std::recursive_mutex
是 C++ 标准库中提供的一种互斥量(mutex)的实现,它允许同一个线程多次对互斥量进行加锁。这意味着,如果一个线程已经拥有了 std::recursive_mutex
的锁,那么它可以再次加锁,而不会发生死锁。
? 在标准库中,互斥量是用于保护共享资源,防止多个线程同时访问这些资源,从而避免数据竞争。std::recursive_mutex
在互斥量的基础上加入了递归特性,使得同一个线程可以多次加锁,而不会引发死锁。
? 以下是 std::recursive_mutex
的主要特点和用法:
std::recursive_mutex
,而不会产生死锁。lock()
和 unlock()
方法进行锁定和解锁。std::recursive_mutex
进行多次加锁和解锁。std::recursive_mutex
可能有一些性能开销,因为需要维护额外的信息来跟踪递归调用。? 以下是一个简单的示例,演示了 std::recursive_mutex
的基本用法:
#include <iostream>
#include <mutex>
#include <thread>
std::recursive_mutex myMutex;
void foo(int threadID, int depth) {
std::unique_lock<std::recursive_mutex> lock(myMutex);
// 执行一些操作
std::cout << "Thread " << threadID << " locked mutex at depth " << depth << std::endl;
if (depth > 0) {
foo(threadID, depth - 1); // 递归调用
}
// 执行一些其他操作
std::cout << "Thread " << threadID << " unlocked mutex at depth " << depth << std::endl;
}
int main() {
std::thread t1(foo, 1, 3);
std::thread t2(foo, 2, 2);
t1.join();
t2.join();
return 0;
}
? 虽然递归互斥锁可以解决同一个互斥锁频繁获取互斥锁资源的问题,但是还是建议少用,主要原因如下:
std::timed_mutex
是 C++ 标准库中提供的一种带有超时功能的互斥量(mutex)实现。它允许线程在尝试获取锁的过程中设置超时时间,如果在规定的时间内无法获得锁,线程可以选择放弃或者执行其他逻辑。这个超时机制对于避免死锁和处理锁等待时间较长的情况很有用。
以下是 std::timed_mutex
的主要特点和使用方法:
锁定和解锁: 通过 lock()
和 unlock()
方法进行锁定和解锁,与常规互斥量类似。
超时机制:
通过 try_lock_for() 和 try_lock_until()方法,线程可以尝试在一段时间内获取锁,如果超过指定的时间仍未获取到锁,就会返回失败。
try_lock_for(duration)
:在指定的时间段内尝试获取锁。try_lock_until(time_point)
:在指定的时间点之前尝试获取锁。等待超时: 如果超时机制启用,线程可以通过返回值来判断是否成功获取锁。如果返回 true
,表示成功获取锁;如果返回 false
,表示在规定时间内未能获取锁。
可与条件变量一起使用: std::timed_mutex
可以与 std::condition_variable
一起使用,实现更为复杂的同步机制。
以下是一个简单的示例,演示了 std::timed_mutex
的基本用法:
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
timed_mutex g_mutex;
void work()
{
chrono::seconds timeout(1);
while (true)
{
// 通过阻塞一定的时长来争取得到互斥锁所有权
if (g_mutex.try_lock_for(timeout))
{
cout << "当前线程ID: " << this_thread::get_id()
<< ", 得到互斥锁所有权..." << endl;
// 模拟处理任务用了一定的时长
this_thread::sleep_for(chrono::seconds(10));
// 互斥锁解锁
g_mutex.unlock();
break;
}
else
{
cout << "当前线程ID: " << this_thread::get_id()
<< ", 没有得到互斥锁所有权..." << endl;
// 模拟处理其他任务用了一定的时长
this_thread::sleep_for(chrono::milliseconds(50));
}
}
}
int main()
{
thread t1(work);
thread t2(work);
t1.join();
t2.join();
return 0;
}
程序运行结果为:
当前线程ID: 18912, 得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 没有得到互斥锁所有权...
当前线程ID: 19828, 得到互斥锁所有权...
? 在上面的例子中,通过一个while循环不停的去获取超时互斥锁的所有权,如果得不到就阻塞1秒钟,1秒之后如果还是得不到阻塞50毫秒,然后再次继续尝试,直到获得互斥锁的所有权,跳出循环体。
? 关于递归超时互斥锁std::recursive_timed_mutex的使用方式和std::timed_mutex是一样的,只不过它可以允许一个线程多次获得互斥锁所有权,而std::timed_mutex只允许线程获取一次互斥锁所有权。另外,递归超时互斥锁std::recursive_timed_mutex也拥有和std::recursive_mutex一样的弊端,不建议频繁使用。
std::unique_lock
是一个灵活的锁管理类,与 std::lock_guard
类似,但提供了更多的功能,如可延迟锁定、手动解锁等。它可以与各种互斥锁一起使用,包括 std::mutex
、std::timed_mutex
、std::recursive_mutex
等。
#include <mutex>
std::mutex myMutex;
std::unique_lock<std::mutex> myUniqueLock(myMutex);
std::shared_mutex
是一种读写锁,允许多个线程同时获取读锁,但只有一个线程能够获取写锁。这对于共享和独占访问共享资源非常有用。
#include <shared_mutex>
std::shared_mutex mySharedMutex;