C++——多线程编程:<condition_variable> && <atomic>,同步与生产者消费者模型

一、condition_variable

条件变量是线程同步的一种方式,为线程之间的协同提供了一种媒介。顾名思义,这是一个描述条件的变量,条件有两种状态,一种是满足,一种是不满足。这种设计使得线程可以在条件满足时工作,而在条件不满足时等待。多个线程以一个条件为准则而执行。

A condition variable is an object able to block the calling thread until notified to resume.
条件变量是一个能阻塞线程的对象,直到线程被通知时恢复
Objects of type condition_variable always use unique_lock to wait: for an alternative that works with any kind of lockable type, see condition_variable_any
condition_variable对象在wait时总是需要unique_lock,如果需要用其他任意有锁机制的类型来代替unique_lock,可以看condition_variable_any

std::condition_variable

condition_variable(); // 无参构造
condition_variable (const condition_variable&) = delete; // 禁止拷贝构造

描述一种条件的对象不允许被拷贝。

成员函数

void wait(unique_lock<mutex>& _Lck);
template <class Predicate>
void wait (unique_lock<mutex>& lck, Predicate pred);

cv_status wait_for(unique_lock<mutex>& _Lck, const chrono::duration<_Rep, _Period>& _Rel_time);
cv_status wait_until(unique_lock<mutex>& _Lck, const chrono::time_point<_Clock, _Duration>& _Abs_time);

void notify_one();
void notify_all();

wait

第一个版本
参数_lck是一个锁管理器的引用。调用wait的时候需要把锁解锁交出去,以让其他线程可以获得锁的使用权。等到当前线程因为等待的条件满足而被唤醒时自动重新获得锁,就不会造成我被唤醒了但是我获取不到锁的情况。
在调用wait的时候,会自动调用传进去的锁的unlock,这就是为什么要使用unique_lock而不是lock_guard

第二个版本

If pred is specified, the function only blocks if pred returns false, and notifications can only unblock the thread when it becomes true (which is specially useful to check against spurious wake-up calls).
用于检查虚假唤醒的情况:如果pred被指定,那只有在pred为false的时候才会阻塞,只有在pred为true的时候才能被唤醒
这种调用等价于:while(pred == false) cond.wait(_lck);

wait_for

wait_for同样有两个版本,一个有pred,一个没有。
没有pred的版本,返回类型是cv_status,是一个enum,表示等待是否超时,用于判断是否执行其他任务

{
    // 伪代码
  std::thread th;
  std::unique_lock<std::mutex> lck;

  while (std::cv_status::timeout == cond.wait_for(lck, std::chrono::seconds(1))) 
  {
    // 如果超时,否则跳出while
  }
}

而如果是pred的版本,返回值则是bool,始终取决于pred的返回值,而跟函数调用无关系,因为函数调用成功或失败的先决条件都是pred。

wait_until

第二个参数需要传入的是某个时间点std::chrono::time_point。

notify_one

从条件变量的等待队列中唤醒一个线程,如果有多个线程,则随机唤醒一个。

notify_all

从条件变量的等待队列中唤醒所有的线程。

std::condition_variable_any

与condition_variable不同的是,any的wait函数不需要指定unique_lock,而是其他任意具有锁机制的类型。

有关wait函数和notify函数的用法,请参考《C++ 多线程同步condition_variable的用法》,只有condition_variable_any的wait阻塞函数与condition_variable的wait阻塞函数不同,将第一个参数unique_lock<mutex>替换为mutex即可,其他都使用方法一样。

这玩意用得少,实际使用也跟condition_variable差不多,在此我就不做介绍了。

小练习

到这,如果要实现一个生产者消费者模型,多个线程对一个变量做线程安全的加加和减减操作。你会用mutex控制互斥,保证消费对象数据的安全,因为加减操作都是至少需要三条汇编语句,所以需要加锁控制;用condition_variable控制同步,保证数据的操作要以某个条件为准则,因为可能这个变量不允许越过某个条件。

简易的生产者消费者模型

逻辑是:编写一个Task类,用于执行任务(可以重载operator()),目的是为了把任务和队列的实现解耦。编写一个BlockQueue类,阻塞队列,即带访问控制的生产和消费的媒介。在外部可以调用push和pop,同步互斥地访问这个队列。

class Task
{
public:
	static int m_count;

	virtual void consume()
	{
		m_count--;
	}

	virtual void produce()
	{
		m_count++;
	}
};
int Task::m_count = 0;

class Task
{
public:
	~Task() {};
	static int m_count;
    // 获取单例
	static std::shared_ptr<Task>& GetInstance()
	{
		static std::shared_ptr<Task> m_task(new Task());
		return m_task;
	}

	virtual void consume()
	{
		m_count--;
	}

	virtual void produce()
	{
		m_count++;
	}
};
int Task::m_count = 0;

如上是一个任务类,即生产和消费的对象。生产和消费定义成虚函数,以便扩展。

class BlockQueue
{
private:

	size_t m_capacity;

	std::mutex m_mutex;
	std::queue<Task> m_queue;
	std::condition_variable m_consumer;
	std::condition_variable m_producer;

private:

	void producer_wait(std::unique_lock<std::mutex>& _lock)
	{
		m_producer.wait(_lock);
	}

	void consumer_wait(std::unique_lock<std::mutex>& _lock)
	{
		m_consumer.wait(_lock);
	}

	void producer_awake()
	{
		m_producer.notify_one();
	}

	void consumer_awake()
	{
		m_consumer.notify_one();
	}

	bool empty()
	{
		return m_queue.empty();
	}

	bool full()
	{
		return m_queue.size() == m_capacity;
	}

public:
	BlockQueue(size_t _capacity)
		:m_capacity(_capacity)
	{}

	void push(const Task& _in)
	{
		std::unique_lock<std::mutex> _lock(m_mutex);
        // 如果满了,生产者应该阻塞等待
		while (full() == true)
		{
			std::cout << "Full !" << std::this_thread::get_id() << ' ' << m_queue.size() << std::endl;
			producer_wait(_lock);
		}
		m_queue.push(_in);
		Task::GetInstance()->produce(); // 非满,可以生产,并唤醒消费者
		consumer_awake();
		_lock.unlock();
		std::cout << "Push: " << std::this_thread::get_id() << ": m_count = " << Task::m_count << std::endl;
	}

	void pop()
	{
		std::unique_lock<std::mutex> _lock(m_mutex);
		while (empty() == true)
		{
			std::cout << "Empty !" << std::this_thread::get_id() << std::endl;
			consumer_wait(_lock);
		}
		m_queue.pop();
		Task::GetInstance()->consume();
		producer_awake();
		_lock.unlock();
		std::cout << "Pop: " << std::this_thread::get_id() << ": m_count = " << Task::m_count << std::endl;
	}
};
int main()
{
    // 指定容量,创建生产者和消费者,分配任务
	BlockQueue bq(100);
	std::vector<std::thread> Producers(2);
	std::vector<std::thread> Consumers(2);

	for (auto& Producer : Producers)
	{
		Producer = std::thread([&bq]() {
			while (true)
			{
				bq.push(Task());
			}
		});
		Producer.detach();
	}

	for (auto& Consumer : Consumers)
	{
		Consumer = std::thread([&bq]() {
			while (true)
			{
				bq.pop();
			}
		});
		Consumer.detach();
	}

	while (true)
	{
	}
	return 0;
}

死锁问题

编写上面的代码时,我犯了一个错,我在主函数中,把队列看作是一个类似count的临界资源,因为对于生产者和消费者都要操作bq,因此我选择给操作bq这个过程加锁。但是忽略了本身调用push和pop就已经是线程安全的,我在push之前加锁,进入push后阻塞,那么消费者线程永远拿不到外部的锁,也就永远无法消费。生产者要push需要消费者pop,而消费者一直阻塞在pop之前,导致了死锁。值得反思!

惊群效应

再回头看一个问题,为什么线程的唤醒接口提供了两个呢?一个是one一个是all。

  • notify_one: 只唤醒等待队列中的一个线程,因此不存在锁的竞争。
  • notify_all: 会唤醒等待队列中的所有线程,因此存在锁的竞争。

存在锁的竞争意味着,线程的调度优先级高,切换速度快等等因素能影响一个线程能不能更快抢占到锁。但是始终只有一个线程能抢占到。

什么是惊群

惊群效应是多线程中,多个线程阻塞等待一个事件时,一旦事件就绪,可能会唤醒所有等待中的线程,但是最终只有一个线程能成功竞争到这个锁,而其他线程只会因为竞争不到而重新转为休眠。
这样带来的是性能的开销,如内核对于线程频繁的切换和调度,CPU会频繁地操作寄存器和运行队列,把性能浪费在了无谓的切换,而不是线程的工作内容。

惊群的经典场景

在多进程、多线程服务器中,惊群效应可能存在,原因是Linux2.6版本之前,socket编程的accept会导致惊群,多个进程都会阻塞在accept处,当内核将半连接队列中的socket放到全连接队列中后,就会导致所有阻塞中的进程唤醒,然后只有部分进程能够提供服务。Linux2.6版本之后引入了一些机制解决了一个问题,而像Nginx是自己再加一层锁来防止惊群。

虚假唤醒

虚假唤醒是指:一个线程在等待条件变量的时候, 因为某种原因被唤醒,但是此时条件却不满足的现象。虚假唤醒发生的原因有类似:条件就绪后,等待该条件的线程获取到锁并运行之前这段时间,可能有另外一个线程闯进来,并更改了条件,导致条件又不满足。结果就是:线程被唤醒了,但是却竞争失败。在多处理器系统上,虚假唤醒的问题可能更加明显。因为如果有多个线程在等待条件变量,当条件满足时,系统会决定唤醒他们,因此打破了1比1的竞争关系。在操作系统内部,为了能够更加灵活地处理错误条件和线程竞争,条件变量也允许在没有信号的情况下在等待中返回。

典型的例子

if(count < 10)
{ g_cond.wait(lock); }

是用if判断而不是while,当count小于10,我应该等待,当我被唤醒了,说明count应该是大于等于10的。但是如果在我被唤醒后,一个线程闯了进来,把count改成了0,那此时条件又是不满足了,而我却被唤醒了,这就导致我之后是在条件不满足的情况下执行的,这就是虚假唤醒。因而应该采用while判断,唤醒后,执行前再次判断条件是否满足。不满足那么继续等待。

这样虽然能解决虚假唤醒导致结果出错的问题,但是无法从根源上解决操作系统无所谓地调度导致性能损耗的问题。

自旋锁

while(true)
{
	if(条件不满足)
	{}
	else
	{}
}

自旋锁顾名思义,就是线程在获取锁的时候会一直旋转,即循环式地获取,直到获取到锁才往后执行。与互斥锁相比,自旋锁不会阻塞,但是会空耗CPU,因为线程一直在循环,始终是活跃的。但是也减少了内核态和用户态之前的切换。总的来说没有用处。

二、atomic

atomic类型保证该对象的访问不会导致竞争,用于线程间的同步。头文件里包含了两个类:atomic 和 atomic_flag,还声明了一整套与C语言原子操作兼容的C风格函数。

std::atomic

成员函数

is_lock_free

bool is_lock_free() const volatile noexcept;
bool is_lock_free() const noexcept;

用于判断当前对象是否是没有锁机制的,如果true,则没有锁机制。

store

void store (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;
void store (T val, memory_order sync = memory_order_seq_cst) noexcept;

typedef enum memory_order {
	memory_order_relaxed,
	memory_order_consume,
	memory_order_acquire,
	memory_order_release,
	memory_order_acq_rel,
	memory_order_seq_cst
	} memory_order;

第一个参数val表示要存储的值。
第二个参数sync表示操作的同步方式。

  • memory_order_relaxed:无序访问内存;不做任何同步,仅保证该原子类型变量的操作是原子化的,并不保证其对其他线程的可见性和正确性。
  • memory_order_consume:与消费者关系有关的顺序;保证本次读取之前所有依赖于该原子类型变量值的操作都已经完成,但不保证其他线程对该变量的存储结果已经可见。
  • memory_order_acquire:获取关系的顺序;
  • memory_order_release:释放关系的顺序;保证本次写入之后所有后于该原子类型变量写入内存的操作都已经完成,并且其他线程可以看到该变量的存储结果。
  • memory_order_acq_rel:
  • memory_order_seq_cst:顺序一致性的顺序;保证本次操作以及之前和之后的所有原子操作都按照一个全局的内存顺序执行,从而保证多线程环境下对变量的读写的正确性和一致性。这是最常用的内存顺序。

看不懂,一般使用默认值std::memory_order_seq_cst。

load

T load (memory_order sync = memory_order_seq_cst) const volatile noexcept;

std::atomic<size_t> g_count;
size_t count = g_count.load();

用于获取值。返回的是T类型对象的拷贝。

operator T

operator T() const volatile noexcept;

这是隐式转换,返回类型是T,同样是对象的拷贝。

exchange

T exchange (T val, memory_order sync = memory_order_seq_cst) volatile noexcept;

将val替换为新的值,并将旧的值返回。整个过程都是原子性的。

compare_exchange_strong() 和 compare_exchange_weak()

bool compare_exchange_weak (T& expected, T val,memory_order sync = memory_order_seq_cst) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order sync = memory_order_seq_cst) noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order success, memory_order failure) volatile noexcept;
bool compare_exchange_weak (T& expected, T val,memory_order success, memory_order failure) noexcept;

将对象的值与expected比较。
如果相等:则用val的值替换对象的值,类似与store。
如果不相等:则用对象的值替换expected。

举例:
std::atomic<int> m_val = 10;
int i = 1000;
m_val.compare_exchange_strong(i, 0);
// 因为expected不等于m_val,所以把expected改为m_val
// m_val = 10, i = 10;

std::atomic<int> m_val = 10;
int i = 10;
m_val.compare_exchange_strong(i, 0);
// 因为expected等于m_val,所以把m_val改为val(0)
// m_val = 0, i = 10;

支持的数据类型

常见的数据类型如bool、char等单字节,short双字节,int等四字节的数据类型都支持。同时也支持自定义类型,但是需要满足以下条件。

std::is_trivially_copyable<T>::value;
std::is_copy_constructible<T>::value;
std::is_move_constructible<T>::value;
std::is_copy_assignable<T>::value;
std::is_move_assignable<T>::value;

(未完)

你可能感兴趣的:(C/C++,c++,linux,开发语言)