本章节内容为记录改写RTK代码时,学习的知识
同步和异步区别
1.定义不同:同步需要将通信双方的时钟统一到一个频率上,异步通信发送的字符间隔时间可以是任意的;
2.准确性不同:同步通信需要比较高精度的精确度,异步则不需要;
3.成本不同:异步通信的设备通常比同步的简单、便宜。
async_read和async_read_some区别
asio::async_read 通常用户读取指定长度的数据,读完或出错才返回。而socket的async_read_some读取到数据或出错就返回,不一定读完了整个包。
tcp/udp同步/异步功能
tcp套接字同步读写:
ip::tcp::endpoint ep( ip::address::from_string("127.0.0.1"), 80);
ip::tcp::socket sock(service);
sock.connect(ep);
sock.write_some(buffer("GET /index.html\r\n"));
std::cout << "bytes available " << sock.available() << std::endl;
char buff[512];
size_t read = sock.read_some(buffer(buff));
udp套接字同步读写:
ip::udp::socket sock(service);
sock.open(ip::udp::v4());
ip::udp::endpoint receiver_ep("87.248.112.181", 80);
sock.send_to(buffer("testing\n"), receiver_ep);
char buff[512];
ip::udp::endpoint sender_ep;
sock.receive_from(buffer(buff), sender_ep);
udp套接字中异步读取:
using namespace boost::asio;
io_service service;
ip::udp::socket sock(service);
boost::asio::ip::udp::endpoint sender_ep;
char buff[512];
void on_read(const boost::system::error_code & err, std::size_t read_bytes) {
std::cout << "read " << read_bytes << std::endl;
sock.async_receive_from(buffer(buff), sender_ep, on_read);
}
int main(int argc, char* argv[]) {
ip::udp::endpoint ep(ip::address::from_string("127.0.0.1"),
8001);
sock.open(ep.protocol());
sock.set_option(boost::asio::ip::udp::socket::reuse_address(true));
sock.bind(ep);
sock.async_receive_from(buffer(buff,512), sender_ep, on_read);
service.run();
}
tcp/udp/icmp功能函数
名字 | TCP | UDP | ICMP |
---|---|---|---|
async_read_some | 是 | - | - |
async_receive_from | - | 是 | 是 |
async_write_some | 是 | - | - |
async_send_to | - | 是 | 是 |
read_some | 是 | - | - |
receive_from | - | 是 | 是 |
write_some | 是 | - | - |
send_to | - | 是 | 是 |
阻塞线程的方法
在C中有信号量、互斥量、条件变量、读写锁等可用于线程同步,他们都有对应的可以使之线程阻塞的方法
1.信号量:信号量 (semaphore) 是一种轻量的同步原件,用于制约对共享资源的并发访问。在可以使用两者时,信号量能比条件变量更有效率。
定义于头文件 <semaphore>
counting_semaphore 实现非负资源计数的信号量
binary_semaphore 仅拥有二个状态的信号量(typedef)
- acquire 减少内部计数器或阻塞到直至能如此
- try_acquire 尝试减少内部计数器而不阻塞
- try_acquire_for 尝试减少内部计数器,至多阻塞一段时长
- try_acquire_until 尝试减少内部计数器,阻塞直至一个时间点
2.互斥:互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。
互斥算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。
定义于头文件 <mutex>
-
lock 锁定互斥,若互斥不可用则阻塞 ,其中lock可锁定多个mutex对象,并内置免死锁算法避免死锁。
-
try_lock尝试锁定互斥,若互斥不可用则返回 (不阻塞)
-
unlock 解锁互斥
通常不直接使用 std::mutex ,一般使用 std::unique_lock 、 std::lock_guard 或 std::scoped_lock 互斥器管理器使用。
-
lock_guard 实现严格基于作用域的互斥体所有权包装器
-
scoped_lock 用于多个互斥体的免死锁 RAII 封装器
-
unique_lock 实现可移动的互斥体所有权包装器
3.条件变量:条件变量是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。条件变量始终关联到一个互斥。
定义于头文件 <condition_variable>
condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
有意修改变量的线程必须
- 获得 std::mutex (常通过 std::lock_guard )
- 在保有锁时进行修改
- 在std::condition_variable 上执行 notify_one 或 notify_all (不需要为通知保有锁)
即使共享变量是原子的,也必须在互斥下修改它,以正确地发布修改到等待的线程。
由上可得,条件变量在线程同步时,需要获得互斥量才能进行。而 unique_lock 这个互斥器管理器允许自由的unlock,所以一般条件变量与unique_lock一起使用。
- wait 、 wait_for 或 wait_until ,等待操作自动释放互斥,并悬挂线程的执行(阻塞)。
4.Future:标准库提供了一些工具来获取异步任务(即在单独的线程中启动的函数)的返回值,并捕捉其所抛出的异常。这些值在共享状态中传递,其中异步任务可以写入其返回值或存储异常,而且可以由持有该引用该共享态的 std::future 或 std::shared_future 实例的线程检验、等待或是操作这个状态。
- get 返回结果 ,get 方法等待直至 future 拥有合法结果并(依赖于使用哪个模板)获取它。它等效地调用 wait()等待结果。(阻塞)
- wait 等待结果变得可用 。阻塞直至结果变得可用。调用后 valid() == true 。
- wait_for等待结果,如果在指定的超时间隔后仍然无法得到结果,则返回。 阻塞直至经过指定的 timeout_duration,或结果变为可用
- wait_until 等待结果,如果在已经到达指定的时间点时仍然无法得到结果,则返回。阻塞直至抵达指定的 timeout_time,或结果变为可用
5.this_thread:在this_thread命名空间下,有this_thread::sleep_for、this_thread::sleep_until、this_thread::yield 三个方法。
其中前两个可以通过让线程睡眠一段时间而达到阻塞线程的目的。
后者,可以通过一定的条件使得当前线程让度给其他线程达到阻塞的目的。while (condition) this_thread::yield();
6.原子操作(CAS)
此外,使用原子量实现自旋锁,也可以在一定时间内阻塞线程。
class CAS // 自旋锁
{
private:
std::atomic<bool> flag; // true 加锁、false 无锁
public:
CAS() :flag(true) {} // 注意这里初始化为 true,因此,第一次调用 lock()就会阻塞
~CAS() {}
CAS(const CAS&) = delete;
CAS& operator=(const CAS&) = delete;
void lock() // 加锁
{
bool expect = false;
while (!flag.compare_exchange_strong(expect, true))
{
expect = false;
}
}
void unlock()
{
flag.store(false);
}
};
该段引自:https://blog.csdn.net/weixin_43919932/article/details/119985704
仿函数
仿函数是一种强大的编程技术,它最大的优势在于可以将复杂的编程任务变得简单易懂。
例子:用generator_n()来生成10个随机数,其中第三个参数是仿函数。
理解:用结构体或者类的构造函数来当作函数调用,这样在调用时候效率更高。
class Point
{
friend ostream& operator<<(ostream& o, const Point& other);
public:
Point(int x = 0, int y = 0):_x(x), _y(y){}
private:
int _x, _y;
};
//仿函数,我们想要一个取值范围为[left, right)的随机点
struct RandPoint
{
//把需要传递的参数作为成员变量并用构造函数初始化
int _left, _right;
RandPoint(int left, int right) :_left(left), _right(right) {}
//函数运算符()的重载
Point operator()()
{
//返回[left, right)的随机数的点,先生成[0, right-left),再加left就是[left, right)范围了
return Point(rand() % (_right - _left) + _left, rand() % (_right - _left) + _left);
}
};
ostream& operator<<(ostream& o, const Point& other)
{
o << "[" << other._x << "," << other._y << "]";
return o;
}
int main()
{
list<Point> allPoint(10); //所有点的数组,初始化时会调用Point的无参构造函数,因为定义有参构造函数时给了默认值,所以会调用自定义的构造函数
generate_n(allPoint.begin(), 10, RandPoint(10, 30)); //产生10个随机数的点,最后一个参数是仿函数,把类的匿名对象作为参数传递给generate_n()函数
for (Point x : allPoint) //遍历数组输出所有点
{
cout << x << " ";
}
}
线程状态
该段引自:https://blog.csdn.net/weixin_43157935/article/details/105872993
async_wait
功能:创建一个io对象,定义一个五秒计时器,时间到了执行handler,异步操作结束前阻塞,结束后返回。
boost::asio::io_service io_service;
boost::asio::deadline_timer timer(io_service, boost::posix_time::seconds(5)); //定义一个5秒的计时器 ,这里指定的是绝对时间
timer.async_wait(handler); //计时时间一到,开始执行handler函数
io_service.run(); //异步操作结束前堵塞,所有异步结束后返回
C++类型转换之reinterpret_cast
原博主写的非常清晰:https://zhuanlan.zhihu.com/p/33040213
我使用到的是reinterpret_cast,可以直接理解为将16进制的数据强制转换成我们指定的类型即可。
static_cast<类型说明符>(表达式)
dynamic_cast<类型说明符>(表达式)
const_cast<类型说明符>(表达式)
reinterpret_cast<类型说明符>(表达式)
互斥锁std::mutex和std::lock_guard/std::unique_lock
访问共享数据的代码片段称之为临界区(critical section)。
我们现在先只关注最基本的mutex,mutex是最基础的API。其他类都是在它的基础上的改进。所以这些类都提供了下面三个方法,并且它们的功能是一样的:
方法 | 说明 |
---|---|
lock() | 锁定互斥体,如果不可用,则阻塞 |
try_lock() | 尝试锁定互斥体,如果不可用,直接返回 |
unlock() | 解锁互斥体 |
这三个方法提供了基础的锁定和解除锁定的功能。使用lock意味着你有很强的意愿一定要获取到互斥体,而使用try_lock则是进行一次尝试。这意味着如果失败了,你通常还有其他的路径可以走。
所以最基本的使用:
std::mutex mtx;
void someOp(){
mtx.lock(); //加锁
... //执行你的操作,这块是临界区
mtx.unlock(); //解锁
}
在进入临界区时,执行lock()加锁操作,如果这时已经被其它线程锁住,则当前线程在此排队等待。退出临界区时,执行unlock()解锁操作。
采用“资源分配时初始化(RAII)”方法来加锁、解锁,标准库就提供了下面的这些API,来简化我们手动加锁和解锁的“体力活”。
API | 说明 |
---|---|
lock_guard | 实现严格基于作用域的互斥体所有权包装器 |
unique_lock | 实现可移动的互斥体所有权包装器 |
锁定策略 | 说明 |
---|---|
defer_lock | 类型为 defer_lock_t,不获得互斥的所有权 |
try_to_lock | 类型为try_to_lock_t,尝试获得互斥的所有权而不阻塞 |
adopt_lock | 类型为adopt_lock_t,假设调用方已拥有互斥的所有权 |
lock_guard
所以上边的代码可以变成这样:
std::mutex mtx;
void someOp(){
std::lock_guard<std::mutex> lock(mtx);; //加锁
... //执行你的操作,这块是临界区
}
//出了作用域,自动释放锁mtx
在std::lock_guard对象构造时,传入的mutex对象(即它所管理的mutex对象)会被当前线程锁住。在lock_guard对象被析构时,它所管理的mutex对象会自动解锁,不需要程序员手动调用lock和unlock对mutex进行上锁和解锁操作。lock_guard对象并不负责管理mutex对象的生命周期,lock_guard对象只是简化了mutex对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个lock_guard对象的生命周期内,它所管理的锁对象会一直保持上锁状态;而lock_guard的生命周期结束之后,它所管理的锁对象会被解锁。程序员可以非常方便地使用lock_guard,而不用担心异常安全问题。
unique_lock
unique_lock具有lock_guard的全部功能,但是更加灵活:
1.lock_guard在构造时或者构造前(std::adopt_lock)就已经获取互斥锁,并且在作用域内保持获取锁的状态,直到作用域结束;而unique_lock在构造时或者构造后(std::defer_lock)获取锁,在作用域范围内可以手动获取锁和释放锁,作用域结束时如果已经获取锁则自动释放锁。
2.lock_guard锁的持有只能在lock_guard对象的作用域范围内,作用域范围之外锁被释放,而unique_lock对象支持移动操作,可以将unique_lock对象通过函数返回值返回,这样锁就转移到外部unique_lock对象中,延长锁的持有时间。
所以上边代码也可以这样:
std::mutex mtx;
void someOp(){
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);; //此时还未加锁
lock.lock();//手动获取锁
... //执行你的操作,这块是临界区
lock.unlock();//手动释放锁
}
另外和条件变量condition_variable一起使用的时候,也需要用unique_lock!
该段引自:https://blog.csdn.net/aiynmimi/article/details/127492406
MutableBufferSequence可变缓冲序列
https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio/reference/read.html