C++11语言级别的多线程编程

本文深入探讨C++中的多线程编程,包括thread类的使用、线程互斥锁mutex、生产者-消费者模型、lock_guard和unique_lock的区别、atomic原子类型以及线程安全的打印问题。通过实例代码展示了如何利用锁和原子操作解决并发编程中的同步问题。
摘要由CSDN通过智能技术生成

一、多线程类thread

C++语言层面thread,其实底层还是调用操作系统对象的多线程函数,例如windows下调用createThread,linux下调用的是pthread_create。语言层面的支持就做到了跨平台编译运行。

thread示例代码:

#include <iostream>
#include <thread>

using namespace std;

void fun(string info){
	// 线程2睡眠4秒
	std::this_thread::sleep_for(std::chrono::seconds(2));
	cout <<"info:" << info << endl;
}

int main(){
	// 创建thread线程对象,传入线程函数和参数,线程直接启动运行
	thread t(fun, "hello world");
	
	// 等待线程t和t1执行完,main线程再继续运行
	t.join();
	
	// 子线程设置为分离线程
	// t.detach();
	
	cout << "main thread end!" << endl;
	return 0;
}

二、线程互斥锁mutex

可参考:大秦坑王

#include <iostream>
#include <thread>
#include <string>
#include <mutex>

using namespace std;

// aotomic_int tickets = 100;
int tickets = 100;

// 全局的互斥锁
mutex mtx;

// 线程函数
void sellTicket(int window){
	while (tickets > 0){
		mtx.lock();
		// 不加if可能导致tickets=1时,两个线程同时进入while循环,导致tickets成为负数
		if (tickets > 0){
			cout << window << " 售卖第" << tickets << "张票" << endl;
			tickets--;
		}
		mtx.unlock();
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}

int main(){
	thread t1(sellTicket, 1);
	thread t2(sellTicket, 2);
	thread t3(sellTicket, 3);
	// 等待三个线程执行完成
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

lock_guard自动释放锁

防止程序运行时不可预知的错误,拿到互斥锁后做完操作,一定要进行释放。
出作用域后进行资源释放,类似于智能指针。

部分源码如下,lock_guard不允许拷贝构造和赋值

	lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;
void sellTicket(int window){
	while (tickets > 0){
		{
			// 构造的时候获取锁,出作用域析构的时候释放锁
			lock_guard<mutex> lock(mtx);
			if (tickets > 0) {
				cout << window << " 售卖第" << tickets << "张票" << endl;
				tickets--;
			}
		}
		// 出局部作用域后,释放互斥锁
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
	}
}

三、生产者—消费者模型

使用条件变量互斥锁实现简单的生产者—消费者模型(生产一个消费一个)

正规的模型可参考博客:经典进程同步和互斥问题

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>

using namespace std;

// 定义互斥锁,做线程互斥操作
mutex mtx; 

// 定义条件变量,做线程通信
condition_variable cv;

class Queue {
public:
	void put(int val) {
		// 构造的时候获取锁,出作用域析构的时候释放锁
		unique_lock<mutex> lck(mtx);  // 锁被占用时,阻塞到此处,无法向下执行
		if (!_q.empty()) {
			// 队列不空时,生产者通知消费者消费,消费后再继续生产
			// 生产者线程释放互斥锁,并且进入等待状态,等待其他线程通知
			// 得到通知时:等待 -> 阻塞;拿到互斥锁时:阻塞 -> 运行;wait时:运行 -> 等待
			cv.wait(lck);
		}
		_q.push(val);
		cv.notify_all();
		cout << "producer produces : " << val << endl;
	}

	int get() {
		// 构造的时候获取锁,出作用域析构的时候释放锁
		unique_lock<mutex> lck(mtx);
		if (_q.empty()) {
			// 队列空时,消费者通知生产者生产
			// 消费者线程进入等待状态,并且释放互斥锁
			// 条件变量这里需要unique_lock
			cv.wait(lck);
		}
		int val = _q.front();
		_q.pop();
		cv.notify_all();
		cout << "consumer gets : " << val << endl;
		return val;
	}
private:
	queue<int> _q;
};

void producer(Queue* q) {
	for (int i = 0; i < 10; i++) {
		q->put(i);
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}

void consumer(Queue* q) {
	for (int i = 0; i < 10; i++) {
		q->get();
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}

int main(){
	Queue q;
	thread p(producer, &q);
	thread c(consumer, &q);

	p.join();
	c.join();

	return 0;
}

在这里插入图片描述

四、再谈lock_guard和unique_lock

(1)mutex用法

mutex mtx;
	
int main(){
	mtx.lock();   // 获取
	mtx.unlock(); // 释放
}

上述写法类似于裸指针,可能出现获取但没释放的情况

(2)lock_guard

一般我们使用lock_guard,功能类似于智能指针,构造函数获取锁,析构函数释放锁

部分源码如下,lock_guard不允许拷贝构造和赋值

	lock_guard(const lock_guard&) = delete;
    lock_guard& operator=(const lock_guard&) = delete;

由于没有拷贝构造函数和等号运算符重载函数,不可能用于函数的参数传递以及返回过程中,只能用在临界区代码段加锁解锁。

用法如下:

lock_guard<mutex> guard(mtx);

(3)unique_lock

同样是构造函数获取锁,析构函数释放锁。

删除了左值的拷贝构造和等号运算符重载,提供了右值的拷贝构造和等号运算符重载,unique_lock可用于函数的参数传递以及返回过程中。

底层也提供了互斥锁的lock()unlock()方法

用法如下:

unique_lock<mutex> lck(mtx);

总结: unique_lock完全可以取代lock_guard,就像unique_ptr可以完全取代scope_ptr一样,lock_guard只能通过构造加锁,析构解锁,而unique_lock除了通过构造析构加解锁,也提供了加解锁的方法,一般和condition_variable配合使用

(4)condition_variable

通常和unique_lock搭配使用

	unique_lock<mutex> lck(mtx);
	cv.wait(lck); // 1. 使线程进入等待状态    2. lck.unlock()释放互斥锁mtx   3. 等待通知,收到通知后会从wait返回,返回后就重新加上锁了
	// 通知cv上等待的线程,条件成立了,让其他线程从等待状态进入阻塞状态,开始抢互斥锁(此时当前线程释放锁)
	// 抢到互斥锁的线程从阻塞状态->运行状态
	cv.notify_all();

线程调用wait()方法后:

  1. 使线程进入等待状态
  2. lck.unlock()释放互斥锁mtx,让其他线程争夺锁
  3. 等待通知,收到通知后会从等待状态到阻塞状态,如果获取到锁,则可以从wait返回,顺利向下执行

lock_guard和unique_lock都是RAII机制下的锁,即依靠对象的创建和销毁也就是其生命周期来自动实现一些逻辑,而这两个对象就是在创建时自动加锁,在销毁时自动解锁。所以如果仅仅是依靠对象生命周期实现加解锁的话,两者是相同的,都可以用,因跟生命周期有关,所以有时会用花括号指定其生命周期。但lock_guard的功能仅限于此,unique_lock是对lock_guard的扩展,允许在生命周期内再调用lock和unlock来加解锁以切换锁的状态

根据linux下条件变量的机制,condition_variable在wait成员函数内部会先调用参数unique_lock的unlock临时解锁,让出锁的拥有权(以让其它线程获得该锁使用权加锁,改变条件,解锁),然后自己等待notify信号,等到之后,再调用参数unique_lock的lock加锁,处理相关逻辑,最后unique_lock对象销毁时自动解锁

也即是说condition_variable的wait函数内伪代码如下:

condition_variable::wait(std::unique_lock<std::mutex>& lk){
      lk.unlock();

      waiting_signal();

      lk.lock();
}

五、基于CAS操作的atomic原子类型

互斥锁用于比较复杂的场景,而简单的++--使用轻量的atomic原子类型即可。

一般也搭配volatile使用,volatile防止线程对变量进行缓存,操作的都是原始内存中的值

不加volatile的话,每个线程都会拷贝一份自己的线程栈上的变量,带到CPU的寄存器,这样效率较高,但也可能出错

#include <iostream>
#include <thread>
#include <list>
#include <atomic>

using namespace std;

volatile atomic_bool is_ready = false;
volatile atomic_int cnt = 0;

void task() {
	if (!is_ready) {
		// 当前线程让出时间片
		this_thread::yield();
	}
	// 每个线程都加100次
	for (int i = 0; i < 100; i++) {
		cnt++;
	}
}

int main(){
	list<thread> tlist;
	for (int i = 0; i < 10; i++) {
		tlist.push_back(thread(task));
	}
	this_thread::sleep_for(chrono::seconds(2));
	is_ready = true;
	for (thread& t : tlist) {
		t.join();
	}
	cout << cnt << endl; // 1000
	return 0;
}

六、n个线程打印0-m

不用任何原子类型或者锁

打印一定要先打印,修改flag条件一定要最后修改。防止一修改flag,其他线程就通过了if条件

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

// num_thread个线程输出
// 1-n   0-m
int flag = 1;
int num = 0;
const int num_thread = 3;
const int num_max = 7;

// 线程函数
void print(int tid) {
	while (true) {
		if (tid == flag) {
			// 打印一定要先打印,修改flag条件一定要最后修改。防止一修改flag,其他线程就通过了if条件
			cout << tid << " " << num << endl;
			num = (num + 1) % (num_max + 1);            // 0 ~ num_max
			flag = flag % num_thread + 1;         // 1 ~ num_thread
		}
	}
}

int main() {
	vector<thread> vec;
	for (int i = 1; i <= num_thread; i++) {
		vec.push_back(thread(print, i));
	}
	for (thread& t : vec) {
		t.join();
	}
	return 0;
}

在这里插入图片描述

只打印一次 0~num_max

#include <iostream>
#include <thread>
#include <vector>

using namespace std;

// num_thread个线程输出
// 1-n   0-m
int flag = 1;
int num = 0;
const int num_thread = 4;
const int num_max = 7;

// 线程函数
void print(int tid) {
	while (true) {
		if (num == num_max + 1) {
			return;
		}
		if (tid == flag) {
			if (num == num_max) {
				cout << tid << " " << num << endl;
				// 这里修改num,让num超过num_max,表示可以退出程序了,让其他两个线程退出
				num++;
				// 不修改flag,其他两个线程就不会进入if(tid == flag)
				return;
			}
			cout << tid << " " << num << endl;
			num = (num + 1) % (num_max + 1);            // 0 ~ num_max 
			flag = flag % num_thread + 1;               // 1 ~ num_thread
		}
	}
}

int main() {
	vector<thread> vec;
	for (int i = 1; i <= num_thread; i++) {
		vec.push_back(thread(print, i));
	}
	for (thread& t : vec) {
		t.join();
	}
	return 0;
}

在这里插入图片描述

七、三个线程打印abcabcabc

不使用任何锁或者线程安全类型

#include <iostream>
#include <thread>

using namespace std;

char flag = 'a';

void print(char c) {
	while (true) {
		// 由于有if判断,同一时刻,只有一个线程在执行满足if条件的语句
		if (flag == c) {
			// 先打印,再修改
			cout << string(1, c) << endl;
			flag++;
			if (flag == 'd') {
				flag = 'a';
			}
		}
	}
}

int main() {
	thread t1(print, 'a');
	thread t2(print, 'b');
	thread t3(print, 'c');
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

打印指定数量的abc

#include <iostream>
#include <thread>

using namespace std;

char flag = 'a';
int num = 1;

void print(char c) {
	while (num <= 1000) {
		// 由于有if判断,同一时刻,只有一个线程在执行满足if条件的语句
		if (flag == c) {
			
			// 先打印,再修改
			cout << string(1, c);
			
			if (c == 'c') {
				cout  << " " << num << endl;
			}
			
			flag++;
			if (flag == 'd') {
				num++;
				if(num > 1000){
					break;
				}
				flag = 'a';
			}
		}
	}
}

int main() {
	thread t1(print, 'a');
	thread t2(print, 'b');
	thread t3(print, 'c');
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

使用互斥锁和condition_variable条件变量

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>

using namespace std;

// 定义互斥锁,做线程互斥操作
mutex mtx;

// 定义条件变量,做线程通信
condition_variable cv;

char flag = 'a';

void print(char c) {
	while (true) {
		unique_lock<mutex> lck(mtx);
		if (flag != c) {
			// if不正确,wait解除阻塞后,当前flag不一定就和局部变量c相等
			cv.wait(lck);
		}
		cout << string(1, c) << endl;
		flag++;
		if (flag == 'd') {
			flag = 'a';
		}
		cv.notify_all();
	}
}

int main() {
	thread t1(print, 'a');
	thread t2(print, 'b');
	thread t3(print, 'c');
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

以上代码并不能完成abcabc打印预期,比如此时flag为b,t1和t3线程都阻塞在wait,t2打印完后通知t1、t3起来抢互斥锁,假如t1抢到了锁,从wait返回,就会直接向下打印字符a,因为在wait前就进行了if判断,拿到锁从wait返回后就可以继续向下走

所以这里我们需要用while循环继续判断,判断是否相等,不相等再wait释放锁然后进入等待状态,相等就拿着锁向下执行

线程调用wait()方法后:

  1. 使线程进入等待状态
  2. lck.unlock()释放互斥锁mtx,让其他线程争夺锁
  3. 等待通知,收到通知后会从等待状态到阻塞状态,如果获取到锁,则可以从wait返回,顺利向下执行

正确代码如下:

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>

using namespace std;

// 定义互斥锁,做线程互斥操作
mutex mtx;

// 定义条件变量,做线程通信
condition_variable cv;

char flag = 'a';

void print(char c) {
	while (true) {
		unique_lock<mutex> lck(mtx);
		while (flag != c) {
			// 释放锁,阻塞等待,尝试加锁
			cv.wait(lck);
		}
		cout << string(1, c) << endl;
		flag++;
		if (flag == 'd') {
			flag = 'a';
		}
		cv.notify_all();
	}
}

int main() {
	thread t1(print, 'a');
	thread t2(print, 'b');
	thread t3(print, 'c');
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

仅使用一把互斥锁

mutex mtx;

char flag = 'a';

void print() {
	while (true) {
		unique_lock<mutex> lck(mtx);
		cout << string(1, flag);
		flag++;
		if (flag == 'd') {
			flag = 'a';
		}
	}
}

int main() {
	thread t1(print);
	thread t2(print);
	thread t3(print);
	t1.join();
	t2.join();
	t3.join();
	return 0;
}

detach方法

detach的作用是将子线程和主线程的关联分离,也就是说detach后子线程在后台独立继续运行,主线程无法再取得子线程的控制权,即使主线程结束,子线程未执行也不会结束。当主线程结束时,由运行时库负责清理与子线程相关的资源

实际应用如让一个文字处理应用同时编辑多个文档,让每个文档处理窗口拥有自己的线程,每个线程运行同样的代码,并隔离不同窗口处理的数据

detach()同时也带来了一些问题,如子线程要访问主线中的对象,而主线中的对象又因为主线程结束而被销毁时,会导致程序崩溃。所以传递参数时需要注意一些陷阱

三个线程打印1-100(java)

class PrintHundred {
    private static volatile int number = 1;
    private static volatile int order = 1;

    private static final Object lock = new Object();

    public static void print(final int flag) {
        while(number < 100){
            synchronized (lock) {
                // while防止虚假唤醒,醒来以后order被修改了,或者条件依然不成立
                while(order != flag) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if(number <= 100){
                    System.out.println(Thread.currentThread().getName() + ": " + number);
                }
                number++;
                if(++order == 4){
                    order = 1;
                }
                lock.notifyAll();
            }
        }
    }

    public static void main(String[] args) {
        new Thread(()->{
            print(1);
        }, "线程1").start();
        new Thread(()->{
            print(2);
        }, "线程2").start();
        new Thread(()->{
            print(3);
        }, "线程3").start();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bugcoder-9905

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值