C++多线程学习笔记一

本文介绍了C++中四种创建线程的方式:Lambda表达式、对象、std::bind及Lambda调用成员函数,并探讨了线程管理函数、互斥锁、RAII原则在多线程中的应用,以及条件变量在并发控制中的角色。示例代码展示了不同线程创建方法的使用,并分析了线程同步中的一些问题和解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一.线程执行体:

Lambda表达式的多线程

#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
using namespace std;
int main() {
    thread td([](int a, int b) {
        cout << a << "+" << b << "=" << a + b << endl;
    },1,2);
    td.join();
    system("pause");
}

对象的多线程

struct functor
{
    void operator()(int a, int b) {
        cout << a << "+" << b << "=" << a + b << endl;
    }
};
int main() {
    thread td(functor(),1,2);
    td.join();
    system("pause");
}

使用std::bind表达式绑定对象和其非静态成员函数

using namespace std;
class C {
    int data_;
public:
    C(int data) :data_(data) {}
    void member_fun(int c) {
        cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
    }
};
int main() {
    C obj(10);
    thread td(bind(&C::member_fun, &obj,3));
    td.join();
    system("pause");
}

使用Lambda表达式调用对象的非静态成员函数

class C {
public:
    int data_;
    C(int data) :data_(data) {}
    void member_fun(int c) {
        cout << "this->data=" << this->data_ << "; extend c=" << c << endl;
    }
};
int main() {
    C obj(10);
    auto a = [obj]()mutable {obj.member_fun(3); };
    obj.data_ = 11;
    thread td(a);
    td.join();
    thread td2([&obj]() {obj.member_fun(4); });
    td2.join();
    system("pause");
}

注意结果的输出,两种lambda策略,上面一种是复制obj,下面是引用。所以打印时一个是10,一个是11

二.线程管理函数

1.

#include <iostream>  
#include <thread>  
#include <iomanip>  
  
int main()  
{  
  std::thread td([](){});  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
  td.detach();  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
}  

2.

#include <iostream>  
#include <thread>  
#include <iomanip>  
  
int main()  
{  
  std::thread td([](){});  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
  td.join();  
  std::cout << "td.joinable() = " << std::boolalpha << td.joinable() << std::endl;  
}  

3.RAII

 class thread_guard {  
    std::thread& t_;  
  public:  
    explicit thread_guard(std::thread& t) : t_(t) { }  
    thread_guard(const thread_guard&) =delete;  
    thread_guard& operator=(const thread_guard&) =delete;  
  
    ~thread_guard() { if (t_.joinable()) t_.join(); }  
  };  

三.互斥Mutex

std::mutex 互斥对象

std::timed_mutex 带有超时的互斥,超时后直接放弃

std::recusive_mutex 允许被同一个程序递归的lock unlock

std::recusive_timed_mutex 带了超时的xx

std::shared_timed_mutex(c++14) 允许多个线程共享所有权的互斥对象,比如读写锁

用mutex对set的insert操作进行保护,实现安全的并发访问

#include<iostream>
#include<thread>
#include<vector>
#include<algorithm>
#include "ThreadGuard.h"
#include <set>
#include <mutex>
#include<random>
int main() {
	std::set<int> int_set;
	std::mutex mt;
	auto f = [&int_set, &mt]() {
		try {
			std::random_device rd;
			std::mt19937 gen(rd());
			std::uniform_int_distribution<> dis(1, 1000);
			for (std::size_t i = 0; i != 100000; ++i) {
				mt.lock();
				int_set.insert(dis(gen));
				mt.unlock();
			}
		}
		catch (...) {}
	};
	std::thread td1(f), td2(f);
	td1.join();
	td2.join();
	system("pause");
}

四.使用RAII管理互斥对象

std::lock_guard严格基于作用域的锁管理类模板,构造时是否加锁是可选的,析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。。lock_guard 对象不可被拷贝构造或移动构造

std::unique_lock 更加灵活的锁管理模板,构造时是否加锁可选,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命周期允许手动加锁和释放锁。构造(或移动(move)赋值)时,unique_lock 对象需要传递一个 Mutex 对象作为它的参数,新创建的 unique_lock 对象负责传入的 Mutex 对象的上锁和解锁操作。

unique_lock(const unique_lock&) = delete;
unique_lock(unique_lock&& x);

 

 

std::shared——lock(c++14)

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>

std::mutex mtx;           // mutex for critical section

void print_thread_id(int id) {
	try {
		for (int i = 0; i < 10; i++) {
			//mtx.lock();
			//std::lock_guard<std::mutex> lck(mtx, std::adopt_lock
			//std::lock_guard<std::mutex> lck(mtx);
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
			std::cout << "thread #" << id << ">>" << i <<'\n';
			if (i == 7) throw (std::logic_error("fake error"));
			//mtx.unlock();
		}
	}
	catch (...) {
		std::cout << "exception" << std::endl;
	}
}

int main()
{
	std::thread threads[10];
	// spawn 10 threads:
	for (int i = 0; i<10; ++i)
		threads[i] = std::thread(print_thread_id, i + 1);
	for (auto& th : threads) th.join();
	system("pause");
	return 0;
}

加锁策略:

1.默认 请求锁,阻塞当前线程直到成功获得锁  三种都支撑

2.std::defer_lock 不请求锁                                 unique_lock,shared_lock

3.std::try_to_lock 尝试请求锁,但不阻塞线程,锁不可用时也会立即返回     unique_lock,shared_lock

4.std::adopt_lock 假定当前线程已经获得互斥对象的所有权,所以不再请求锁   lock_guard,unique_lock,shared_lock

{
    std::unique_lock<std::mutex> lock1(mutex1, std::defer_lock);
    std::unique_lock<std::mutex> lock2(mutex2, std::defer_lock);
    std::lock(mtx1, mtx2);
    do_sth();
}


{
    std::unique_lock<std::mutex> lock1(mutex1, std::try_to_lock);
    if(lock1.owns_lock()){
        do_sth1();
    } esle {
        do_sth2();
    }
}

std::unique_lock std::lock_guard都能实现自动加锁与解锁功能,但是std::unique_lock提供了 lock(), unlock() try_lock() 函数,要比std::lock_guard灵活控制锁的范围,减小锁的粒度但是更灵活的代价是占用空间相对更大一点且相对更慢一点

五.条件变量

条件变量:一种同步原语(Synchronization Primitive)用于多线程之间的通信,它可以阻塞一个或同时阻塞多个线程直到,收到来至其他线程的通知;超时;发送虚假唤醒(Spurious Wakeup)。

C++11的条件变量有两个类

std::condition_variable:必须与std::unique_lock配合使用

std::condition_variable_any:更加通用的条件变量,可以与任意类型的锁配合使用,相比前者使用时会有额外的开销

两者在线程要等待条件变量前,都必须要获取相应的锁

二者相同的成员函数:

notify_one

notify_all

wait

wait_for  >>超时设置为时间长度

wait_until >>超时设置为时间点

遗留说明:

condition_variable_any的额外开销是什么?虚假唤醒是啥?

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::lock_guard, std::adopt_lock
#include <chrono>
#include <stdexcept>
#include <queue>
#include <string>
#include <ctime>
#include <sys/timeb.h>
std::mutex mtx;           // mutex for critical section
std::queue<std::string> dataQueue;
std::condition_variable dataCond;
bool isStop = false;


std::string getSystemTime()
{
	std::this_thread::sleep_for(std::chrono::milliseconds(10));
	struct timeb t;
	ftime(&t);
	return std::to_string(1000 * t.time + t.millitm);
}


void DataProduceThread() {
	while (true) {
		std::string data = getSystemTime();
		std::lock_guard<std::mutex> lock(mtx);
		dataQueue.push(data);
		dataCond.notify_one();//尝试注释该行,执行下,有助于理解条件变量有什么用
		if (isStop) break;
	}
}

void DataConsumeThread(int consumerId) {
	while (true)
	{
		std::unique_lock<std::mutex> lock(mtx);
		dataCond.wait(lock, [] {return !dataQueue.empty(); });
		std::string data = dataQueue.front();
		dataQueue.pop();
		lock.unlock();//wait返回时mutex处于locked状态,为了提高并发应用效率,应立即显示解锁,后继续处理数据
		//handle data....
		std::cout << "[consumerId:" << consumerId << "] handle data: " << data << std::endl;
		std::this_thread::sleep_for(std::chrono::milliseconds(100));
		if (isStop) break;
	}
}



int main()
{
	std::thread pd(DataProduceThread);
	std::thread consumers[10];
	for (int i = 0; i < 10; i++) {
		consumers[i] = std::thread(DataConsumeThread, i + 1);
	}
	std::this_thread::sleep_for(std::chrono::seconds(10));
	isStop = true;
	pd.join();
	for (auto& t : consumers) t.join();
	system("pause");
	return 0;
}

结合这个例子,大家可以再试着将consumer减到3,在lock获取,cond通过后的地方加上打印,结合具体大家,个人得出下面心得:

1.虽然打印会有乱序,原因是lock.unlock后才进行的数据处理。导致cout输出流程,存在并发冲突调用的情况。如果将lock.unlock移到线程等待的sleep_for前面,就不会有这个问题了。但如果移到sleep_for后面程序会没法跑结束,为什么呢?

这个是因为,dataCond.wait(lock,[]{})。条件变量的wait是阻塞等待,当produce线程先停止后,经notify_one(),导致总会有线程没有被唤醒,出现阻塞卡死等待。

这时候可以这样验证下,将producer里的notify_one()改成notify_all()。恩,结果发现还不行?!!

那个这个时候就要在看下dataCond.wait(lock,[]{})这个了。可以看到wait有两个参数,后面那个是lambda表达式,啥式不是关键。关键是这个参数的作用,简单看下定义,它是条件判断。换句话说,condition被唤醒了还不算真被唤醒,他还可以通过这个参数进行判断,到底是否满足条件,如果不行它还是会进行阻塞,等待下一次唤醒。

所以如果要验证,可以这样再改下,把判断条件去了dataCond.wait(lock);然后就可以跑完,正常退出了。

最后这个问题到底应该怎么正确修复了,个人觉得可以将wait改为wait_for,超时则退出,进行下一次循环,不要一直死等。当然判断条件函数还是有意义的,可以防止虚假唤醒,提高整体的运作效率。有兴趣的同学,可以想办法构造验证下如果producer是notify_one,加入第一个被notify的线程A不满足条件没被唤醒,是否会有其他的线程B继续被notify然后判断是否满足条件。恩这个个人感觉是这样的,但毕竟没实际验证,可能有出入。dataCond.wait_for(lock, std::chrono::milliseconds(2), [] {return !dataQueue.empty(); });

 

2.通过打压分析,多线程之间,mtx是生效的,lock锁可以保障同时只有一个可以进入,算是实现了并发冲突的解决。

3.既然mtx+lock已经实现了并发冲突,condition的意义到底是什么?我如今的理解是阻塞,防止线程无意义的空转。条件变量,不满足条件就别xx嘛。mtx是互斥锁,本质上他的作用是解决并发冲突的;lock只是对mutex的封装,本质上解决lock和unlock分开写,过程异常导致未正常释放的问题;所以这么看mutex和lock都是没有阻塞等待的作用。so如果不想空转,又不想没轮等个xx时间再来一轮,就有了condition。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值