C++线程间的互斥和通信场景分析

互斥锁(mutex)

为了更好地理解,互斥锁,我们可以首先来看这么一个应用场景:模拟车站卖票。

模拟车站卖票

场景说明:
Yang车站售卖从亚特兰蒂斯到古巴比伦的时光飞船票;因为机会难得,所以票数有限,一经发售,谢绝补票。
飞船票总数:100张;
售卖窗口:3个。
对于珍贵的飞船票来说,这个资源是互斥的,比如第100张票,只能卖给一个人,不可能同时卖给两个人。3个窗口都有权限去售卖飞船票(唯一合法途径)。

不加锁的结果

根据场景说明,我们可以很快地分析如下:
可以使用三个线程来模拟三个独立的窗口同时进行卖票;
定义一个全局变量,每当一个窗口卖出一张票,就对这个变量进行减减操作。
故写出如下代码:

#include <iostream>
#include <thread>
#include <list>
using namespace std;

int tickets = 100; // 车站剩余票数总数

void sellTickets(int win)
{
	while (tickets > 0)
	{
		{
			if (tickets > 0)
			{
				cout << "窗口:" << win << " 卖出了第:" << tickets << "张票!" << endl;
				tickets--;
			}
			std::this_thread::sleep_for(std::chrono::microseconds(400));
		}
	}
}

int main()
{
	list<std::thread> tlist;
	for (int i = 1; i <= 3; ++i)
	{
		tlist.push_back(std::thread(sellTickets, i));
	}
	for (std::thread& t : tlist)
	{
		t.join();
	}
	cout << "所有窗口卖票结束!" << endl;

	return 0;
}

运行结果如下:

通过运行,我们可以发现问题:
对于一张票来说,卖出去了多次!
这不白嫖吗???这合适吗?
原因也很简单,对于线程来说,谁先执行,谁后执行,完全是根据CPU的调度,根本不可能掌握清楚。
所以,这个代码是线程不安全的!
那,怎么解决呢?
当然是:互斥锁了!

加锁后的结果

我们对上述代码做出如下修改:

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;

int tickets = 100;
std::mutex mtx;

void sellTickets(int win)
{
	while (tickets > 0)
	{
		{
			lock_guard<std::mutex> lock(mtx);
			if (tickets > 0)
			{
				cout << "窗口:" << win << " 卖出了第:" << tickets << "张票!" << endl;
				tickets--;
			}
			std::this_thread::sleep_for(std::chrono::microseconds(400));
		}
	}
}

int main()
{
	list<std::thread> tlist;
	for (int i = 1; i <= 3; ++i)
	{
		tlist.push_back(std::thread(sellTickets, i));
	}
	for (std::thread& t : tlist)
	{
		t.join();
	}
	cout << "所有窗口卖票结束!" << endl;

	return 0;
}

首先定义了一个全局的互斥锁std::mutex mtx;接着在对票数tickets进行减减操作时,定义了lock_guard,这个就相当于智能指针scoped_ptr一样,可以出了作用域自动释放锁资源。
运行结果如下:

我们可以看到这一次,就没问题了。

简单总结

互斥锁的使用可以有三种:
(首先都需要在全局定义互斥锁std::mutex mtx

  • 首先可以直接在需要加锁和解锁的地方,手动进行:加锁mtx.lock()、解锁mtx.unlock()
  • 可以在需要加锁的地方定义保护锁:lock_guard<std::mutex> lock(mtx),这个锁在定义的时候自动上锁,出了作用域自动解锁。(其实就是借助了智能指针的思想,定义对象出调用构造函数底层调用lock(),出了作用域调用析构函数底层调用unlock());
  • 可以在需要加锁的地方定义唯一锁:unique_lock<std::mutex> lock(mtx),这个锁和保护锁类似,但是比保护锁更加好用。(可以类比智能指针中的scoped_ptrunique_ptr的区别,二者都是将拷贝构造和赋值重载函数删除了,但是unique_ptrunique_lock都定义了带有右值引用的拷贝构造和赋值)

条件变量(conditon_variable)

如果说,互斥锁是为了解决线程间互斥的问题,那么,条件变量就是为了解决线程间通信的问题。
同样的,我们可以首先来看一个问题(模型):

生产者消费者线程模型

生产者消费者线程模型是一个很经典的线程模型;
首先会有两个线程,一个是生产者,一个是消费者,生产者只负责生产资源,消费者只负责消费资源。

产生问题

根据上述互斥锁的理解,我们可以写出如下代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
using namespace std;

std::mutex mtx; 

class Queue
{
public:
	void put(int num)
	{
		lock_guard<std::mutex> lock(mtx);
		que.push(num);
		cout << "生产者,生产了:" << num << "号产品" << endl;
	}
	void get()
	{
		lock_guard<std::mutex> lock(mtx);
		int val = que.front();
		que.pop();
		cout << "消费者,消费了:" << val << "号产品" << endl;
	}
private:
	queue<int> que;
};

void producer(Queue* que)
{
	for (int i = 0; i < 10; ++i)
	{
		que->put(i);
		std::this_thread::sleep_for(std::chrono::milliseconds(200));
	}
}

void consumer(Queue* que)
{
	for (int i = 0; i < 10; ++i)
	{
		que->get();
		std::this_thread::sleep_for(std::chrono::milliseconds(200));
	}
}
int main()
{
	Queue que;

	std::thread t1(producer, &que);
	std::thread t2(consumer, &que);

	t1.join();
	t2.join();

	return 0;
}

同样的,我们定义了两个线程:t1t2分别作为生产者消费者,并且定义了两个线程函数producerconsumer,这两个函数接受一个Queue*的参数,并且通过这个指针调用putget方法,这两个方法就是往资源队列里面执行入队和出队操作。
运行结果如下:

我们会发现,出错了。
多运行几次试试:


我们发现,每次运行的结果还都不一样,但是都会出现系统崩溃的问题。
仔细来看这个错误原因:

我们再想想这个代码的逻辑:
一个生产者只负责生产;
一个消费者只负责消费;
他们共同在队列里面存取资源;
存取资源操作本身是互斥的。
发现问题了吗?
这两个线程之间彼此的操作独立,换句话说,
没有通信!
生产者生产的时候,消费者不知道;
消费者消费的时候,生产者也不知道;
但是消费者是要从队列里面取资源的,如果某一个时刻,队列里为空了,它就不能取了!

解决问题

分析完问题之后,我们知道了:
问题出在:没有通信上面。
那么如何解决通信问题呢?
当然就是:条件变量了!
我们做出如下代码的修改:

#include <iostream>
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
using namespace std;

std::mutex mtx; // 互斥锁,用于线程间互斥
std::condition_variable cv;// 条件变量,用于线程间通信

class Queue
{
public:
	void put(int num)
	{
		unique_lock<std::mutex> lck(mtx);
		while (!que.empty())
		{
			cv.wait(lck);
		}
		que.push(num);
		cv.notify_all();
		cout << "生产者,生产了:" << num << "号产品" << endl;
	}
	void get()
	{
		unique_lock<std::mutex> lck(mtx);
		while (que.empty())
		{
			cv.wait(lck);
		}
		int val = que.front();
		que.pop();
		cv.notify_all();
		cout << "消费者,消费了:" << val << "号产品" << endl;
	}
private:
	queue<int> que;
};

void producer(Queue* que)
{
	for (int i = 0; i < 10; ++i)
	{
		que->put(i);
		std::this_thread::sleep_for(std::chrono::milliseconds(200));
	}
}

void consumer(Queue* que)
{
	for (int i = 0; i < 10; ++i)
	{
		que->get();
		std::this_thread::sleep_for(std::chrono::milliseconds(200));
	}
}
int main()
{
	Queue que;

	std::thread t1(producer, &que);
	std::thread t2(consumer, &que);

	t1.join();
	t2.join();

	return 0;
}

这个时候我们再来看运行结果:

这个时候就是:
生产一个、消费一个。

原子类型(atomic)

我们前面遇到线程不安全的问题,主要是因为涉及++--操作的时候,有可能被其他的线程干扰,所以使用了互斥锁
只允许得到的线程进行操作;
其他没有得到的线程只能眼巴巴的干看着。
但是,对于互斥锁来说,它是比较重的,它对于临界区代码做的事情比较复杂。
简单来说,如果只是为了++--这样的简单操作互斥的话,使用互斥锁,就有点杀鸡用牛刀的意味了。
那么有没有比互斥锁更加轻量的,并且能够解决问题的呢?
当然有,就是我们要说的原子类型

简单使用

我们可以简单设置一个场景:
定义十个线程,对一个公有的变量myCount进行task的操作,该操作是对变量进行100次的++
所以,如果顺利,我们会最终得到myCount = 1000
代码如下:

#include <iostream>
#include <thread>
#include <atomic>
#include <list>

volatile std::atomic_bool isReady = false;
volatile std::atomic_int myCount = 0;

void task()
{
	while (!isReady)
	{
		// 线程让出当前的CPU时间片,等待下一次调度
		std::this_thread::yield();
	}

	for (int i = 0; i < 100; ++i)
	{
		myCount++;
	}
}

int main()
{
	std::list<std::thread> tlist;
	for (int i = 0; i < 10; ++i)
	{
		tlist.push_back(std::thread(task));
	}

	std::this_thread::sleep_for(std::chrono::milliseconds(200));
	isReady = true;

	for (std::thread& it : tlist)
	{
		it.join();
	}
	std::cout << "myCount:" << myCount << std::endl;

	return 0;
}

运行结果如下:

改良车站卖票

对于原子类型来说,使用方法非常简单:
首先包含头文件:#include <atomic>
接着把需要原子操作的变量定义为对应的原子类型就好:
bool -> atomic_bool;
int -> atomic_int;
其他同理。
理解了这个以后,我们可以使用原子类型对我们的车站卖票进行改良:

#include <iostream>
#include <thread>
#include <list>
#include <mutex>
#include <atomic>
using namespace std;

std::atomic_int tickets = 100; // 车站剩余票数总数

void sellTickets(int win)
{
	while (tickets > 0)
	{
		tickets--;
		cout << "窗口:" << win << " 卖出了第:" << tickets << "张票!" << endl;
	}
}

int main()
{
	list<std::thread> tlist;
	for (int i = 1; i <= 3; ++i)
	{
		tlist.push_back(std::thread(sellTickets, i));
	}
	for (std::thread& t : tlist)
	{
		t.join();
	}
	cout << "所有窗口卖票结束!" << endl;

	return 0;
}

可以看到,从代码长度来说就轻量了很多!
运行结果如下:

虽然还有部分打印乱序的情况:
(毕竟线程的执行顺序谁也摸不清 😦 )
但是,代码的逻辑没有问题!
不会出现一张票被卖了多次的情况!
这个原子类型也被叫做:无锁类型,像是一些无锁队列之类的实现,就是靠的这个东西。

以上就是C++线程间的互斥和通信的详细内容,更多关于C++线程间通信的资料请关注我们其它相关文章!

(0)

相关推荐

  • C++11中多线程编程-std::async的深入讲解

    前言 C++11中提供了异步线程接口std::async,std::async是异步编程的高级封装,相对于直接使用std::thread,std::async的优势在于: 1.std::async会自动创建线程去调用线程函数,相对于低层次的std::thread,使用起来非常方便: 2.std::async返回std::future对象,通过返回的std::future对象我们可以非常方便的获取到线程函数的返回结果: 3.std::async提供了线程的创建策略,可以指定同步或者异步的方式去创建

  • c++11多线程编程之std::async的介绍与实例

    本节讨论下在C++11中怎样使用std::async来执行异步task. C++11中引入了std::async 什么是std::async std::async()是一个接受回调(函数或函数对象)作为参数的函数模板,并有可能异步执行它们. template<class Fn, class... Args> future<typename result_of<Fn(Args...)>::type> async(launch policy, Fn&& fn

  • C++11 简单实现线程池的方法

    什么是线程池 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中.如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙.如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值.超过最大值的线程可以排队,但他们要等到其他线程完成后才启动. 不使用

  • c++多线程为何要使用条件变量详解

    先看示例1: #include <iostream> #include <windows.h> #include <mutex> #include<deque> #include <thread> using namespace std; int nmax = 20; std::deque<int> m_que; std::mutex mymutex; //生产者 void producterex() { int i = 1; whi

  • c++11 多线程编程——如何实现线程安全队列

    线程安全队列的接口文件如下: #include <memory> template<typename T> class threadsafe_queue { public: threadsafe_queue(); threadsafe_queue(const threadsafe_queue&); threadsafe_queue& operator=(const threadsafe_queue&) = delete; void push(T new_va

  • c++ 单线程实现同时监听多个端口

    前言 多年前开发了一套网络库,底层实现采用IOCP(完成端口).该库已在公司多个程序中应用:经过多次修改,长时间检验,已经非常稳定高效. 最近把以前的代码梳理了一下,又加进了一些新的思路.代码结构更加合理,性能也有所提升.打算将该库一些的知识点写出来,以供参考. 服务端要在多个端口监听,这种场合并不多见.但作为一个完善的网络库,似乎有必要支持此功能的. 传统实现方法 如果监听端口个数很少,也可以采用传统的方法.因为accept函数是阻塞的,所以要实现在n个端口监听,就需要n个线程.如果监听端口个

  • 详解C++11 线程休眠函数

    C++ 11之前并未提供专门的休眠函数.c语言的sleep.usleep其实都是系统提供的函数,不同的系统函数的功能还有些差异. 在Windows系统中,sleep的参数是毫秒. sleep(2*1000); //sleep for 2 seconds 在类Unix系统中,sleep()函数的单位是秒. sleep(2); //sleep for 2 seconds 从C++11开始,中C++标准库提供了专门的线程休眠函数,使得你的代码可以独立于不同的平台. std::this_thread::

  • 浅谈c++11线程的互斥量

    为什么需要互斥量 在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源.这个过程有点类似于,公司部门里,我在使用着打印机打印东西的同时(还没有打印完),别人刚好也在此刻使用打印机打印东西,如果不做任何处理的话,打印出来的东西肯定是错乱的. #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> #include <chrono> #include <thread>

  • C++线程间的互斥和通信场景分析

    互斥锁(mutex) 为了更好地理解,互斥锁,我们可以首先来看这么一个应用场景:模拟车站卖票. 模拟车站卖票 场景说明: Yang车站售卖从亚特兰蒂斯到古巴比伦的时光飞船票:因为机会难得,所以票数有限,一经发售,谢绝补票. 飞船票总数:100张: 售卖窗口:3个. 对于珍贵的飞船票来说,这个资源是互斥的,比如第100张票,只能卖给一个人,不可能同时卖给两个人.3个窗口都有权限去售卖飞船票(唯一合法途径). 不加锁的结果 根据场景说明,我们可以很快地分析如下: 可以使用三个线程来模拟三个独立的窗口

  • C++详细分析线程间的同步通信

    目录 1.多线程编程两个问题 1.1.线程间的互斥 1.2.线程间的同步通信 2.生产者-消费者线程模型 3.lock_gard和unique_lock 4.流程分析 1.多线程编程两个问题 1.1.线程间的互斥 竞态条件: 多线程执行的结果是一致的,不会随着CPU对线程不同的调用顺序,而产生不同的运行结果. 发生竞态条件的代码段,称为临界区代码段(只有一个线程可以进来),保证临界区代码段原子操作,通过线程互斥锁mutex,也可以使用轻量级的无锁实现CAS. C++11的mutex底层实现: 使

  • Java并发编程之线程间的通信

    一.概念简介 1.线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题,通常使用互斥锁来控制该逻辑.但是在还有这样一类场景,任务执行是有顺序控制的,例如常见的报表数据生成: 启动数据分析任务,生成报表数据: 报表数据存入指定位置数据容器: 通知数据搬运任务,把数据写入报表库: 该场景在相对复杂的系统中非常常见,如果基于多线程来描述该过程,则需要线程之间通信协作,才能有条不紊的处理该场景业务. 2.等待通知机制 如上的业务场景,

  • Java编程之多线程死锁与线程间通信简单实现代码

    死锁定义 死锁是指两个或者多个线程被永久阻塞的一种局面,产生的前提是要有两个或两个以上的线程,并且来操作两个或者多个以上的共同资源:我的理解是用两个线程来举例,现有线程A和B同时操作两个共同资源a和b,A操作a的时候上锁LockA,继续执行的时候,A还需要LockB进行下面的操作,这个时候b资源在被B线程操作,刚好被上了锁LockB,假如此时线程B刚好释放了LockB则没有问题,但没有释放LockB锁的时候,线程A和B形成了对LockB锁资源的争夺,从而造成阻塞,形成死锁:具体其死锁代码如下:

  • 深入解析Java的线程同步以及线程间通信

    Java线程同步 当两个或两个以上的线程需要共享资源,它们需要某种方法来确定资源在某一刻仅被一个线程占用.达到此目的的过程叫做同步(synchronization).像你所看到的,Java为此提供了独特的,语言水平上的支持. 同步的关键是管程(也叫信号量semaphore)的概念.管程是一个互斥独占锁定的对象,或称互斥体(mutex).在给定的时间,仅有一个线程可以获得管程.当一个线程需要锁定,它必须进入管程.所有其他的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程.这些其他的线程被

  • 浅析iOS应用开发中线程间的通信与线程安全问题

    线程间的通信   简单说明 线程间通信:在1个进程中,线程往往不是孤立存在的,多个线程之间需要经常进行通信   线程间通信的体现 1个线程传递数据给另1个线程 在1个线程中执行完特定任务后,转到另1个线程继续执行任务   线程间通信常用方法 复制代码 代码如下: - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait; - (void)performSelecto

  • java线程间通信的通俗解释及代码示例

    线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度). 进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统.以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制.这样进程间的通信就不局限于单台计算机了,实现了网络通信.线程通信主要分为以下几个部分,下面通过生活中图书馆借书的例子简单讲解以下: 通过共享对象通信 加入图书馆只有

  • Java编程线程间通信与信号量代码示例

    1.信号量Semaphore 先说说Semaphore,Semaphore可以控制某个资源可被同时访问的个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可.一般用于控制并发线程数,及线程间互斥.另外重入锁ReentrantLock也可以实现该功能,但实现上要复杂些. 功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了.另外等待的

  • Android线程间通信 Handler使用详解

    目录 前言 01.定义 02.使用 第一步.创建 第二步.发送消息 第一种是 post(Runnable) 第二种是 sendMessage(Message) 第三步.处理消息 03.结语 前言 Handler,可谓是面试题中的一个霸主了.在我<面试回忆录>中,几乎没有哪家公司,在面试的时候是不问这个问题的.简单一点,问问使用流程,内存泄漏等问题.复杂一点,纠其源码细节和底层 epoll 机制来盘你.所以其重要性,不言而喻了吧. 那么今天让我们来揭开 Handler 的神秘面纱.为了读者轻松易

  • 浅谈Java线程间通信之wait/notify

    Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.先来我们来看下相关定义: wait() :调用该方法的线程进入WATTING状态,只有等待另外线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁. wait(long):超时等待最多long毫秒,如果没有通知就超时返回. notify() :通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提

随机推荐