C++多线程

线程创建

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构建对象
析构删除
waitWait until notified
wait_forWait for timeout or until notified
wait_untilWait 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

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值