分离关注点
通过将相关的代码与无关的代码分离,可以使程序更容易理解和测试,从而减少出错的可能性
更好的性能
将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(task parallelism)。
为性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能使代码复杂化,使其更难理解,并更容易出错。
当程序中有共享数据,肯定不想让其陷入条件竞争,或是不变量被破坏。那么,将所有访问共享数据结构的代码都标记为互斥岂不是更好?这样任何一个线程在执行这些代码时,其他任何线程试图访问共享数据结构,就必须等到那一段代码执行结束。于是,一个线程就不可能会看到被破坏的不变量,除非它本身就是修改共享数据的线程。
当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。线程库需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据,而不破坏不变量。
#include
#include
#include
#include
using namespace std;
void print(const char* s)
{
while (*s != '\0')
{
cout << *s;
s++;
this_thread::sleep_for(chrono::seconds(1));
}
}
void fun1()
{
const char* s1 = "hello";
print(s1);
}
void fun2()
{
const char* s2 = "world";
print(s2);
}
int main()
{
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
return 0;
}
C++中通过实例化std::mutex创建互斥量,通过调用成员函数lock()进行上锁,unlock()进行解锁。不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况。
C++标准库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。
#include
#include
#include
#include
using namespace std;
mutex mlock;//互斥量
void print(const char* s)
{
mlock.lock();
while (*s != '\0')
{
cout << *s;
s++;
this_thread::sleep_for(chrono::seconds(1));
}
mlock.unlock();
}
void fun1()
{
const char* s1 = "hello";
print(s1);
}
void fun2()
{
const char* s2 = "world";
print(s2);
}
int main()
{
thread t1(fun1);
thread t2(fun2);
t1.join();
t2.join();
return 0;
}
注意:对一个unlock的mutex进行unlock的行为导致的结果是未定义的:
If the mutex is not currently locked by the calling thread, it causes undefined behavior.
在同一个线程中lock两次会造成死锁:
If the mutex is currently locked by the same thread calling this function, it produces a deadlock (with undefined behavior)
每次使用lock
都要记得unlock
,太麻烦,直接使用lock_guard
:
构造时,mutex object
被lock
,析构时,mutex object
被unlock
。
尽管lock_guard
不管理mutex object
的生存周期,但是mutex object
的生存期应该至少到lock_guard
析构,因为lock_guard
在析构时需要进行unlock
,如果mutex object
已经不存在了,那怎么unlock
呢?
mutex mlock;//互斥量
void print(const char* s)
{
lock_guard<mutex> lg(mlock);
while (*s != '\0')
{
cout << *s;
s++;
this_thread::sleep_for(chrono::seconds(1));
}
}
互斥变量使用pthread_mutex_t
数据类型来表示,在使用互斥变量之前应该对其初始化,可以把它设置为常量PTHREAD_MUTEX_INITALIZER
(只对静态分配的互斥量),也可以调用函数pthread_mutex_init()
,如果动态地分配互斥量(调用malloc
),那么释放内存前需要调用pthread_mutex_destory()
。
#include
int pthread_mutex_init(pthread_mutex_t* restrict mutex, const pthread_mutexattr_t* restrict attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
//成功返回0,失败返回错误代码
调用pthread_mutex_lock
对互斥量进行加锁,如果互斥量已经上锁,那么线程会阻塞。
#include
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
//成功返回0,失败返回错误代码
由于调用pthread_mutex_lock
可能会阻塞,所以如果不希望线程阻塞的话,可以调用pthread_mutex_trylock
,如果互斥量被锁住,该函数会返回EBUSY
,然后继续执行下面的语句,如果互斥量没锁住,那么该函数会给互斥量加锁。
参考博客:c++线程中的几种锁
假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。
首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。
而自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。
从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。
当发生阻塞时,互斥锁可以让CPU去处理其他的任务;而自旋锁让CPU一直不断循环请求获取这个锁。通过两个含义的对比可以我们知道“自旋锁”是比较耗费CPU的。
自旋锁的类型:spinlock_t
相关函数:
#include
spin_lock_init(spinlock_t *x);//初始化
spin_lock(x);//只有在获得锁的情况下才返回,否则一直“自旋”
spin_trylock(x);//如立即获得锁则返回真,否则立即返回假
spin_unlock(x);释放锁
spin_is_locked(x)//该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是, 返回真,否则返回假。
注意:自旋锁适合于短时间的的轻量级的加锁机制。