线程池-入门

发布时间 2023-12-16 20:59:22作者: zxinlog

线程池

1. 创建线程 thread

#include <thread>

int main(){
	std::thread th()
}

thread 直接创建一个线程,参数是所需执行的函数。

2. join

当开启线程后,主线程不会等待其他线程执行完后再关闭,所以需要让主线程去等待其他线程执行完之后,再关闭主线程。

可通过 joinable 判断该线程是否是可 join 的。添加判断条件

th.join();

if(th.joinable()){
	th.join();
}

3. detach

主线程不等待主线程执行结束,且主线程结束之后,子线程依然可以执行。

当使用detach之后,主线程和子线程分离,子线程在后台依旧可以运行。

4. std::ref

可以将一个临时变量、局部变量转换成引用变量。

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


void modify(int& num){
	num *= 2;
}
int main(){
	int num = 3;
	std::thread th(modify, std::ref(num));
	std::cout << "num = " << num << std::endl;
	system("pause");
}

5. thread 绑定成员函数

倘若现在存在一个类,其中有一个成员函数 func

class A{
public:
	void func(){
		std::cout << "A::func()" << std::endl;
	}
}

使用thread去绑定func的时候,

std::thread th(&A::func);
//上面这句是不对的,除了绑定类::成员函数之外,因为成员函数还包含一个隐含的this指针,所以需要再后面添加一个对象的指针。
//可以创建出一个对象,可以new,也可以使用make_shared创建一个智能指针。

std::shared_ptr<A> pa = std::make_shared<A>();
std::thread th(&A::func, pa);

如果 A中的func 的访问修饰符从 public 改成 private,则 thread绑定就绑定不到该成员函数。

可以在类A中将执行thread 的函数声明成友元函数。

class A{
	friend void func();
//    或者直接将thread相关的语句写在main中,声明main函数为友元
    friend int main();
private:
	void func(){
		std::cout << "A::func()" << std::endl;
	}
}

void func(){
	std::shared_ptr<A> pa = std::make_shared<A>();
	std::thread th(&A::func, pa);
}

int main(){
	func();
}

6. 互斥锁

互斥锁本质上就是为了解决多线程访问共享变量时所发生的问题,当多个线程同时访问同一个变量的时候,对该变量进行的操作在同一时刻被视为重合的,因此会曲解部分操作内容。

比如两个线程同时对一个为 0 的变量各进行一万次的+1操作,当结果出来时,理想结果应该是20000,但最终可能是不到20000的数据,因为中间部分操作发生了冲突。也就是存在竞态条件问题。竞态条件指的是多线程并发执行操作时,程序结果依赖于线程的执行顺序,因此导致结果是不确定性的。

使用互斥锁,mutex 可以进行上锁可解锁。在一个线程对共享数据进行操作时,进行上锁。当操作结束时,进行解锁。在上锁和解锁中间的这块区域被称为临界区,临界区如果过大,则会有线程饥饿的问题。临界区如果过小,则会有频繁上锁解锁的等待问题。因此临界区的大小设计要合理。

上锁可以理解为获取一个资源的所有权。
解锁可以理解为释放一个资源的所有权。

#include <mutex>

int main(){
	std::mutex _mutex;
    
	_mutex.lock(); // 上锁
	// 临界区
	mutex.unlock(); // 解锁
}

既然有互斥锁,那么不可避免的就会出现死锁问题。死锁问题的产生有多种方式,比如一个不可重入锁被重复上锁;或者当前有两把锁,AB线程各锁一把,且都等待对方的锁的释放

std::mutex mutexA;
std::mutex mutexB;

void func1(){
	mutexA.lock();
	mutexB.lock();
	// TODO
	mutexB.unlock();
	mutexA.unlock();
}

void func2(){
	mutexB.lock();
	mutexA.lock();
	// TODO
	mutexA.unlock();
	mutexB.unlock();
}

此时两个线程同时执行,就会出现死锁问题。

7. lock_guard

lock_guard 接收一个 mutex 参数,当接收该参数后,构造函数会默认对其进行上锁,析构函数会进行解锁,且 lock_guard 无法进行复制和移动,只能定义在局部作用域下。

且lock_guard提供了其他的构造函数,可以不上锁的一个构造函数版本。

std::lock_guard lk(mutex);

当lock_guard 所在的局部作用域释放后,自动解锁。

std::mutex mutex;
int shared_data = 0;

void func(){
	for(int i=0;i<10000;i++){
		std::lock_guard<std::mutex> lk(mutex);
		shared_data = 0;
	}
}

int main(){
    std::thread th1(func);
    std::thread th2(func);
    
    th1.join();
    th2.join();
    system("pause");
}

8. unique_lock

unique_lock 提供了比lock_guard 更加丰富的加锁解锁功能,如 try_lock 、超时等操作。

在使用unique_lock 构造锁的时候,不对锁进行任何其他操作,

std::unique_lock<std::mutex> lk(mutex, std::defer_lock);

std::defer_lock 表示不对其进行加锁,调用其中的一个构造函数。

可以使用 try_lock 或 try_lock_for进行延时加锁。普通的锁也不支持延时操作,因此在使用try_lock_for的时候,需要使用的是一个 时间锁。

std::time_mutex mutex;
std::unique_lock<std::time_mutex> lk(mutex, std::defer_lock);
lk.try_lock_for(std::chrono::seconds(5));

当线程使用延迟锁的时候,给定的时间内拿不到共享资源就会直接返回。

9. call_once

为了防止如单例模式下,一个单例在多线程中被创建了多次对象,可以使用 call_once 来确保某个函数只会执行一次。

且call_once只能在多线程中使用。

void f(){
	std::once_flag once;
	std::call_once(once, func);
}
// 多线程运行该函数,call_once确保只有一个线程可以执行 func