八、多线程

八、多线程

8.1 多线程概念

  • **程序:**程序就是指令和数据的结合,是一个静态的概念,如果不运行的话,没有实际意义。

  • 进程:进程就是运行中的程序,此时它是动态的,它占用了操作系统的各种资源。一个程序可以包括多个进程。

  • 线程:线程是进程中的实际运行单元,是操作系统进行运算调度的最小单位。每个进程至少需要一个线程,当然也可以有多个,那就是多线程。

  • 多线程的理解:

    • 软件层面的多线程并不意味着所有的线程在同时执行,要实现真正的同时执行的多线程,需要硬件的支持,也就是说CPU有多个计算核心,
    • 或者有多个CPU,否则多线程也是在CPU中逐个去执行的,它的效率并不一定比单线程高。CPU的计算分成很多个小单元,叫做时间片,给线程使用。
    • 由于时间片很短,所以即使多个线程依此执行,看起来也像在同时执行。

8.2 thread类

  • 在C++11之前,没有多线程类库,所以如果想实现多线程编程,只能调用操作系统本身的多线接口,但是这种方式很麻烦,因为不同的操作系统接口不一样,
    而且不能跨平台。

  • C++11之后提供了thread类库,让我们方便地进行多线程编程,而且可以实现跨平台。需要引入头文件

    #include <thread>
    

thread的构造函数:

  • 1)thread t();默认的无参构造,构造一个空的线程对象,它没有关联任何可执行的函数,所以不会启动任何线程。

  • 2)thread t(func);这个线程对象关联了一个函数func,但它不会立刻执行,而是需要我们手动让它执行,可以选择两种执行方式:阻塞join和分离detach。

    • **阻塞执行:join()函数来完成阻塞执行,**阻塞就是在某处停下来,指的是当前的线程必须执行完毕,才能往下进行。比如说在main函数中我们构造一个线程t,
      main函数有自己的线程,它是父线程,而我们自己构造的线程t是子线程,子线程的控制权属于父线程,父线程可以杀掉它。如果父线程在子线程之前就
      提取结束了,这个时候不管子线程有没有结束,父线程都会杀掉它。如果子线程不想被杀掉,想一直执行到结束,那就可以使用join()函数阻塞父线程,
      这种情况下,父线程就会等待子线程结束后才会继续运行。
    • **分离执行:detach()函数实现,**即main函数的线程不会取得对子线程t的控制权,也就无法杀死它。它们各自独立执行。此时t进程可以称之为守护线程。
  • 3)**thread t(func,args1,args2…);这个线程关联了func函数,并且给func函数提供了参数args1,args2…**这种情况下,线程会马上开始执行。也需要让线程t
    选择阻塞还是分离的方式来执行。同上。

  • 4)移动构造函数:移动构造即在构造对象的时候,去掉拷贝的动作,选择直接移动,这样效率更高。对于线程对象来说,很适合做移动构造,因为线程是禁止拷贝的,不能用拷贝构造。
    **thread t(move(other));//将线程对象other的所有权转交给或者叫移动给t对象,之后other就消失了,**并且通过查看可以发现,other和t的线程id是一样的。

    //准备线程函数,给线程调用
    void fun1(int n)
    {
    	for (int i = 0; i < 5; i++)
    	{
    		cout << "fun1执行,参数是:" << n << endl;
    		//休眠一下,当前线程休息500毫秒ms
    		this_thread::sleep_for(chrono::milliseconds(500));
    	}
    }
    void fun2(int& n)
    {
    	for (int i = 0; i < 5; i++)
    	{
    		cout << "fun2执行,参数是:" << n << endl;
    		n++;
    		//休眠一下,当前线程休息500毫秒ms
    		this_thread::sleep_for(chrono::milliseconds(300));
    	}
    }
    
    void test01()
    {
    	//调试线程,使用线程的构造函数和成员函数
    	int n = 0;
    	thread t1;//无参构造,没有绑定函数也不会执行
    	cout << "t1=" << t1.get_id() << endl;//此时t1没有执行,所以没有id,此时输出的id=0
    	thread t2(fun1, n);//t2线程绑定了函数,并且给函数传了参数,马上开始执行
    	cout << "t2=" << t2.get_id() << endl;//t2开始运行了,所以有id
    	thread t3(fun2, ref(n));//由于fun2是引用传参,所以需要使用ref函数来传递
    	cout << "t3=" << t3.get_id() << endl;//t3开始运行了,所以有id
    	thread t4(move(t3));//移动构造,用到move函数,将t3转移给t4,t3就消失了,t4拥有了t3的id
    	cout << "t3=" << t3.get_id() << endl;//t3消失了,id变为0
    	cout << "t4=" << t4.get_id() << endl;//t4接管了t3,所以有t3的id
    	//为了保证子线程能执行完毕,需要选择阻塞或者分离的方式让它们去执行
    	t2.join();
    	t4.join();
    	//t2.detach();//换成分离执行后,观察输出的效果有什么不同
    	//t4.detach();
    	cout << "运行完毕后,n=" << n << endl;
    }
    
  • thread的其他常用函数:

    • get_id();获取线程id,每个线程都有一个独一无二的id号
    • join();阻塞执行,调用后会阻塞主线程,当该线程结束后,主线程才会继续执行。
    • detach();分离执行,主线程和子线程不汇合,各自执行,主线程没有对子线程的控制权。
    • joinable();判断一种状态,返回bool值,如果一个线程已经join或者detach执行了,那么返回的结果就是false,否则是true。
  • 休眠函数:
    即让程序停止执行一段时间。
    C++11之前没有专门的休眠函数,也是需要调用操作系统的休眠函数,比如window的休眠函数的时间单位是毫秒,Linux的时间单位是秒。
    C++11之后提供了专门的休眠函数,需要引入头文件#include
    this_thread::sleep_for(休眠时间); 让当前线程休眠一段时间,如:this_thread::sleep_for(chrono::milliseconds(500));休眠500毫秒

//演示阻塞join的用法
void test02()
{
	thread t5(fun3);//t5不会立刻执行,没有给fun3传参,需要手动执行
	t5.join();
	cout << "主线程等子线程执行完毕后,再继续执行" << endl;
	for (int i = 10; i < 15; i++)
	{
		cout << "这是主线程打印的:" << i << endl;
	}
}
//演示分离detech的用法
void test03()
{
	thread t6(fun3);//t6不会执行
	t6.detach();//分离的方式执行,观察输出效果,主线程不会等待子线程执行完毕,而是各自分开执行,主线程结束后,输出窗口就不可输出了,但子线程仍然执行到完毕
	cout << "主线程正常执行" << endl;
	for (int i = 10; i < 15; i++)
	{
		cout << "这是主线程打印的:" << i << endl;
	}
}
//joinable的用法
void test04()
{
	thread t7(fun4);
	cout << "joinable=" << t7.joinable() << endl;//返回1,true
	t7.join();
	cout << "joinable=" << t7.joinable() << endl;//返回0,false
	//thread对象析构的时候,会判断joinable的状态,如果当前为true,就会调用一个terminate()函数结束线程。
	//所以当一个线程对象绑定了函数执行后,既没有使用join也没有使用detech的方式,那么joinable就为true,就会被异常结束掉。
}

8.3多线程应用

  • 简单总结:
    ​ 1)先创建的子线程不一定跑的最快(多线程运行有偶然性)
    ​ 2)线程绑定的函数返回后,线程也将终止,id变为0
    ​ 3)如果主线程先退出,那么它旗下所有的子线程也将被全部终止,除非我们使用阻塞或者分离的方式执行。

8.4线程互斥

  • 在多线程环境中运行的代码段,有些代码是存在竞争关系的,我们叫做竞态条件。比如说两个线程都去对一个资源(例如一个变量)进行操作,这时候就存在竞争关系,我们无法保证这个资源的安全有效。这样代码段不是线程安全的,不应该运行在多线程环境下。这样的代码段称为关键代码段或者临界区代码段。

  • 为了解决上面的线程安全问题,我们应该禁止多个线程同时操作同一个资源,而是保证某个线程代码段执行前,独占这个资源,等它执行完毕后,再释放资源,
    给别的线程代码段执行。这样的操作就是线程互斥。

  • 线程互斥通过锁机制来实现,需要引入头文件**#include **

    • 上锁:lock(),使用前先上锁,保证自己独占资源
    • 解锁:unlock(),使用后解锁,资源可以再给别人使用
    //线程互斥
    void fun6()//加上互斥锁,保证每个线程都会独享num变量,这样就是线程安全的
    {
    	//用num变量之前,先加锁,保证只有自己在用
    	num_lock.lock();
    	for (int i = 0; i < 5; i++)
    	{
    		cout << "线程" << this_thread::get_id() << "给num+1,num=" << ++num << endl;
    	}
    	num_lock.unlock();//用完之后,解锁,让别的线程去用
    }
    void test06()//加上互斥锁
    {
    	thread t8(fun6);
    	thread t9(fun6);
    	t8.join();
    	t9.join();
    	//可见,加了锁之后,两个线程都可以保证独立使用num变量,完成自己的全部5次循环操作。
    }
    
  • 除了上面的锁机制完成线程互斥,保证线程安全之外,还有一种更加轻量级(效率更高)的方式也可以实现线程安全,通过CAS原子操作类来实现,

  • 需要引入头文件**#include **,这个类库提供了很多基于原子操作的数据类型,可以从根本上保证数据的线程安全。
    原子操作就是指不可再分的操作,一个操作要完整的执行完毕,不能被分隔或打断。

//在多线程大量操作下,int类型是不安全的,可能会出错。但是原子整型没问题,可以保证安全可靠。
void fun_int()//普通类型,多线程下,不安全
{
	for (int i = 0; i < 10000; i++)
	{
		num++;
	}
}
void fun_atomic()//原子类型,多线程下,安全可靠
{
	for (int i = 0; i < 10000; i++)
	{
		sum++;
	}
}
void test07()//使用int,多线程大量操作,可能出错
{
	//模拟一个大量运算的场景,启动多个线程
	//创建100个线程,写到循环中,放到容器中,逐个启动,共运行100万次加法
	vector<thread> vec;
	for (int i = 0; i < 100; i++)
	{
		vec.push_back(thread(fun_int));
	}
	for (int i = 0; i < 100; i++)
	{
		vec[i].join();
	}
	cout << "num=" << num << endl;//结果应该是一百万,但有可能出错
}
void test08()//使用原子整型,多线程大量操作,安全可靠
{
	//模拟一个大量运算的场景,启动多个线程
	//创建100个线程,写到循环中,放到容器中,逐个启动,共运行100万次加法
	vector<thread> vec;
	for (int i = 0; i < 100; i++)
	{
		vec.push_back(thread(fun_atomic));
	}
	for (int i = 0; i < 100; i++)
	{
		vec[i].join();
	}
	cout << "sum=" << sum << endl;//结果应该是一百万,不会出错
}
  • lock_guard 与 unique_lock

std::lock_guard`是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。

std::lock_guard 的特点如下:

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

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

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

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

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

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

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

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

8.5线程同步

  • 多个线程在运行时,都随着操作系统的调度来运行,它们之间没有顺序和规律可言。但是在某些情况下,我们需要线程之间打配合,
    ​ 比如说一个线程需要等待另外一个线程的执行结果,才能继续执行,这就是线程之间的同步通信机制。

  • 线程同步需要用到条件变量,作为线程之间传递消息的中介。

  • 条件变量需要引入类库**#include <condition_variable>**,同时要结合互斥锁一起使用。

    std::condition_variable 的步骤如下:

    1. 创建一个 std::condition_variable 对象。

    2. 创建一个互斥锁 std::mutex 对象,用来保护共享资源的访问。

    3. 在需要等待条件变量的地方

      使用 std::unique_lock<std::mutex> 对象锁定互斥锁

      并调用 std::condition_variable::wait()std::condition_variable::wait_for()std::condition_variable::wait_until() 函数等待条件变量。

    4. 在其他线程中需要通知等待的线程时,调用 std::condition_variable::notify_one()std::condition_variable::notify_all() 函数通知等待的线程。

线程同步案例:
写一个经典的线程同步案例:生产者-消费者模型,见代码(如下)

//生产者线程调用的函数
void producter()
{
	//一共生产10个产品
	for (int i = 0; i < 10; i++)
	{
		//生产者每生产一个就通知消费者去消费一个,消费者每消费一个就通知生产者去生产一个。
		//用到互斥锁的更加封装的用法,用到了unique_lock<mutex>类
		unique_lock<mutex> lock(g_mtx);//完成了上锁,待会还会自动解锁
		//作为生产者,首先要判断容器中有没有产品,没有产品才需要生产,有产品就不需要生产,而是通知消费者去消费
		while (!g_vector.empty())//容器不为空,不用生产,通知消费者去消费
		{
			//通知需要用到条件变量
			g_convar.wait(lock);//这个wait做了几件事:1.释放锁。2.等待消费者发来的信号。3.阻塞在这里
		}
		//如果容器为空,就需要生产
		g_vector.push_back(i);
		cout << "生产者生产了产品:" << i << endl;
		//接下来通知消费者去消费
		g_convar.notify_all();//通知所有等待的消费者可以消费了
		this_thread::sleep_for(chrono::milliseconds(300));//休眠100ms
	}
}

//消费者线程调用的函数
void consumer()
{
	//一共消费10个产品
	for (int i = 0; i < 10; i++)
	{
		//消费者每消费一个就通知生产者去生产一个。
		//用到互斥锁的更加封装的用法,用到了unique_lock<mutex>类
		unique_lock<mutex> lock(g_mtx);//完成了上锁,待会还会自动解锁
		//作为消费者,首先要判断容器中有没有产品,没有产品就需要等待,有产品就去消费,消费完了别忘了通知生产者继续生产
		while (g_vector.empty())//容器为空,不能消费,需要释放锁,阻塞,等待生产者发来通知
		{
			//通知需要用到条件变量
			g_convar.wait(lock);//这个wait做了几件事:1.释放锁。2.等待消费者发来的信号。3.阻塞在这里
		}
		//如果容器不为空,就可以消费了
		int p_num = g_vector.back();//先拿到这个产品,一会用于输出
		g_vector.pop_back();//消费了一个产品,从容器中删除
		cout << "消费者者消费产品:" << p_num << endl;
		//接下来通知生产者去生产
		g_convar.notify_all();//通知所有等待的生产者
		this_thread::sleep_for(chrono::milliseconds(300));//休眠100ms
	}
}
void test09()//生产者消费者模型,线程同步
{
	thread t_p(producter);//生产者线程
	thread t_c(consumer);//消费者线程
	t_p.join();
	t_c.join();
}

**建议:**多线程能不使用的情况下就不要使用,用不好容易出问题,而且多线程不一定比单线程效率高。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值