C++11:互斥量

多任务的同步与互斥,一个基于 Linux 下并发编程关于锁的一些介绍。相较于 C++ 而言就是换汤不换药,主要是一些语法上的区别,集体的作用什么的看那篇博客吧,本文简单介绍一下。

为什么需要互斥量

  在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的。

#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,它是非阻塞的。

#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 操作。

#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 资源导致被挂起。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。

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 进行原子操作。线程安全。

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,引入了原子操作的概念。如果我们在多个线程中对这些类型的共享资源进行操作,编译器将保证这些操作都是原子性的,也就是说,确保任意时刻只有一个线程对这个资源进行访问,编译器将保证多个线程访问这个共享资源的正确性。从而避免了锁的使用,提高了效率。

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";
}