线程创建
1.C++11的标准库中的多线程库 #include<thread>
2.创建线程的格式 : 关键字thread 线程名(入口函数,入口函数的参数);
若入口函数参数比较多,按照顺序往后写,用英文逗号隔开。
3.子线程开始运行的两张方式:
- join() 子线程阻塞主线程,子线程运行完之后,主线程再次开始运行;
- detach() 主线程和子线程同时运行,如果主线程需要使用子线程的数据,在用到子线程数据的地方,子线程还没有运行结束,那么这个时候主线程使用的数据还是子线程最初的数据,由此会产生一些错误,不太建议使用这种方式。
例如:
- 入口函数:void fun(int a);
- 创建线程:thread mythread(fun,a);
#include<iostream>
#include<thread>
#include <windows.h>
using namespace std;
void fun(int a)
{
cout << "子线程id为" << this_thread::get_id() << endl;
cout << "子线程睡眠5秒" << endl;
Sleep(5000);
}
int main()
{
int a = 10;
thread mythread(fun, 10);
cout << "主线程中的子线程id:" << mythread.get_id() << endl;
cout << "子线程加入:" << endl;
mythread.join();//主线程被阻塞直至子线程执行结束
//mythread.detach();//主线程子线程分别执行
cout << "子线程结束,返回主线程"<< endl;
return 0;
}
互斥量
1.c++中互斥量是一个类,我们可以理解成是一把锁,在一些需要互斥使用的临界区(代码段)加上锁,锁住的代码区域要适中,太少起步到保护效果,太多会影响运行效率;
2.互斥量的头文件 #include<mutex>
3.互斥量的创建 mutex m; 这个m是创建的类对象,mutex为类名;
4.lock()和unlock()要成对使用;
#include<iostream>
#include<thread>
#include <windows.h>
#include<mutex>
using namespace std;
mutex m;
int a = 1;
void fun1()
{
m.lock();
cout << "thr1拿到锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << ++a << endl;
m.unlock();
}
void fun2()
{
m.lock();
cout << "thr2拿到锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << --a << endl;
m.unlock();
}
int main()
{
thread thr1(fun1);
thread thr2(fun2);
//下面两个线程同时加入,但是执行顺序可能thr1在前,也可能thr2在前
//所以执行结果也有多种
//thr1拿到锁:
//原参数值1
//现参数值2
//thr2拿到锁:
//原参数值2
//现参数值1
//或
//thr2拿到锁:
//原参数值1
//现参数值0
//thr1拿到锁:
//原参数值0
//现参数值1
thr1.join();
thr2.join();
return 0;
}
为避免lock()之后忘记unlock(),造成临界区一直被锁,产生了类似于智能指针的两种方式
lock_guard
1.原理:构造函数中lock(),析构函数中unlouck();
2.可以通过使用{}来调整lock()的作用域范围,让互斥量m在合适的地方被解锁
3.使用lock_guard后不能手动lock()与手动unlock();如果lock_guard加了adopt_lock参数,表示构造函数中不再加锁,需要提前手动加锁。用std::adopt_lock的前提是,自己需要先把mutex lock上;
例如:m.lock();//手动锁定,m为mutex的类对象
lock_guard<mutex> lockg(m,adopt_lock);
4. lock_guard不可以复制也不可以转移(move)
#include<iostream>
#include<thread>
#include <windows.h>
#include<mutex>
using namespace std;
mutex m;
int a = 1;
void fun1()
{
lock_guard<mutex> lockg1(m);
cout << "thr1拿到锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << ++a << endl;
}
void fun2()
{
m.lock();//先手动锁定,才能再使用unique_lock的adopt_lock参数
lock_guard<mutex> lockg2(m, adopt_lock);
cout << "thr2拿到锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << --a << endl;
}
int main()
{
thread thr1(fun1);
thread thr2(fun2);
//下面两个线程同时加入,但是执行顺序可能thr1在前,也可能thr2在前
//所以执行结果也有多种
//thr1拿到锁:
//原参数值1
//现参数值2
//thr2拿到锁:
//原参数值2
//现参数值1
//或
//thr2拿到锁:
//原参数值1
//现参数值0
//thr1拿到锁:
//原参数值0
//现参数值1
thr1.join();
thr2.join();
return 0;
}
unique_lock
1.于功能上unique_lock类似于lock_guard,但是unique_lock的功能更强大一些,用法上也更灵活一些,但是比lock_guard要慢一些,内存占用多一些;
2.与lock_guard不同的是,使用unique_lock后可以手动lock()与手动unlock();
3.unique_lock的参数有adopt_lock、try_to_lock、defer_lock
- adopt_lock:用std::adopt_lock的前提是,自己需要先把mutex lock上;
- try_to_lock:尝试加锁,尝试失败就立即返回,用这个try_to_lock的前提是你自己不能先lock
- defer_lock:初始化了一个没有加锁的mutex,用std::defer_lock的前提是,你不能自己先lock,否则会报异常
4. unique_lock的成员函数 lock()、unlock()、try_lock()、release()
lock():加锁
lunlock():解锁
try_lock():尝试给互斥量加锁,如果拿不到锁,返回false,如果拿到了锁,返回true,这个函数是不阻塞的
release():返回它所管理的mutex对象指针,并释放所有权;也就是说,这个unique_lock和mutex不再有关系。
5. unique_lock不可以复制,可以把所有权转移给其他锁(move)
例如:
unique_lock<mutex> g1(m,defer_lock);
unique_lock<mutex> g2(move(g1));//所有权转移,此时由g2来管理互斥量m
#include<iostream>
#include<thread>
#include <windows.h>
#include<mutex>
using namespace std;
mutex m;
int a = 1;
void fun1()
{
unique_lock<mutex> lockg1(m, defer_lock);//初始化一个没有加锁的mutex
lockg1.lock();//unique_lock加锁,不是m.lock();
cout << "fun1拿到m锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << ++a << endl;
//lockg1.unlock();
}
void fun2()
{
unique_lock<mutex> lockg2(m, try_to_lock);//尝试拿锁,只尝试一次,如果没拿到就返回,不会导致本线程一直阻塞在lock这里。
if (lockg2.owns_lock())
{
cout << "fun2拿到m锁:" << endl;
cout << "原参数值" << a << endl;
cout << "现参数值" << --a << endl;
}
else
{
cout << "fun2没有拿到m锁" << endl;
}
}
int main()
{
thread thr1(fun1);
thread thr2(fun2);
//下面两个线程同时加入,但是执行顺序可能thr1在前,也可能thr2在前
//所以执行结果也有多种:
/*fun2拿到m锁:
原参数值1
现参数值0
fun1拿到m锁:
原参数值0
现参数值1*/
//或
//fun2没有拿到m锁fun1拿到m锁:
//原参数值
//1
//现参数值2
//或
//fun1拿到m锁:fun2没有拿到m锁
//原参数值1
//现参数值2
thr2.join();
thr1.join();
return 0;
}
多线程交替打印奇偶数
#include<iostream>
#include<thread>
#include<mutex>
using namespace std;
bool flag = true;
mutex my_mutex;
condition_variable nv;
void printfodd()
{
unique_lock<mutex> my_guard(my_mutex);
for (int i = 0; i < 50; i++)
{
nv.wait(my_guard, []() {return flag == false ? true : false; });
flag = true;
cout << 2 * i + 1 << endl;
nv.notify_one();
}
my_guard.unlock();
}
void printfenev()
{
unique_lock<mutex> my_guard(my_mutex);
for (int i = 0; i < 50; i++)
{
nv.wait(my_guard, []() {return flag == true ? true : false; });
flag = false;
cout << 2 * i << endl;
nv.notify_one();
}
my_guard.unlock();
}
int main()
{
thread thread1(printfodd);
thread thread2(printfenev);
thread1.join();
thread2.join();
return 0;
}
3. condition_variable
condition_variable的头文件有两个variable类,一个是condition_variable,另一个是condition_variable_any。condition_variable必须结合unique_lock使用。condition_variable_any可以使用任何的锁。下面以condition_variable为例进行介绍。
condition_variable条件变量可以阻塞(wait、wait_for、wait_until)调用的线程直到使用(notify_one或notify_all)通知恢复为止。condition_variable是一个类,这个类既有构造函数也有析构函数,使用时需要构造对应的condition_variable对象,调用对象相应的函数来实现上面的功能。
类型 | 说明 |
---|---|
condition_variable | 构建对象 |
析构 | 删除 |
wait | Wait until notified |
wait_for | Wait for timeout or until notified |
wait_until | Wait until notified or time point |
notify_one | 解锁一个线程,如果有多个,则未知哪个线程执行 |
notify_all | 解锁所有线程 |
cv_status | 这是一个类,表示variable 的状态,如下所示 |
enum class cv_status { no_timeout, timeout };
3.1 wait
当前线程调用 wait() 后将被阻塞(此时当前线程应该获得了锁(mutex),不妨设获得锁 lck),直到另外某个线程调用 notify_* 唤醒了当前线程。
在线程被阻塞时,该函数会自动调用 lck.unlock() 释放锁,使得其他被阻塞在锁竞争上的线程得以继续执行。另外,一旦当前线程获得通知(notified,通常是另外某个线程调用 notify_* 唤醒了当前线程),wait()函数也是自动调用 lck.lock(),使得lck的状态和 wait 函数被调用时相同。
代码示例:
#include <iostream> // std::cout
#include <thread> // std::thread, std::this_thread::yield
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable
std::mutex mtx;
std::condition_variable cv;
int cargo = 0;
bool shipment_available() {return cargo!=0;}
void consume (int n) {
for (int i=0; i<n; ++i) {
std::unique_lock<std::mutex> lck(mtx);//自动上锁
//第二个参数为false才阻塞(wait),阻塞完即unlock,给其它线程资源
cv.wait(lck,shipment_available);
// consume:
std::cout << cargo << '\n';
cargo=0;
}
}
int main ()
{
std::thread consumer_thread (consume,10);
consumer_thread.join();
for (int i=0; i<10; ++i) {
//每次cargo每次为0才运行。
while (shipment_available()) std::this_thread::yield();
std::unique_lock<std::mutex> lck(mtx);
cargo = i+1;
cv.notify_one();
}
return 0;
}
说明:
主线程中的while,每次在cargo=0才运行。
每次cargo被置为0,会通知子线程unblock(非阻塞),也就是子线程可以继续往下执行。
子线程中cargo被置为0后,wait又一次启动等待。也就是说shipment_available为false,则等待。
3.2 wait_for
与std::condition_variable::wait() 类似,不过 wait_for可以指定一个时间段,在当前线程收到通知或者指定的时间 rel_time 超时之前,该线程都会处于阻塞状态。 而一旦超时或者收到了其他线程的通知,wait_for返回,剩下的处理步骤和 wait()类似。
template <class Rep, class Period>
cv_status wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time);
另外,wait_for 的重载版本的最后一个参数pred表示 wait_for的预测条件,只有当 pred条件为false时调用 wait()才会阻塞当前线程,并且在收到其他线程的通知后只有当 pred为 true时才会被解除阻塞。
template <class Rep, class Period, class Predicate>
bool wait_for (unique_lock<mutex>& lck,
const chrono::duration<Rep,Period>& rel_time, Predicate pred);
代码示例:
#include <iostream> // std::cout
#include <thread> // std::thread
#include <chrono> // std::chrono::seconds
#include <mutex> // std::mutex, std::unique_lock
#include <condition_variable> // std::condition_variable, std::cv_status
std::condition_variable cv;
int value;
void read_value() {
std::cin >> value;
cv.notify_one();
}
int main ()
{
std::cout << "Please, enter an integer (I'll be printing dots): \n";
std::thread th (read_value);
std::mutex mtx;
std::unique_lock<std::mutex> lck(mtx);
while (cv.wait_for(lck,std::chrono::seconds(1))==std::cv_status::timeout) {
std::cout << '.' << std::endl;
}
std::cout << "You entered: " << value << '\n';
th.join();
return 0;
}
通知或者超时都会解锁,所以主线程会一直打印。
示例中只要过去一秒,就会不断的打印。
4. 线程池
4.1 概念
在一个程序中,如果我们需要多次使用线程,这就意味着,需要多次的创建并销毁线程。而创建并销毁线程的过程势必会消耗内存,线程过多会带来调动的开销,进而影响缓存局部性和整体性能。
线程的创建并销毁有以下一些缺点:
创建太多线程,将会浪费一定的资源,有些线程未被充分使用。
销毁太多线程,将导致之后浪费时间再次创建它们。
创建线程太慢,将会导致长时间的等待,性能变差。
销毁线程太慢,导致其它线程资源饥饿。
线程池维护着多个线程,这避免了在处理短时间任务时,创建与销毁线程的代价。
4.2 线程池的实现
因为程序边运行边创建线程是比较耗时的,所以我们通过池化的思想:在程序开始运行前创建多个线程,这样,程序在运行时,只需要从线程池中拿来用就可以了.大大提高了程序运行效率.
一般线程池都会有以下几个部分构成:
线程池管理器(ThreadPoolManager):用于创建并管理线程池,也就是线程池类
工作线程(WorkThread): 线程池中线程
任务队列task: 用于存放没有处理的任务。提供一种缓冲机制。
append:用于添加任务的接口
线程池实现代码:
#ifndef _THREADPOOL_H
#define _THREADPOOL_H
#include <vector>
#include <queue>
#include <thread>
#include <iostream>
#include <stdexcept>
#include <condition_variable>
#include <memory> //unique_ptr
#include<assert.h>
const int MAX_THREADS = 1000; //最大线程数目
template <typename T>
class threadPool
{
public:
threadPool(int number = 1);//默认开一个线程
~threadPool();
std::queue<T *> tasks_queue; //任务队列
bool append(T *request);//往请求队列<task_queue>中添加任务<T *>
private:
//工作线程需要运行的函数,不断的从任务队列中取出并执行
static void *worker(void *arg);
void run();
private:
std::vector<std::thread> work_threads; //工作线程
std::mutex queue_mutex;
std::condition_variable condition; //必须与unique_lock配合使用
bool stop;
};//end class
//构造函数,创建线程
template <typename T>
threadPool<T>::threadPool(int number) : stop(false)
{
if (number <= 0 || number > MAX_THREADS)
throw std::exception();
for (int i = 0; i < number; i++)
{
std::cout << "created Thread num is : " << i <<std::endl;
work_threads.emplace_back(worker, this);//添加线程
//直接在容器尾部创建这个元素,省去了拷贝或移动元素的过程。
}
}
template <typename T>
inline threadPool<T>::~threadPool()
{
std::unique_lock<std::mutex> lock(queue_mutex);
stop = true;
condition.notify_all();
for (auto &ww : work_threads)
ww.join();//可以在析构函数中join
}
//添加任务
template <typename T>
bool threadPool<T>::append(T *request)
{
/*操作工作队列时一定要加锁,因为他被所有线程共享*/
queue_mutex.lock();//同一个类的锁
tasks_queue.push(request);
queue_mutex.unlock();
condition.notify_one(); //线程池添加进去了任务,自然要通知等待的线程
return true;
}
//单个线程
template <typename T>
void *threadPool<T>::worker(void *arg)
{
threadPool *pool = (threadPool *)arg;
pool->run();//线程运行
return pool;
}
template <typename T>
void threadPool<T>::run()
{
while (!stop)
{
std::unique_lock<std::mutex> lk(this->queue_mutex);
/* unique_lock() 出作用域会自动解锁 */
this->condition.wait(lk, [this] { return !this->tasks_queue.empty(); });
//如果任务为空,则wait,就停下来等待唤醒
//需要有任务,才启动该线程,不然就休眠
if (this->tasks_queue.empty())//任务为空,双重保障
{
assert(0&&"断了");//实际上不会运行到这一步,因为任务为空,wait就休眠了。
continue;
}
else
{
T *request = tasks_queue.front();
tasks_queue.pop();
if (request)//来任务了,开始执行
request->process();
}
}
}
#endif
说明:
构造函数创建所需要的线程数
一个线程对应一个任务,任务随时可能完成,线程则可能休眠,所以任务用队列queue实现(线程数量有限),线程用采用wait机制。
任务在不断的添加,有可能大于线程数,处于队首的任务先执行。
只有添加任务(append)后,才开启线程condition.notify_one()。
wait表示,任务为空时,则线程休眠,等待新任务的加入。
添加任务时需要添加锁,因为共享资源。
测试代码:
#include "mythread.h"
#include<string>
#include<math.h>
using namespace std;
class Task
{
public:
void process()
{
//cout << "run........." << endl;
//测试任务数量
long i=1000000;
while(i!=0)
{
int j = sqrt(i);
i--;
}
}
};
int main(void)
{
threadPool<Task> pool(6);//6个线程,vector
std::string str;
while (1)
{
Task *tt = new Task();
//使用智能指针
pool.append(tt);//不停的添加任务,任务是队列queue,因为只有固定的线程数
cout<<"添加的任务数量: "<<pool.tasks_queue.size()<<endl;;
delete tt;
}
}
参考:
原文链接:https://blog.csdn.net/QLeelq/article/details/115747717