一文从小白手撕C++(11)线程池

本文记录学习c++(11) Thread相关的多线程使用。

1. Thread类的线程创建:

线程对象的创建较为简单:thread th1;便完成了对象的创建;
在创建完对象后,在多线程对象中传入需要在多线程运行的函数;以下是一个代码实例:

//导入线程类的头文件“thread”
#include <thread>
#include <iostream>

using namespace std;

//需要多线程运行的函数
void func(int& a){
	//将传入的引用所对应的int变量
	for(int i = 0; i < 100000; ++i){
		a++;
	}
}

int main(){
	int a = 0;
	//创建th1线程对象,传入func()函数及所需的参数
	//std::ref(a),因为传入的参数是引用类型,所以这里应该使用std::ref()将转为引用类型,否则会报错:数据未定义;
	thread th1(func, ref(a));
	//thread::joinable()会判断这个函数是否适合在多线程中运行,并返回一个bool
	if(th1.joinable()){
		//调用thread的join()方法,join()方法调用后主线程会等待线程结束并释放线程资源。如果线程已经结束,则立即返回。
		//如果线程被中断,则会抛出std::system_error异常
		th1.join();
		//th1.detach()与thread::join()不同的是,主线程不会等待detach()分离的线程,而是使其成为后台线程。调用此函数后,线程将独立运行,主线程不需要等待其结束。如果线程已经被detach或者线程还没有开始执行,则没有效果。
	}
}

2. 线程使用过程中常见问题

  1. 数据共享问题,当多个线程同时操作同一个数据时且至少有一个线程以上对数据进行了写操作,就容易出现数据竞争问题,具体表现为:线程1在进行写的同时,线程2也进行了写操作,线程2是在线程1写完之前进入写操作的,就会覆盖掉线程1的写操作。以下是代码示例:
#include <iostream>
#include <thread>

using namespace std;

void func(int& a){
	for(int i = 0; i < 100000; ++i){
		a++;
	}
}

int main(){
	int a = 0;
	thread th1(func, ref(a));
	thread th2(func, ref(a));
	th1.join();;
	th2.join();
	cout << a << endl;
}

上述代码在运行时极大可能会出现数据共享问题,a输出的值是小于等于200000的随机数。
为了避免这种情况发生,则需要在线程内给操作的数据加锁。具体如下:

#include <instream>
#include <thread>
//导入互斥锁mutex的头文件
#include <mutex>

using namespace std;

//创建mutex互斥锁对象
mutex mtu;
void func(int& a){
	for(int i = 0; i < 100000; ++i){
		//加锁操作
		mtu.lock();
		a++;
		//解锁操作
		mtu.unlock();
		//mutex::lock()和mutex::unlock()一定成对出现的,否则会造成数据的死锁,使别的线程一直等待这个线程释放这个数据的锁。
	}
}
int main(){
	int a = 0;
	thread th1(func, ref(a));
	thread th2(func, ref(a));
	th1.join();
	th2.join();
	cout << a << endl;
}

此时,由于func()中每次操作a的时候都会加锁,当th1操作a时,th2会等待th1执行mutex::unlock()进行锁的释放,当锁释放后th2才会进行加锁并操作a,同样th2在释放锁之前,th1也会进行等待。

  1. 互斥锁死锁问题。互斥锁死锁造成的原因是:当两个或以上多线程中运行的函数中操作了两个及以上的互斥锁,加锁解锁顺序混乱引起的。如:
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex m1, m2;
void func1(){
	m1.lock();
	m2.lock();
	m1.unlock();
	m2.unlock();
}
void func2(){
	m2.lock();
	m1.lock();
	m1.unlock();
	m2.unlock();
}

int main(){
	thread th1(func1);
	thread th2(func2);
	th1.join();
	th2.join();
}

上述代码在运行过程中可能出现的情况:func1m1加锁的同时func2中的m2也加了锁,所以会造成func1在等待func2继续往下执行到m2解锁,但由于func1m1加了锁,此时func2也在等待func1m1解锁。两个线程互相等待程序就卡死在这里不往下执行,就叫做死锁

为了避免这种情况的发生,只需要注意,mutex互斥锁的加锁顺序
func1中加锁顺序是m1m2,其他函数中也应当先给m1加锁,再给m2加锁。


3. std::lock_guard与std::unique_lock。

**lock_guard是C++提供的一种互斥锁封装类,用于保护共享数据,防止多个线程同时访问同一资源而造成数据竞争问题。

std::lock_guard的特点如下:

  • 当构造函数被调用时,该互斥量会被自动锁定。
  • 当析构函数被调用时,该互斥量会被自动解锁。
  • std::lock_guard对象不能复制或移动,因此它只能再局部作用域中使用

std::lock_guard使用示例:

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

using namespace std;

mutex m1;
void func(int& a){
	for(int i = 0; i < 100000; ++i){
		//创建lock_guard对象,并传入m1对lg进行初始化。此时lg会调用构造函数,当lock_guard类的构造函数被调用时,m1会被自动加锁。
		lock_guard<metux> lg(m1);//此时m1加锁。
		a++;
		//当局部作用域结束时,lock_guard会调用自己的析构函数,此时传入用于初始化lg的m1就会被解锁。
	}
}

int main(){
	int a = 0;
	thread th1(func, ref(a));
	thread th2(func, ref(a));
	th1.join();
	th2.join(); 
	cout << a << endl;
}

**unique_lock也是c++标准库提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。

std::unique_lock提供了以下几个成员函数:

  • lock():尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。

  • try_lock():尝试对互斥量进行加锁操作,如果当前量已被其他线程持有则返回false,否则返回true。

  • try_lock_for(const std::chrono::duration<Rep, Period>& rel_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或超过了指定时间点。

  • try_lock_until(cost std::chrono::time_point<Clock, Duration>& abs_time):尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或超过指定的时间点。

  • unlock():对互斥量进行解锁操作。

     除上述几个成员函数外,std::unique_lock还提供以下几个构造函数:
    
  • unique_lock() noexcept = default:默认构造函数,创建一个未关联任何互斥量的std::unique_lock对象。

  • explicit unique_lock(mutex_type& m):构造函数,使用给定的互斥量m初始化,并对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, defer_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化操作,但不对该互斥量进行加锁操作。

  • unique_lock(mutex_type& m, try_to_lock_t) noexcept:构造函数,使用给定的互斥量m进行初始化操作,并尝试对互斥量m进行加锁操作,如果加锁失败,则创建的std::unique_lock不与任何互斥量关联。

  • unique_lock(mutex_type& m, adopt_lock_t):构造函数,使用给定的互斥量进行初始化操作,并假设该互斥量已经被当前线程成功加锁。

std::unique_lock的操作极为灵活,所以一般使用此类进行互斥量的使用与管理。

unique_lock的使用实例:

//使用unique_lock(mutex_type& m)构造函数,以m1对lg进行初始化,并加锁。
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

mutex m1;

void func(int& a){
	for(int i = 0; i < 100000; ++i){
		//
		unique_lock<mutex> lg(m1);
		a++;
	}
}

int main(){
	int a = 0;
	thread th1(func, ref(a));
	thread th2(func, ref(a));
	th1.join();
	th2.join();
	cout << a << endl;
}

//使用unique_lock(mutex_type& m1, defer_lock_t)构造函数,以m1对lg进行初始化,但不直接加锁,需要手动加锁lg.lock()。
//如上述做法的话,看起来会多此一举,为什么不直接用可以自动加锁的构造函数呢?
//实质上,如果使用带defer_lock_t参数的构造方法,一般都是要搭配try_lock_for来进行延迟加锁的。如下所示:
#include <iostream>
#include <thread>
#include <mutex>

using namespace std;

timed_mutex m1;//mutex不能进行演示加锁,所以要换成支持延时加锁的互斥量timed_mutex。
void func(int& a){
	for(int i = 0; i < 100000; ++i){
		unique_lock<timed_mutex> lg(m1, defer_lock);//当前已完成初始化,但未加锁。
		lg.try_lock_for(chrono::seconds(5));//摒弃lock(),使用try_lock_for()来进行延迟枷锁的操作,参数为5秒钟,这句代码的意思为:在5秒钟内对m1进行加锁,5秒内成功加锁返回true,超时则返回flase。
		a++;
		/**
		当局部作用域结束时,会自动调用lg的析构函数进行解锁操作。
		**/
	}
}

int main(){
	int a = 0;
	thread th1(func, ref(a));
	thread th2(func, ref(a));
	th1.join();
	th2.join();
	cout << a << endl;
}

延迟加锁即当,线程阻塞时进行等待初始化传入的时间值,若在指定时间内获取到锁则返回true,否则不再等待锁的获取,直接返回false。


4. call_once与其使用场景

单例设计模式用于确保某个类只创建一个实例
由于单例模式的实例是唯一的,所以在多线程环境中使用单例模式,需要考虑线程安全问题。

首先理解一下单例模式,请看以下代码:

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

using namespace std;

class Log{
public:
	Log(){};//默认构造函数
	Log(const Log& log) = delete;//禁用拷贝构造函数
	Log& operator=(const Log& log) = delete;//禁用赋值构造

	static Log& getInstance(){
		Log *log = nullptr;			
		if(!log){
			log = new Log();
		}
		return *log;
	}

	void PrintLog(string msg){
		cout << __DATE__ << "?" << __TIME__  << " ? " << msg << endl;
	}
};

int main(){
	Log::getInstance().PringLog("平安无事。");
}

以上是一个日志类懒汉模式单例,一般项目中只有一个日志类来创建管理所有的系统生成的日志文件信息,所以只能有一个日志类的对象,单例模式就是帮助我们来更好的创建一个只有一个对象的类。
由于懒汉模式存在线程安全问题(饿汉模式不存在线程安全问题),具体表现为:当th1调用getInstance()时,在完成判断但还未创建对象的时候,th2在此时也判断了*log是指向Log对象,由于th1还未创建对象所以th2也准备创建对象,此时这个单例模式就不单例了,创建了两个对象。
要避免这一问题,需要用到call_once()函数。以下是示例代码:

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

using namespace std;

class Log{
public:
	//call_once()可以保证传入的creat()函数只被执行一次,call_once()传入的第一个参数为once_flag类型(struct)的对象。
    static Log& getInstance(){
        if(!log){
            call_once(once_, &Log::creat, nullptr);
        }
        return *log;
    }

    void print(string msg){
        cout << __DATE__ << " " << __TIME__ << " " << msg << endl;
    }

    Log(){}

private:;
    Log(const Log& log) = delete;
    Log& operator=(const Log& log) = delete;
    static void creat(void*){
        log = std::make_shared<Log>();
    }
    static once_flag once_;
    static std::shared_ptr<Log> log;
};

std::shared_ptr<Log> Log::log = nullptr;
std::once_flag Log::once_;

void func(void*){
    cout << &Log::getInstance() << endl;
}

int main(){
    thread th1(func(nullptr));
    thread th2(func(nullptr));
    th1.join();
    th2.join();
}

5. condition_variable及其使用场景

使用condition_variable的一般使用场景:
1. 创建一个condition_variable的对象。
2. 创建一个互斥量mutex对象,来保护共享资源的访问。
3. 在需要等待条件变量的地方使用unique_lock< mutex>对象互斥锁并调用condition_variable::wait()condtion_variable::wait_for()condition_variable::wait_until()函数等待条件变量。
4. 在其他线程中需要通知等待的线程时,调用condition_variable::notify_one()condition_variable::notify_all()函数通知等待线程。

下面是一个生产者-消费者模型,其中使用condition_variable()来实现线程的等待和通知:

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

using namespace std;

mutex g_mutex;
condition_variable g_cv;
queue<int> g_queue;

void Producer(){
	for(int i = 0; i < 10; ++i){
		{
			unique_lock<mutex> lock<g_mutex>;
			g_queue.push(i);
			cout << "Produced : " << i << endl;
		}
		//每存入一个任务进入任务列表,调用一次condition_variable::notify_one()使消费者线程来执行任务
		g_cv.notify_one();
		this_thread::sleep_for(chrono::milliseconds(100));
	}
}

void Consumer(){
	while(true){
		unique_lock<mutex> lock(g_mutex);
		//线程执行条件是当任务列表不为0时,线程开始执行(前提是g_mutex已被获取)
		g_cv.wait(lock, [](){return !g_queue.empty();});
		int value = g_queue.front();
		g_queue.pop();
		cout << "Consumer : " << value << endl;
	}
}

int main(){
	thread th_producer(Producer);
	thread th_Consumer(Consumer);
	th_producer.join();
	th_consumer.join();
	return 0;
}

就是这样,当一个线程调用notify_one时会唤醒其对应的等待函数(wait()、wait_for()…),上述代码为wait()wait()分为有条件式与无条件式等待(上述为有条件式,当任务列表为空的时候返回false,线程阻塞,当任务列表不为空的时候,返回true,线程执行。),条件式一般写为lambda表达式使其返回一个bool类型的值。


6. 简单实现C++(11)跨平台线程池

多线程程序,当任务量小的时候可以一个线程对应一个任务,线程的创建与销毁、上下文切换所消耗的资源都可以忽略不计,但在高并发、高吞吐、低延迟的场景下,必须降低线程资源管理的时间消耗的情况下,就必须使用到线程池了。
换句话说,线程池可以帮你减小多线程运行过程中所需要消耗的资源。

线程池运行的基本逻辑为:管理一个任务队列和线程队列,主线程提交任务到任务队列中,线程队列在初始时就已经创建好了一定数量的线程。这些个线程会在任务列表有任务时运行任务列表中的任务。

线程池的线程是复用的,在线程创建以后这些线程会一直存在,直到线程池关闭退出。每个线程创建后,循环获取任务列表中的任务并执行。

线程池与任务列表之间需要匹配,是一个典型的生产者-消费者模型,需要解决资源访问的冲突,并保证任务为空时,线程应该等待(阻塞),即需要实现线程安全并有同步机制的任务队列。

以下是一个简单的线程池实现:

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

using namespace std;

class ThreadPool{
public:
	/***
	有参构造
	创建对象时初始化stop的值,当stop为false时表示线程池在创建中
	**/
    ThreadPool(int nums): stop(false){
    	//循环创建nums个线程
        for (int i = 0; i < nums; ++i) {
        	//使用emplace_back()是因为thread类不允许拷贝构造和赋值
        	//emplace_back()是通过调用元素类型的构造函数去重新创建一个同类型的元素再去使用。
            threads.emplace_back([this] {
            	//在while中循环直到构建新线程成功
                while(true){
                    unique_lock<mutex> lock(mtx);
                    //等待获取lock及lambda表达式返回true
                    cv.wait(lock, [this]{
                        return !tasks.empty() || stop;
                    });
                    //如果线程池构建完成及任务列表为空则不再往下继续执行
                    if(stop && tasks.empty()){
                        return;
                    }
                    //将tasks中的任务拷贝到task中,并在任务列表中删除
                    function<void()> task(move(tasks.front()));
                    tasks.pop();
                    lock.unlock();
                    //执行任务
                    task();
                }
            });
        }
    }

	//析构函数
    ~ThreadPool(){
    	//将stop置为true表示线程池构建完毕
        {
            unique_lock<mutex> lock(mtx);
            stop = true;
        }
        //唤醒所有线程
        cv.notify_all();
        for(auto& t : threads){
            t.join();
        }
    }

    template<class F, class... Args>
    void enqueue(F&& f, Args&&... args){
        function<void()> task = bind(forward<F>(f), forward<Args>(args)...);
        {
            unique_lock<mutex> lock(mtx);
            tasks.template emplace(move(task));
        }
        cv.notify_one();
    }
private:
    vector<thread> threads;                     //线程队列
    queue<function<void()>> tasks;              //任务队列
    mutex mtx;                                  //互斥锁
    condition_variable cv;                      //线程运行的条件变量
    bool stop;                                  //表示线程池创建什么时候终止

};

int main(){
    ThreadPool pool(4);//创建线程池,并构造4个线程
    //给任务列表中添加任务,此处是让每个线程执行时都休眠1s,打印起始日志。
    for (int i = 0; i < 10; ++i) {
        pool.enqueue([i]{
           cout << "task : " << i << " is running." << endl;
           this_thread::sleep_for(chrono::seconds(1));
           cout << "task : " << i << " is done." << endl;
        });
    }
    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值