C++线程/阻塞/同步异步----2

本章节内容为记录改写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功能函数

名字TCPUDPICMP
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


boost asio比较全面的两个链接:

https://blog.csdn.net/hyl999/article/details/105864622/
https://blog.csdn.net/qq_44918090/article/details/126575034


请添加图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值