既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
在多线程工作中,线程同步是绕不过的一个问题。线程同步的方式主要有互斥量、临界区、原子操作等,本文主要研究信号量在线程同步中的作用。
信号量的实质是一把数据锁,本身并不具备数据传递的功能。但我在实际使用中,常常会需要在发送一个通知的同时,传递数据给线程,因此就希望能让信号量拥有数据传递的能力。
1. condition_variable和mutex
c++11中,本身便提供了很方便的信号量实现方案,即利用条件变量condition_variable和互斥量mutex,来实现信号量。
我们一步步来,首先来一个最简单的:
#include <condition_variable>
class cv_reusable_semaphore
{
public:
cv_reusable_semaphore()
{}
~cv_reusable_semaphore()
{}
void wait()
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_cv.wait(lock);//*注释1
}
void notify_all()
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_cv.notify_all();
}
void notify_one()
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_cv.notify_one();
}
private:
std::condition_variable m_cv;
#ifdef USE_RECURSIVE_MUTEX
std::recursive_mutex m_recursive_mutex;
#else
std::mutex m_mutex;
#endif
};
互斥量可以选择mutex或recursive_mutex,但条件变量condition_variable只能选用unique_lock锁。
以上的代码确实可以使用,但存在一个问题,也就是在[注释1]处的wait()阻塞。可能在没有收到notify的情况下,也会被唤醒,这就是条件变量的虚假唤醒(spurious wakeups)问题。解决这个问题最常见的方式,就是在wait()外面加上while循环进行判断,避免虚假唤醒的情况。
接下来我们解决这个虚假唤醒问题,并加上数据传递的功能:
#include <condition_variable>
template<class _type>
class cv_reusable_semaphore
{
//标识该信号量当前所处的状态
enum class flag
{
wait = 0, //等待状态
notify_all, //已发送notify_all
notify_one, //已发送notify_one
received, //已接收
};
public:
cv_reusable_semaphore()
:m_flag(flag::wait)
{}
~cv_reusable_semaphore()
{}
_type wait()
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
//避免虚假唤醒
while (m_flag == flag::wait)
{
m_cv.wait(lock);
}
//若发送的状态为notify_all,则置为已接收状态
if (m_flag == flag::notify_all)
{
m_flag = flag::received;
}
//若发送的状态为notify_one,则置为等待状态,等待接收下一个通知
if (m_flag == flag::notify_one)
{
m_flag = flag::wait;
}
return m_data;
}
void notify_all(_type d)
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_data = d;
m_flag = flag::notify_all;
m_cv.notify_all();
}
void notify_one(_type d)
{
#ifdef USE_RECURSIVE_MUTEX
std::unique_lock<std::recursive_mutex> lock(m_recursive_mutex);
#else
std::unique_lock<std::mutex> lock(m_mutex);
#endif
m_data = d;
m_flag = flag::notify_one;
m_cv.notify_one();
}
private:
std::condition_variable m_cv;
#ifdef USE_RECURSIVE_MUTEX
std::recursive_mutex m_recursive_mutex;
#else
std::mutex m_mutex;
#endif
flag m_flag;
_type m_data;
};
和之前的代码相比,为了可以传递各种类型的参数,这个类修改成了模板类,并且添加了两个参数,分别是m_flag和m_data。
m_flag是一个枚举对象,用以标识信号量当前的状态。
m_data是模板类型的对象,用以保存和传递数据。
根据以上代码,不难看出 template<class _type> class cv_reusable_semaphore 拥有以下特性:
1.每一个对象可以建立多个发送和多个接收;
2.每一个对象都可以重复使用,但如果前一个发送没有被接收,再次发送的数据会覆盖前一个发送的数据;
3.wait()和notify_*()函数调用先后顺序不影响结果。若wait()先被调用,则进入阻塞,调用的线程阻塞,等待notify_*()被调用。若notify_*()先被调用,则会将消息存储,当wait()被调用时,不会进入等待,直接将存储的值取;
2. promise和future
2.1. class semaphore
除了用condition_variable和m_mutex来实现信号量之外,c++11里还有一对好基友,也就是promise和future。
利用这一对好基友,也可以很简单的实现我的目的,也就是可以传递数据的信号量,同样也是从最简单的开始:
#include <future>
#define SEMAPHORE_STATE_NULL 0 //初始状态
#define SEMAPHORE_STATE_PROMISE 1 //已发送
#define SEMAPHORE_STATE_FUTURE 2 //已接收
typedef unsigned int semaphore_state;
template<typename _type>
class semaphore
{
public:
semaphore()
:m_state(SEMAPHORE_STATE_NULL)
{}
~semaphore()
{}
_type wait()
{
m_state |= SEMAPHORE_STATE_FUTURE;
std::future<_type> f = m_promise.get_future();
return f.get();
}
void notify(_type data)
{
m_state |= SEMAPHORE_STATE_PROMISE;
m_promise.set_value(data);
}
semaphore_state state()
{
return m_state;
}
private:
std::promise<_type> m_promise;
semaphore_state m_state;
};
可以看到用promise和future来实现的话,代码更为简洁(甚至其中和semaphore_state有关的变量和宏定义,对于这一个类的功能而言没有意义,是为了配合之后实现信号量队列queue_semaphore而添加的)。
基于promise和future的特性,semaphore类拥有不同于cv_reusable_semaphore类的特性:
1.每个对象只能有一个发送和一个接收;
2.每一个对象允许传递一个信号,信号传递完毕后,该对象不可用;
3.wait()和notify()函数调用先后顺序不影响结果(这一点和cv_reusable_semaphore 相同,且以下所有信号量都有该特性,因此在后文中,该特性省略不写);
简单概括一下以上特性:也就是说semaphore类的对象是一次性的,只能进行一次发送(notify)和一次接收(wait),在完成一次发送和接收之后,对象便失去了作用,不允许再次发送或接受。
可以看到,后文中的信号量队列便是利用了semaphore类这种一次性使用的特点。
2.2. class shared_semaphore 和 reusable_semaphore
2.2.1. class shared_semaphore
如果对semaphore类进行一些细微的修改,就能得到不同特性的信号量,例如下面代码,是将future改为了shared_future:
template<typename _type>
class shared_semaphore
{
public:
shared_semaphore()
:m_state(SEMAPHORE_STATE_NULL)
{
m_future = m_promise.get_future().share();
}
~shared_semaphore()
{}
_type wait()
{
return m_future.get();
}
void notify(_type data)
{
m_state |= SEMAPHORE_STATE_PROMISE;
m_promise.set_value(data);
}
semaphore_state state()
{
return m_state;
}
private:
std::promise<_type> m_promise;
std::shared_future<_type> m_future;
semaphore_state m_state;
};
shared_semaphore类拥有以下特性:
1.每一个对象允许传递一个信号,信号发送后,不允许再次发送,但可以继续接收;
2.每个对象只能有一个发送但可以有多个接收;
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
1.每一个对象允许传递一个信号,信号发送后,不允许再次发送,但可以继续接收;
2.每个对象只能有一个发送但可以有多个接收;
[外链图片转存中…(img-5Iq78M2I-1715594606811)]
[外链图片转存中…(img-swJjBMju-1715594606811)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新