当前位置: 首页 > news >正文

C++20新特新——02特性的补充

        虽然上节我们介绍了不少关于协程的特点,但是大家可能对协程还是不是很了解,没关系,这里我们再对其进行补充,详细讲解一下;

一、协程函数与普通函数的区别

这里我们再回归到问题:普通函数和协程在这方面的区别是什么?

  • 普通函数是线程相关的,函数的状态和线程紧密相关!
  • 但是协程的状态和和线程无关!

接下来我们对这方面进行解释说明:

        假设当前我们有Foo这个函数,我们需要调用Foo这个函数,此时在对应的的线程的栈上会记录这个函数的状态(参数、局部变量等),也就是函数栈帧!

        这里是通过移动函数的栈顶指针和栈底指针来实现的;

        详细的大家可以参考我之前写的一篇博客:

如上图所示,此时我们调用FOO普通函数:

这里地址2和地址3分别对应我们的栈顶指针(低地址)和栈底指针!(高地址)

此时如果我们再调用Bar()函数:

  • 这里地址3到地址2是给FOO函数调用使用的;
  • 地址2到地址1是给Bar()函数调用使用的;
  • 如果同时调用两个函数,此时栈顶指针指向地址1;
  • 当bar()销毁的时候,此时栈顶指针从地址1回到地址2;

因此这里可以发现,函数栈帧中存放的函数的状态完全依赖于线程栈!

如果线程栈被销毁了,此时函数的状态也就被销毁掉了;

但是协程不一样,此时如果我们假设Bar()是一个协程:

        此时,协程的状态信息是存放在堆上的!与线程的函数栈帧分开!

        传递给协程的参数都会复制到状态当中,局部变量会直接再协程的状态中进行直接创建!

        但是实际上,调用Bar()的时候,本质上还是一个函数调用,所以栈顶指针也会往下移动,在栈上给执行 Bar() 所需的状态分配空间,其中会有一个引用指向在堆上的状态,这样一来, Bar() 就可以像一个普通函数那样执行了,线程也可以访问到堆上的协程的状态。

        如果协程需要暂停,那么当前执行的代码的位置就会记录到堆的状态当中!

        此时栈上的执行状态会被直接销毁!栈顶指针移动到回收空间;

        而在下一次恢复执行时,堆状态中记录的暂停位置会读取出来,从这个位置接着执行,从而实现一个可暂停和恢复的函数!

二、协程相比于线程函数的优点

        协程的主要优点体现于:其可以优化异步逻辑的代码,与进程相比,尤其是在多进程方面,使得代码的逻辑更简单!

        接下来我们举一个具体的例子:假设我们有一个组件叫 IntReader ,它的功能是从一个访问速度很慢的设备上读取一个整数值,因此它提供的接口是异步的,如下所示:

class IntReader {
public:void BeginRead() {std::thread thread([]() {std::srand(static_cast<unsigned int>(std::time(nullptr)));int value = std::rand();});thread.detach();}
};

这里BeginRead相当于启动1个显得线程用来读取一个随机数;

关于异步的线程接口的使用可以参考我的上一篇博客:

C++20新特新——01协程的入门及一些语法知识-CSDN博客

这里相当于BeginRead为主线程,然后新启动一个线程生成一个随机数,然后主线程和子线程实现线程分离;

  • 调用.join的时候此时主线程会进行同步阻塞;
  • 调用.detch的时候此时主线程和子线程会进行异步分离;

问题:如果我想要获取IntReader的结果,我应该怎么实现?

即此时问题就是一个线程要获取另一个线程的返回值,此时有两种解决方法:回调函数和async;

使用async解决问题

在上一篇博客当中,我提到过async与thread相比,可以获取到线程的返回值!

所以这里我们将上面的代码进行修改:

#include <future>
#include <cstdlib>
#include <ctime>class IntReader {
public:std::future<int> BeginRead() {// 使用 std::async 启动异步任务,返回 future<int>return std::async(std::launch::async, []() {// 生成随机数(需确保线程安全)std::srand(static_cast<unsigned int>(std::time(nullptr)));return std::rand();});}
};
int main() {IntReader reader;std::future<int> future = reader.BeginRead(); // 启动异步任务// 执行其他操作...int value = future.get(); // 阻塞等待结果std::cout << "生成的随机数: " << value << std::endl;return 0;
}

        上面的这里我们返回的std::rand()实际上是一个int类型,然后这个int类型会隐式的转化为std::future类型进行返回;

        而在主函数当中,这里我们定义了future变量,此时就会执行对应的异步代码;然后通过调用get函数会返回生成的结果;

使用回调函数解决问题

class IntReader {
public:void BeginRead(const std::function<void(int)>& callback) {std::thread thread([callback]() {std::srand(static_cast<unsigned int>(std::time(nullptr)));int value = std::rand();callback(value);});thread.detach();}
};void PrintInt() {IntReader reader;reader.BeginRead([](int result) {std::cout << result < std::endl;});
}

这种方式本质就是:

  • 当我们调用BeginRead函数的时候,向其中传入一个回调函数用来接收回收值;
  •  在BeginRead中,这里我们将随机值传递到了回调函数当中;
  • 在main函数,这里我们向其中传入回调函数,形参result用来接收value,然后函数体对其打印即可;

        假如我们需要调用多个 IntReader ,把它们的结果加起来再输出,那么基于回调的代码就会很难看了:

void PrintInt() {IntReader reader1;reader1.BeginRead([](int result1) {int total = result1;IntReader reader2;reader2.BeginRead([total](int result2) {total += result2;IntReader reader3;reader3.BeginRead([total](int result3) {total += result3;std::cout << total << std::endl;});});});
}

        需要注意的是:这里的代码逻辑实际上是一个线程执行完再执行下一个线程,不会出现同时并行执行的效果,是按照串行执行进行的;

        但是这里的代码逻辑很乱,很难整理清楚;

        但是如果我们使用协程就不一样了:

Task PrintInt() {IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;std::cout << total << std::endl;
}

这里每个等待体可以获取到对应的随机值,然后进行返回计算;

整体的逻辑清晰了不少;

三、如何实现一个完整的协程

在第一节,我们已经介绍了对应的协程体的等待体和返回值等,大家可以参考上节博客,这里我们只对协程进行一些补充;

1. 协程的返回类型和promise_type

  • C++对协程的返回类型只有一个要求:包含名为 promise_type 的内嵌类型。
  • 跟上文介绍的 等待体一样, promise_type 需要符合C++规定的协议规范,也就是要定义几个特定的函数。
  • promise_type 是协程的一部分,当协程被调用,在堆上为其状态分配空间的时候,同时也会在其中创建一个对应的 promise_type 对象。
  • 通过在它上面定义的函数,我们可以与协程进行数据交互,以及控制协程的行为。 

2. 协程的返回值和co_return

        协程的返回值取决于我们的需求!例如上面我们所示的例子中,这里PrintInfo函数只是与上面的函数体内进行交互,而不需要返回实际的值给调用者当中;

        普通的线程函数可以通过回调函数或者通过异步接口std:async这样获取到返回值,那么协程如何获取到返回值呢?

        这里协程中提供了一个co_return的关键字,例如下面假如我们不是打印对应的消息,而是要获取对应的信息GetInfo,那么此时我们可以对该协程函数进行修改:

Task GetInt() {IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;co_return total;
}

        这里的co_return total 这个表达式等价于 promise_type.return_value(total) ,也就是说,返回的数据会通过 return_value() 函数传递给 promise_type 对象, promise_type 要实现这个函数才能接收到数据;

        这里我们要区分co_return的本质:实际上是上total的值设置到了promise_type的对象当中!而不是类似普通的线程函数中的return那种;

        这里的total的值是返回到了promise_type当中,所以对应的协程的返回值如果想要从Task当中获取到promise_type当中的value,这里我们可以让Task和promise_type两者共享一份数据!

例如下面所示的协程代码例子:

#define  _CRT_SECURE_NO_WARNINGS 1
#include <coroutine>
#include <iostream>
#include <thread>// 定义一个等待体
class IntReader {
public:// 协程挂起bool await_ready() {return false;}void await_suspend(std::coroutine_handle<> handle) {// 挂起后创建一个子线程,将随机数赋值给value_std::thread thread([this, handle]() {std::srand(static_cast<unsigned int>(std::time(nullptr)));value_ = std::rand();handle.resume();});// 父线程和子线程异步thread.detach();}int await_resume() {return value_;}private:int value_{};
};class Task {
public:class promise_type {public:// 由于promise_type的构造函数调用了promise_type() : value_(std::make_shared<int>()) {}Task get_return_object() {return Task{ value_ };}// co_return 实际上调用了该函数// 将传入的参数value保存到value_当中void return_value(int value) {*value_ = value;}std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}private:std::shared_ptr<int> value_;};public:// 初始化的时候需要传入共享指针Task(const std::shared_ptr<int>& value) : value_(value) {}int GetValue() const {return *value_;}private:// 通过共享指针管理valuestd::shared_ptr<int> value_;
};Task GetInt() {IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;co_return total;
}int main() {auto task = GetInt();std::string line;while (std::cin >> line) {std::cout << task.GetValue() << std::endl;}return 0;
}

问题:这里是如何实现Task和promise_type共享同一个变量value的?

首先我们看这里的promise_type的构造函数:

        // 由于promise_type的构造函数调用了promise_type() : value_(std::make_shared<int>()) {}

        这里我们是让primise_type的value_指向0所示的共享指针,从而进行初始化;

        而当进行返回一个返回值的时候,此时会用value_构造一个Task,这里需要注意的是:因为该成员函数发生在promise_type的内部,所以此时value_采用的是promise_type的value!

Task get_return_object() {return Task{ value_ }; // 将 promise_type 的 value_ 传递给 Task
}

        所以此时Task会用这个value_对Task里面value_进行初始化!

        接下来这里我们再看返回值,当我们调用co_return的时候:

        此时会调用下面的函数:

void return_value(int value) {*value_ = value; // 将值写入共享指针指向的内存
}

这里是将形参value写入到value_当中,但是我们需要注意的是:

  • 这个函数发生在promise_type内部,但是由于此时promise_type和Task共享同一个value_;
  • 所以此时对promise_type里面的value_发生改变,那么Task里面的value_也会发生改变;

所以如果我们想要获取到对应的value,只需要在Task里面定义一个接口即可:

    int GetValue() const {return *value_;}

此时即可获取到对应的value的值!

注意点:

  • 跟普通的 return 一样, co_return 也可以不带任何参数,这时候协程以不带数据的方式返回,相当于调用了 promise_type.return_void() , promise_type 需要定义这个函数以支持不带数据的返回;
  • 如果我们在协程结束的时候没有调用任何 co_return ,那么编译器会隐式地加上一个不带参数的 co_return 调用

        这里我们再重点提醒一下,co_return和传统的return不一样,相当于将值存放到promise_type里面当中!

问题:除了上面所示的共享指针,还有没有其他的方法可以使Task获取到promise_type的成员变量?

        其实有一个特别简单的方法,也就是通过协程句柄:coroutine_handle获取到对应的promise对象,例如我们可以对上面的代码进行修改:

#include <coroutine>
#include <iostream>class Task {
public:class promise_type {public:// 直接存储数据,而非共享指针int value_ = 0;// 返回 Task 对象时,传入协程句柄Task get_return_object() {return Task{std::coroutine_handle<promise_type>::from_promise(*this)};}// 协程完成后挂起,保持协程帧存活std::suspend_always final_suspend() noexcept { return {}; }// 其他必要接口std::suspend_never initial_suspend() { return {}; }void unhandled_exception() {}void return_value(int value) { value_ = value; } // co_return 赋值};public:// 保存协程句柄explicit Task(std::coroutine_handle<promise_type> h) : coro_handle(h) {}// 析构时销毁协程帧~Task() {if (coro_handle) coro_handle.destroy();}// 禁止拷贝,允许移动(避免重复销毁)Task(const Task&) = delete;Task& operator=(const Task&) = delete;Task(Task&& other) noexcept : coro_handle(other.coro_handle) {other.coro_handle = nullptr;}// 通过协程句柄直接访问 promise_type 的数据int GetValue() const {return coro_handle.promise().value_;}private:std::coroutine_handle<promise_type> coro_handle;
};// 示例协程
Task MyCoroutine() {co_return 42; // 调用 return_value(42)
}int main() {Task task = MyCoroutine();std::cout << task.GetValue(); // 输出 42
}

在Task内部:

// 通过协程句柄直接访问 promise_type 的数据int GetValue() const {return coro_handle.promise().value_;}

        这里我们可以直接通过协程句柄获取到promise对象,然后再获取到对应的value的值;

        这种方法理解更为简单; 

3. 协程的关键字co_yield

问题:什么时候我们需要使用co_yield?

        当协程调用了 co_return ,意味着协程结束了,就跟我们在普通函数中用 return 结束函数一样。这时候,与这个协程实例有关的内存都会被释放掉,它不能再执行了

        但是如果需要在协程中多次返回数据而不结束协程的话,可以使用 co_yield 操作符!

        co_yield 的作用是,返回一个数据,并且让协程暂停,然后等下一次机会恢复执行;

        co_yield value 这个表达式等价于 co_await promise_type.yield_value(value) , co_yield 的参数会传递给 promise_type 的 yield_value() 函数,再把这个函数的返回值传给 co_await ;(这里该函数的返回值是一个等待体类型的!);

        在这里就可以使用预定义的 std::supsend_never 或 std::suspend_always ,通常会使用后者来让协程每次调用 co_yield 的时候都暂停;

例如下面这个例子:

#include <coroutine>
#include <iostream>
#include <thread>// 定义等待体
class IntReader {
public:// 将协程挂起bool await_ready() {return false;}// 切换到另一个线程void await_suspend(std::coroutine_handle<> handle) {std::thread thread([this, handle]() {// 定义不被销毁的静态变量static int seed = 0;value_ = ++seed;handle.resume();});// 主线程和子线程异步thread.detach();}int await_resume() {return value_;}private:// 这里实际上是调用统一的列表初始化// 给value_一个默认值为0int value_{};
};class Task {
public:class promise_type {public:Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}// 调用co_yield的时候,调用该函数;// 返回值是一个等待体类型 --- 让传递返回值后总是挂起std::suspend_always yield_value(int value) {value_ = value;return {};}void return_void() { }std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}int GetValue() const {return value_;}private:int value_{};};public:Task(std::coroutine_handle<promise_type> handle) : coroutine_handle_(handle) {}int GetValue() const {return coroutine_handle_.promise().GetValue();}void Next() {coroutine_handle_.resume();}private:std::coroutine_handle<promise_type> coroutine_handle_;
};Task GetInt() {while (true) {IntReader reader;int value = co_await reader;co_yield value;}
}int main() {auto task = GetInt();std::string line;while (std::cin >> line) {std::cout << task.GetValue() << std::endl;task.Next();}return 0;
}

上面的代码相比于之前我们写的确实改动了不少,但是这里我们可以逐个进行分析:

  • 整体的代码框架依然是定义一个等待体、一个协程的返回值、一个协程函数和我们对应的主函数;

这里我们先分析等待体:

    // 切换到另一个线程void await_suspend(std::coroutine_handle<> handle) {std::thread thread([this, handle]() {// 定义不被销毁的静态变量static int seed = 0;value_ = ++seed;handle.resume();});// 主线程和子线程异步thread.detach();}

这里等待体从之前的生成随机数变为递增的整数;

除此之外,当我们调用co_yield的时候,协程保存到对应的数据后可能会挂起,所以我们在返回值Task提供协程的恢复函数:

    void Next() {coroutine_handle_.resume();}

需要注意的是,由于恢复协程需要用到协程句柄,所以我们需要在Tsak里面声明一个协程句柄:

private:std::coroutine_handle<promise_type> coroutine_handle_;

那么此时我们我们通过promise_type返回Task对象的时候,就需要向其传入一个协程句柄用来初始化;

问题:那么在promise_type的内部,我们怎么获取到Task的协程句柄呢?

实际上,promise_type作为连接协程内外的桥梁,这里其提供了一个静态的接口函数

template <class _Promise>
struct coroutine_handle {static coroutine_handle from_promise(_Promise& _Prom) noexcept {...}
}

这里我们向其传入一个promise_type等待体的对象,然后值是一个协程句柄;

所以在promise_type返回一个协程对象的时候,这时候我们就可以通过下面这种方式传入协程句柄:

Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };
}

promise_type内可以通过该静态函数获取到协程体对象;

与此相对应的,Task内部也可以通过协程句柄.promise()获取到对应的promise_type对象!

    int GetValue() const {return coroutine_handle_.promise().GetValue();}

问题:coroutine_handle有coroutine_handle<>和coroutine_handle<romise_type>,这两个有什么区别?

这里我们可以发现在我们定义等待体的时候:

    void await_suspend(std::coroutine_handle<> handle) {// 挂起后创建一个子线程,将随机数赋值给value_std::thread thread([this, handle]() {std::srand(static_cast<unsigned int>(std::time(nullptr)));value_ = std::rand();handle.resume();});// 父线程和子线程异步thread.detach();}

例如这里的await_suspend,这里我们传入的协程句柄是coroutine_handle<>类型!

而当我们在Task定义协程句柄的时候:

private:std::coroutine_handle<promise_type> coroutine_handle_;

类型为:coroutine_handle<romise_type>!

        它们的区别类似于指针 void* 和 promise_type* 的区别,前者是无类型的,后者是强类型的!

        两种类型的协程句柄本质上是相同的东西,它们可以有相同的值,指向同一个协程实例,而且也都可以恢复协程执行。

        但是这里需要注意的是只有强类型的 std::coroutine_handle<promise_type> 才能调用 from_promise() 获取到 promise_type 对象!

        除此之外。这里我们还把协程函数改为无限循环的类型:

Task GetInt() {while (true) {IntReader reader;int value = co_await reader;co_yield value;}
}

我们可以再看之前的使用co_return的协程函数:

Task GetInt() {IntReader reader1;int total = co_await reader1;IntReader reader2;total += co_await reader2;IntReader reader3;total += co_await reader3;co_return total;
}

        其实在协程中使用无限循环是很常见的,因为当我们调用co_yield的时候,此时返回值保存到value当中,并且协程会挂起!不会一直死循环执行!当我们恢复协程时,其执行完工作又继续挂起,和传统的死循环是不一样的!

四、协程的生命周期

        在一开始调用协程的时候,C++会在堆上为协程的状态分配内存,这块内存必须在适当的时机来释放,否则就会造成内存泄漏。释放协程的内存有两种方式:自动释放和手动释放。

        当协程结束的时候,如果我们不做任何干预,那么协程的内存就会被自动释放。调用了 co_return 语句之后,协程就会结束,下面两个协程是自动释放的例子:

Task GetInt() {IntReader reader;int value = co_await reader;co_return value;
}Task PrintInt() {IntReader reader1;int value = co_await reader;std::cout << value << std::endl;
}

        PrintInt() 没有出现 co_return 语句,编译器会在末尾隐式地加上 co_return !
        自动释放的方式有时候并不是我们想要的,参考下面这个例子: 

#include <coroutine>
#include <iostream>
#include <thread>class Task {
public:class promise_type {public:Task get_return_object() {return Task{ std::coroutine_handle<promise_type>::from_promise(*this) };}void return_value(int value) {value_ = value;}int GetValue() const {return value_;}std::suspend_never initial_suspend() { return {}; }std::suspend_never final_suspend() noexcept { return {}; }void unhandled_exception() {}private:int value_{};};public:Task(std::coroutine_handle<promise_type> handle) : coroutine_handle_(handle) {}int GetValue() const {return coroutine_handle_.promise().GetValue();}private:std::coroutine_handle<promise_type> coroutine_handle_;
};Task GetInt() {co_return 1024;
}int main() {auto task = GetInt();std::string line;while (std::cin >> line) {std::cout << task.GetValue() << std::endl;}return 0;
}

打印的结果如下所示: 

会发现打印的是一些随机值!

        造成这个现象的原因是,协程在返回1024之后就被自动释放了, promise_type 也跟着被一起释放了此时在 Task 内部持有的协程句柄已经变成了野指针,指向一块已经被释放的内存。所以访问这个协程句柄的任何行为都会造成不确定的后果!

解决方法:

        修改 promise_type 中 final_supsend() 函数的返回类型,从 std::suspend_never 改成 std::suspend_always ;协程在结束的时候,会调用 final_suspend() 来决定是否暂停,如果这个函数返回了要暂停,那么协程不会自动释放,此时协程句柄还是有效的,可以安全访问它内部的数据;

        不过,这时候释放协程就变成我们的责任了,我们必须在适当的时机调用协程句柄上的 destroy() 函数来手动释放这个协程!

    ~Task() {coroutine_handle_.destroy();}

修改后此时我们再次运行我们的程序:

此时发现可以正常打印值!

五、协程的异常处理

        协程的异常处理机制与普通函数有所不同,主要依赖于 promise_type 中的 unhandled_exception 方法!接下来我们对其进行解释说明:

#include <exception> // for std::current_exceptionclass Task {
public:class promise_type {public:// 存储异常std::exception_ptr exception_;// 当协程抛出未捕获的异常时调用void unhandled_exception() {exception_ = std::current_exception(); // 捕获异常指针}// 其他必要方法(initial_suspend, final_suspend, get_return_object 等)};private:std::coroutine_handle<promise_type> coro_;
};

        promise_type 的 unhandled_exception() 函数会被调用,我们可以在这个函数里面做对应的异常处理!

        而在我们的实际协程的代码框架中,我们可以采用下面的框架伪代码:

try {co_await promise_type.initial_suspend();//协程函数体的代码...
}
catch (...) {promise_type.unhandled_exception();
}co_await promise_type.final_suspend();

首先这里我们先执行:

co_await promise_type.initial_suspend();
  • 看协程是立刻挂起,还是执行到对于的co_await再挂起;
  •  接下来填写的是协程的主逻辑框架;
  • 如果出现异常,此时会交给对应的promise_type.unhandle_exception进行处理!
  • 最后在调用final_suspend()看协程结束后是否需要挂起;

        调用了 unhandled_exception() 之后,协程就结束了,接下来会继续调用 final_suspend() ,与正常结束协程的流程一样;

        C++规定 final_suspend() 必须定义成 noexcept ,也就是说它不允许抛出任何异常!

至此,我们对协程的学习就更进一步了;

相关文章:

C++20新特新——02特性的补充

虽然上节我们介绍了不少关于协程的特点&#xff0c;但是大家可能对协程还是不是很了解&#xff0c;没关系&#xff0c;这里我们再对其进行补充&#xff0c;详细讲解一下&#xff1b; 一、协程函数与普通函数的区别 这里我们再回归到问题&#xff1a;普通函数和协程在这方面的…...

【c++】 我的世界

太久没更新小游戏了 给个赞和收藏吧&#xff0c;求求了 要游戏的请私聊我 #include <iostream> #include <vector>// 定义世界大小 const int WORLD_WIDTH 20; const int WORLD_HEIGHT 10;// 定义方块类型 enum BlockType {AIR,GRASS,DIRT,STONE };// 定义世界…...

Redis从入门到实战 - 高级篇(上)

一、分布式缓存 1. 单点Redis的问题 数据丢失问题&#xff1a;Redis是内存存储&#xff0c;服务重启可能会丢失数据 -> 实现Redis数据持久化 并发能力问题&#xff1a;单节点Redis并发能力虽然不错&#xff0c;但也无法满足如618这样的高并发场景 -> 搭建主从集群&…...

常见的卷积神经网络列举

经典的卷积神经网络&#xff08;CNN&#xff09;在深度学习发展史上具有重要地位&#xff0c;以下是一些里程碑式的模型及其核心贡献&#xff1a; 1. LeNet-5&#xff08;1998&#xff09; 提出者&#xff1a;Yann LeCun特点&#xff1a; 首个成功应用于手写数字识别&#xff…...

Linux如何安装AppImage程序

Linux如何安装AppImage程序 文章目录 Linux如何安装AppImage程序 在 Linux 中&#xff0c;.AppImage 是一种便携式的应用程序格式&#xff0c;无需安装即可运行。 1.赋予该文件可执行权限 可以使用下列命令&#xff0c;赋予可执行权限 # 举个例子 chmod x /path/to/MyApp.App…...

人工智能如何进行课堂管理?

人工智能如何协助老师课堂管理&#xff1f; 第一步&#xff1a;在腾讯元宝对话框中输入&#xff1a;如何协助老师进行课堂管理&#xff0c;通过提问&#xff0c;我们了解了老师高效备课可以从哪些方面入手&#xff0c;提高效率。 第二步&#xff1a;编辑问题进行提问&#xf…...

如何理解参照权

在管理学和组织行为学中&#xff0c;“参照权力”&#xff08;Referent Power&#xff09;是一种非常重要的权力来源&#xff0c;它属于非强制性权力的一种&#xff0c;主要基于个人特质和人际关系。以下是对参照权力的详细解释&#xff1a; 一、定义 参照权力是指一个人由于…...

从一次被抄袭经历谈起:iOS App 安全保护实战

如何保护 iOS App 的最后一道防线&#xff1a;那些你可能忽略的混淆技巧 如果你曾认真反编译过别人的 .ipa 文件&#xff0c;很可能会有这种感受&#xff1a;“哇&#xff0c;这代码也太干净了吧。” 类名像 UserManager&#xff0c;方法名是 getUserToken&#xff0c;甚至资源…...

从交互说明文档,到页面流程图设计全过程

依据交互说明文档绘制页面流程图&#xff0c;能够将抽象的交互逻辑转化为可视化、结构化的表达&#xff0c;为开发、测试及团队协作提供清晰指引。接下来&#xff0c;我们以外卖 App 订单确认页为例&#xff0c;详细拆解从交互说明文档到完整页面流程图的设计全过程。 一、交互…...

fedora系统详解详细版本

Fedora 系统详解&#xff1a;从起源到实践的深度解析 一、Fedora 概述&#xff1a;开源社区的技术先锋 Fedora 是由 Fedora 项目社区 开发、Red Hat 公司赞助 的 Linux 发行版&#xff0c;以 自由开源、技术前沿 和 稳定性平衡 著称。它是 Red Hat Enterprise Linux&#xff…...

2025-05-07-FFmpeg视频裁剪(尺寸调整,画面比例不变)

原比例如图 原比例如图裁剪后的比例 代码&#xff1a; 方法一&#xff1a;极速 ffmpeg -i input.mp4 -vf "crop1080:750:0:345" -c:v libx264 -preset ultrafast -c:a copy output.mp4关键参数说明&#xff1a; vf “crop宽:高❌y”&#xff1a;定义裁剪区域。 …...

RISC-V JTAG:开启MCU 芯片调试之旅

在当今电子科技飞速发展的时代&#xff0c; MCU 芯片成为众多企业追求技术突破与创新的关键领域。而芯片的调试过程则是确保其性能与可靠性的重要环节。本文以国科安芯自研 AS32A601为例&#xff0c;旨在详细记录基于 RISC-V 架构的 MCU 芯片JTAG 调试过程及操作&#xff0c;为…...

51单片机快速成长路径

作为在嵌入式领域深耕18年的工程师&#xff0c;分享一条经过工业验证的51单片机快速成长路径&#xff0c;全程干货无注水&#xff1a; 一、突破认知误区&#xff08;新手必看&#xff09; 不要纠结于「汇编还是C」&#xff1a;现代开发90%场景用C&#xff0c;掌握指针和内存管…...

idea左侧项目资源管理器不见了处理

使用idea误触导致&#xff0c;侧边栏和功能栏没了&#xff0c;如何打开&#xff1f; 1.打开文件&#xff08;File&#xff09; 2. 打开设置&#xff08;Settings&#xff09; 3.选择Appearance&Behavior--->Appearance划到最下面&#xff0c;开启显示工具栏和左侧并排布…...

给小白的AI Agent 基本技术点分析与讲解

引言&#xff1a;重塑交互与自动化边界的 AI Agent 在人工智能技术飞速发展的浪潮中&#xff0c;AI Agent&#xff08;智能体&#xff09;概念的兴起标志着自动化和人机交互正迈向一个全新的阶段。传统的软件系统通常被设计来执行精确预设的指令序列&#xff0c;它们强大且高效…...

[特殊字符] 深入解析:Go 与 Rust 中的数组与动态集合结构

在 Go 和 Rust 这两种现代语言中&#xff0c;数组和动态集合&#xff08;如切片或 Vec&#xff09;是处理数据的基础工具。虽然它们都提供了高效的内存访问能力&#xff0c;但设计理念却截然不同&#xff1a; Go 更注重灵活性和性能&#xff0c;允许开发者直接操作底层指针和容…...

C25-数组应用及练习

第一题 题目: 代码 #include <stdio.h> int main() {//数组及相关数据定义int arr[10];int i;//基于循环的数组数据输入for(i0;i<10;i){arr[i]i;}//基于循环的数组数据输出for(i9;i>0;i--){printf("%d ",arr[i]);}return 0; }结果 第二题 题目 代码 …...

Soft Mask(软遮罩)技术

一、概述 Soft Mask是一种技术或工具&#xff0c;主要用于实现平滑的边缘遮罩效果。它在不同的应用领域有不同的实现和定义 1.在Unity UI设计中 SoftMask是一款专为Unity设计的高级遮罩工具&#xff0c;它突破了传统Mask的限制&#xff0c;提供了更为灵活和细腻的UI遮罩解决方案…...

683SJBH基于J2EE的广州旅游管理系统

第1章  绪论 课题背景 自互联网internet成为一种革命性的大众媒体以来&#xff0c;其发展速度之快令人惊叹。而作为世界最大朝阳产业的旅游&#xff0c;当它与电子商务这一新兴模式相结合时&#xff0c;其潜藏的商业价值表露无遗。根据CNN&#xff08;美国有线电视新闻网&…...

关于STM32 SPI收发数据异常

问题描述&#xff1a; STM32主板做SPI从机&#xff0c;另一块linux主板做主机&#xff0c;通信的时候发现从机可以正确接收到主机数据&#xff0c;但是主机接收从机数据时一直不对&#xff0c;是随机值。 问题原因&#xff1a; 刚发现问题的时候&#xff0c;用逻辑分析仪抓包…...

雅努斯问题(Janus Problem)及解决方案

一、雅努斯简介 雅努斯&#xff08;Janus&#xff09;是罗马神话中的门神&#xff0c;也是罗马人的保护神。他具有前后两个面孔或四方四个面孔&#xff0c;象征开始。雅努斯被认为是起源神&#xff0c;执掌着开始和入门&#xff0c;也执掌着出口和结束&#xff0c;因此他又被成…...

ACE-Step:扩散自编码文生音乐基座模型快速了解

ACE-Step 模型速读 一、模型概述 ACE-Step 是一款由 ACE Studio 和 StepFun 开发的新型开源音乐生成基础模型。它通过整合基于扩散的生成方式、Sana 的深度压缩自编码器&#xff08;DCAE&#xff09;以及轻量级线性变换器&#xff0c;在音乐生成速度、音乐连贯性和可控性等方…...

【论文阅读】在调制分类中针对对抗性攻击的混合训练时和运行时防御

A Hybrid Training-Time and Run-Time Defense Against Adversarial Attacks in Modulation Classification 摘要 在深度学习在包括计算机视觉和自然语言处理在内的许多应用中的卓越性能的推动下,最近的几项研究侧重于应用深度神经网络来设计未来几代无线网络。然而,最近的…...

HDMI布局布线

1 HDMI简介 高清多媒体接口(High Definition Multimedia Interface),简称:HDMI,是一种全数字化视频和声音发送接口,可以发送未压缩的音频及视频信号。随着技术的不断提升,HDMI的传输速率也不断的提升,HDMI2.0最大传输速率可达14.4Gbit/s,HDMI2.1最大传输数据速率可达42.6Gbit/s…...

国家信息中心:基于区块链和区块链服务网络(BSN)的可信数据空间建设指引

推荐语&#xff1a; 可信数据空间包含场景应用、生态主体、数据资源、规则机制、技术系统五大部分。《基于区块链和区块链服务网络&#xff08;BSN&#xff09;的可信数据空间建设指引》聚焦可信数据空间的单个数据空间中的场景应用、数据资源、规则机制及技术系统四大核心要点…...

分区器(1)

1. 需求分析 在分布式计算中&#xff0c;Map任务通常会产生大量的中间结果&#xff0c;这些结果需要被分配到不同的Reducer任务中进行进一步处理。分区器的作用是根据一定的规则将中间结果分配到不同的分区&#xff08;Partition&#xff09;&#xff0c;从而确保数据能够被正…...

设计一个分布式系统:要求全局消息顺序,如何使用Kafka实现?

一、高吞吐低延迟 Kafka 集群设计要点 1. 分区策略优化 // 计算合理分区数公式&#xff08;动态调整&#xff09; int numPartitions max(Tp, Tc) / min(Tp, Tc) // Tp生产者吞吐量 Tc消费者吞吐量建议初始按业务键&#xff08;如订单ID&#xff09;哈希分区单分区吞吐建议…...

大模型工具与案例:云服务器部署dify(1)

如果您可以装wsl&#xff0c;可以在本机部署参考windows安装dify-江鸟阁长 因为笔者的windows电脑不可以安装wsl&#xff0c;所以本文会带大家在linux云服务器上部署。目前很多厂家都推出了一键部署&#xff0c;但是价格也有差 阿里云 通用型服务器 70rmb/月 华为云比较便宜&a…...

屏蔽力 | 在复杂世界中从内耗到成长的转变之道

注&#xff1a;本文为“屏蔽力”相关文章合辑。 略作重排&#xff0c;未全整理。 世上的事再复杂&#xff0c;不外乎这三种 原创 小鹿 读者 2022 年 12 月 02 日 18 : 27 甘肃 文 / 小鹿 在这世上&#xff0c;每天都有大事小事、琐事烦事。我们总为世事奔波忙碌&#xff0c;…...

信息系统项目管理师-软考高级(软考高项)​​​​​​​​​​​2025最新(十一)

个人笔记整理---仅供参考 第十一章项目成本管理 11.1管理基础 11.2项目成本管理过程 11.3规划成本管理 11.4估算成本 11.5制定预算 11.6控制成本...

大数据技术全景解析:Spark、Hadoop、Hive与SQL的协作与实战

引言&#xff1a;当数据成为新时代的“石油” 在数字经济时代&#xff0c;数据量以每年50%的速度爆发式增长。如何高效存储、处理和分析PB级数据&#xff0c;成为企业竞争力的核心命题。本文将通过通俗类比场景化拆解&#xff0c;带你深入理解四大关键技术&#xff1a;Hadoop、…...

Linux 驱动开发步骤及 SPI 设备驱动移植示例

Linux 驱动开发的一般步骤 硬件了解&#xff1a;深入研究目标硬件设备的工作原理、寄存器映射、电气特性、中断机制等。例如&#xff0c;若开发网卡驱动&#xff0c;需清楚网卡如何与网络介质交互、数据包的收发流程、硬件缓冲区的管理等。只有透彻理解硬件&#xff0c;才能编…...

直播数据大屏是什么?企业应如何构建直播数据大屏?

目录 一、直播数据大屏是什么&#xff1f; 1. 定义 2. 特点 ​编辑二、企业如何构建直播数据大屏&#xff1f; &#xff08;一&#xff09;明确需求和目标 &#xff08;二&#xff09;数据采集和整合 &#xff08;三&#xff09;选择合适的可视化工具 &#xff08;四&a…...

Vue与Python的深度整合:构建现代Web应用的全栈范式

在前后端分离架构成为行业标准的今天&#xff0c;Vue.js与Python的组合为全栈开发提供了高效且灵活的技术方案。这种组合不仅继承了Vue组件化开发的敏捷性&#xff0c;更借助Python后端框架&#xff08;如Django/Flask&#xff09;的强大生态&#xff0c;实现了从原型设计到生产…...

移动二维矩阵

1、题目描述 小红获得了一个 n行 m 列的二维字符矩阵&#xff0c;现在她要对这个字符矩阵进行向左循环移位。 向左循环移位规则如下&#xff1a;每一行的每一个字母(除了第一个字母)都向左边移动一位。第一行第一个的字母移动到最后一行的最后一个位置&#xff0c;其它行的第一…...

RabbitMq学习(第一天)

文章目录 1、mq(消息队列)概述2、RabbitMQ环境搭建3、java基于AMQP协议操作RabbitMQ4、基于Spring AMQP操作RabbitMQ5、代码中创建队列与交换机①、配置类创建②、基于RabbitListener注解创建 6、RabbitMQ详解①、work模型②、交换机1、Fanout(广播)交换机2、Direct(定向)交换机…...

基于RK3568多功能车载定位导航智能信息终端

基于安卓系统开发集成5G和4G模块&#xff0c;GPS/BD双模定位模块&#xff08;高精度差分惯导&#xff09;、WIFI模块&#xff0c;蓝 牙模块&#xff0c;RFID模块&#xff0c;音频播放&#xff0c;视频信号输入&#xff08;AHD或CVBS&#xff09;模块等多功能车载定位导航智能信…...

Facebook的元宇宙新次元:社交互动如何改变?

科技的浪潮正将我们推向一个全新的时代——元宇宙时代。Facebook&#xff0c;这个全球最大的社交网络平台&#xff0c;已经宣布将公司名称更改为 Meta&#xff0c;全面拥抱元宇宙概念。那么&#xff0c;元宇宙究竟是什么&#xff1f;它将如何改变我们的社交互动方式呢&#xff…...

【上位机——MFC】对话框

对话框的使用 1.添加对话框资源 2.定义一个自己的对话框类(CMyDlg)&#xff0c;管理对话框资源&#xff0c;派生自CDialog或CDialogEx均可 对话框架构 #include <afxwin.h> #include "resource.h"class CMyDlg :public CDialog {DECLARE_MESSAGE_MAP() publi…...

【信息系统项目管理师】法律法规与标准规范——历年考题(2024年-2020年)

手机端浏览☞【信息系统项目管理师】法律法规与标准规范——历年考题&#xff08;2024年-2020年&#xff09; 2024年上半年综合知识【占比分值3′】 42、关于招标投标的描述&#xff0c;不正确的是&#xff08;属于同一集团组织成员的投标人可以按照该组织要求协同投标&#xf…...

【HarmonyOS 5】鸿蒙Web组件和内嵌网页双向通信DEMO示例

【HarmonyOS 5】鸿蒙Web组件和内嵌网页双向通信DEMO示例 一、前言 在 ArkUI 开发中,Web 组件(Web)允许开发者在应用内嵌入网页,实现混合开发场景。 本文将通过完整 DEMO,详解如何通过WebviewController实现 ArkUI 与内嵌网页的双向通信,涵盖 ArkUI 调用网页 JS、网页调…...

var、let、const的区别

1. var 在ES5中&#xff0c;顶层对象的属性和全局变量是等价的&#xff0c;用var声明的变量即是全局变量&#xff0c;也是顶层变量&#xff0c;在浏览器中顶层对象指的是window对象&#xff0c;在node中顶层对象指的是global对象。 console.log(a) // undefined var a 1 cons…...

计算机视觉注意力机制【一】常用注意力机制整理

在做目标检测项目&#xff0c;尤其是基于 YOLOv5 或 YOLOv7 的改进实验时&#xff0c;我发现不同注意力机制对模型性能的提升确实有明显影响&#xff0c;比如提高小目标检测能力、增强特征表达等。但每次找代码都得翻论文、找 GitHub&#xff0c;效率很低。所以我干脆把常见的注…...

交替序列长度的最大值

1、题目描述 给出n个正整数&#xff0c;你可以随意从中挑选一些数字组成 一段序列S&#xff0c;该序列满足以下两个条件&#xff1a; 1.奇偶交替排列&#xff1a;例如&#xff1a;"奇&#xff0c;偶&#xff0c;奇&#xff0c;偶&#xff0c;奇.…" 或者 "偶&a…...

追踪大型语言模型的思想(下)(来自针对Claude的分析)

多步推理 正如我们上面所讨论的&#xff0c;语言模型回答复杂问题的一种方式就是简单地记住答案。例如&#xff0c;如果问“达拉斯所在州的首府是哪里&#xff1f;”&#xff0c;一个“机械”的模型可以直接学会输出“奥斯汀”&#xff0c;而无需知道德克萨斯州&#xff0c;达拉…...

嵌入式通信协议总览篇:万物互联的基石

嵌入式系统的世界,是靠协议“说话”的世界。 在你设计一个智能设备、构建一个工业控制系统、开发一款 IoT 网关时,一个核心问题始终绕不开:**这些设备之间如何“对话”?**答案就是——通信协议。 本篇作为系列第一章,将带你全面理解嵌入式通信协议的全貌,为后续深入学习…...

Android 连接德佟打印机全实例+踩坑

文章目录 1. sdk下载2. 开始开发2.1 打印之前准备工作2.2 打印机是否连接检测2.3 打印框架设计 最近有个需求是要连接 德佟打印机 进行打印相关事宜, 现在就遇到的问题简单阐述一下。 1. sdk下载 我们首先需要在官网下载对应的SDK&#xff0c;地址为&#xff1a;https://www.d…...

TikTok 矩阵运营新手实操保姆级教程 2.0 版本

在当下这个全球化的数字浪潮中&#xff0c;TikTok 这片充满机遇的流量蓝海&#xff0c;正吸引着无数创业者和品牌方争相角逐。而要想在这激烈的竞争中脱颖而出&#xff0c;TikTok 矩阵运营无疑是至关重要的制胜法宝。今天&#xff0c;就给大家送上这份超实用的新手实操教程&…...

WordPress:Locoy.php火车头采集

<?php /* 模块参数列表&#xff1a; post_title 必选 标题 post_content 必选 内容 tag 可选 标签 post_category 可选 分类 post_date 可选 时间 post_excerpt 可选 摘要 post_author 可选 作者 category_description 可选 分类信息 post_cate_meta[name] 可选 自定义分…...

C++ 有哪些标准版本

目录 1.主要分为以下几个版本C98&#xff08;ISO/IEC 14882:1998&#xff09; 第一个国际标准C03&#xff08;ISO/IEC 14882:2003&#xff09;小幅度修订C11&#xff08;ISO/IEC 14882:2011&#xff09;一次重大更新C14&#xff08;ISO/IEC 14882:2014&#xff09;增量改进C17&…...