[多任务的同步与互斥](https://neo00.top/archives/linux%E7%B3%BB%E7%BB%9F%E7%BC%96%E7%A8%8B%E5%A4%9A%E4%BB%BB%E5%8A%A1%E7%9A%84%E5%90%8C%E6%AD%A5%E4%B8%8E%E4%BA%92%E6%96%A5),一个基于 `Linux` 下并发编程关于锁的一些介绍。相较于 C++ 而言就是换汤不换药,主要是一些语法上的区别,集体的作用什么的看那篇博客吧,本文简单介绍一下。
## 为什么需要互斥量
在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。
```cpp
#include <iostream>
#include <thread>
#include <string>
using namespace std;
void printer(string s) {
for (int i = 0; i < s.size(); i++) {
cout << s[i];
this_thread::sleep_for(chrono::seconds(1));
}
cout << endl;
}
void pthread1() {
string s1 = "hello";
printer(s1);
}
void pthread2() {
string s2 = "world!";
printer(s2);
}
int main() {
thread t1(pthread1);
thread t2(pthread2);
t1.join();
t2.join();
return 0;
}
```
上述代码输出为,显然不是我们想要的一个打印完另一个在打印。
```
hwoerllldo!
结果可能不一样,但都不是我们想要的
```
## 独占互斥量std::mutex
互斥量的基本接口很相似,一般用法是通过 `lock()` 方法来阻塞线程,直到获得互斥量的所有权为止。在线程获得互斥量并完成任务之后,就必须使用 `unlock()` 来解除对互斥量的占用,`lock()` 和 `unlock()` 必须成对出现。`try_lock()` 尝试锁定互斥量,如果成功则返回 `true`, 如果失败则返回 `false`,它是非阻塞的。
```cpp
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
mutex g_lock;
void printer(string s) {
g_lock.lock();//上锁。锁只有一把,当一个线程拿到这把锁后,在执行解锁之前,其他想获取这把锁的线程会被阻塞
for (int i = 0; i < s.size(); i++) {
cout << s[i];
this_thread::sleep_for(chrono::seconds(1));
}
cout << endl;
g_lock.unlock();//解锁
}
void pthread1() {
string s1 = "hello";
printer(s1);
}
void pthread2() {
string s2 = "world!";
printer(s2);
}
int main() {
thread t1(pthread1);
thread t2(pthread2);
t1.join();
t2.join();
return 0;
}
```
上述代码输出如下。
```
hello
world!
或者,毕竟线程之间是竞争的
world!
hello
```
使用 `std::lock_guard` 可以简化 `lock/unlock` 的写法,同时也更安全,因为 `lock_guard` 在构造时会自动锁定互斥量,而在退出作用域后进行析构时就会自动解锁,从而避免忘了 `unlock` 操作。
```cpp
#include <iostream>
#include <thread>
#include <string>
#include <mutex>
using namespace std;
mutex g_lock;
void printer(string s) {
lock_guard<mutex> locker(g_lock); //修改了这一行
for (int i = 0; i < s.size(); i++) {
cout << s[i];
//this_thread::sleep_for(chrono::seconds(1));
}
cout << endl;
//自动解锁
}
void pthread1() {
string s1 = "hello";
printer(s1);
}
void pthread2() {
string s2 = "world!";
printer(s2);
}
int main() {
thread t1(pthread1);
thread t2(pthread2);
t1.join();
t2.join();
return 0;
}
```
## 原子操作
所谓的原子操作,取的就是**“原子是最小的、不可分割的最小个体”**的意义,它表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源,不会因为被其他线程抢占 CPU 资源导致被挂起。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
```cpp
long long total = 0;
void func() {
for (int i = 0; i < 1000000; i++) {
total = total + 1;
}
}
void test2() {
clock_t start = clock();
thread t1(func);
thread t2(func);
t1.join();
t2.join();
clock_t end = clock();
cout << "total = " << total << endl;
cout << "time = " << end - start << " ms\n";
}
```
由于线程间对数据的竞争而导致每次运行的结果都不一样。因为,当 `total` 在进行运算的过程中, 会把 `total + 1` 的值先保存在寄存器中,然后执行赋值,但是在赋值之间, `total` 可能被其他线程拿去进行了 `+1` 的操作,导致一些 `+1` 后的值没有被赋值在 `total` 上。因此,为了防止数据竞争问题,我们需要对 `total` 进行原子操作。线程安全。
```cpp
long long total = 0;
void func() {
//g_lock.lock(); 锁的粒度要尽量小,所以只有在竞争的元素被操作的时候进行加锁,用完立即解锁
for (int i = 0; i < 1000000; i++) {
g_lock.lock();
total = total + 1;
g_lock.unlock();
}
//g_lock.lock();
}
void test2() {
clock_t start = clock();
thread t1(func);
thread t2(func);
t1.join();
t2.join();
clock_t end = clock();
cout << "total = " << total << endl;
cout << "time = " << end - start << " ms\n";
}
```
在新标准 C++ 11,引入了原子操作的概念。如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。
```cpp
atomic<long long> total = 0;
void func() {
for (int i = 0; i < 1000000; i++) {
total = total + 1;
}
}
void test3() {
clock_t start = clock();
thread t1(func);
thread t2(func);
t1.join();
t2.join();
clock_t end = clock();
cout << "total = " << total << endl;
cout << "time = " << end - start << " ms\n";
}
```
C++11:互斥量