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

C++并发编程 -3.同步并发操作

本文介绍如何使用条件变量控制并发的同步操作、C++ 并发三剑客,函数式编程

一.条件变量

1.概念

        C++条件变量(condition variable)是一种多线程编程中常用的同步机制,用于线程间的通信和协调。它允许一个或多个线程等待某个条件的发生,当条件满足时,线程被唤醒并继续执行。

        在C++中,条件变量通常与互斥锁(mutex)一起使用,以确保线程之间的安全访问共享资源。互斥锁用于保护共享资源的访问,而条件变量用于在某个条件满足时通知等待的线程。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>std::mutex mtx;
std::condition_variable cv;
bool isReady = false;void workerThread()
{std::unique_lock<std::mutex> lock(mtx);while (!isReady){cv.wait(lock); // 等待条件变量满足}std::cout << "Worker thread is awake!" << std::endl;
}int main()
{std::thread worker(workerThread);std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一些耗时操作{std::lock_guard<std::mutex> lock(mtx);isReady = true;cv.notify_one(); // 唤醒等待的线程}worker.join();return 0;
}

        主线程创建了一个工作线程,并在工作线程中等待条件变量isReadytrue。主线程在等待一段时间后,将isReady设置为true,并通过cv.notify_one()唤醒工作线程。工作线程在等待期间调用cv.wait(lock),这会使线程进入等待状态,直到条件变量被唤醒。一旦条件满足,工作线程被唤醒并输出消息。

        其中wait参数有三种使用方式:

waitwait_forwait_until
函数原型void wait(std::unique_lock<std::mutex>& lock);template< class Rep, class Period > std::cv_status wait_for(std::unique_lock<std::mutex>& lock, const std::chrono::duration<Rep, Period>& rel_time);template< class Clock, class Duration > std::cv_status wait_until(std::unique_lock<std::mutex>& lock, const std::chrono::time_point<Clock, Duration>& abs_time);
功能调用wait释放锁,等待notify。被唤醒后加锁

1.调用wait释放锁,等待notify。被唤醒后加锁。

2.等待时间超过指定的相对时间,无论是否超时仍加锁。

1.调用wait释放锁,等待notify。被唤醒后加锁。

2.等待时间超过指定的相对时间,无论是否超时仍加锁。

3.等待超过指定时间

返回值

等待时间内被唤醒,则返回std::cv_status::no_timeout;

如果等待时间超时,则返回std::cv_status::timeout

与waitfor一致
示例
cv.wait(lock, []{ return isDataReady; });

cv.wait_for(lock, std::chrono::seconds(5), []{ return isDataReady; }

auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(5);
cv.wait_until(lock, timeout, []{ return isDataReady; })

2.使用条件变量构建线程安全的栈区

        回顾

        在第三章节 -构建线程安全的栈区  为了避免数据竞争,于pop操作,我们如果在线程中调用empty判断是否为空,如果不为空,则pop,因为empty和pop内部分别加锁,是两个原子操作,导致pop时可能会因为其他线程提前pop导致队列为空,从而引发崩溃。

                     

void push(T new_value)
{std::lock_guard<std::mutex> lock(m);data.push(std::move(new_value));
}T pop()
{std::lock_guard<std::mutex> lock(m);auto element = data.top();data.pop();return element;
}

        通过添加《传入引用》或《返回弹出元素的指针》尽可能避免数据异常。传入引用弊端是需要在函数外部构建临时变量影响效率且需要抛出空栈。返回智能指针异常下会导致空指针产生,这么做并不是很友好,所以我们可以通过条件变量完善之前的程序,重新实现一个线程安全队列。

#include <iostream>
#include <thread>
#include <stack>
#include <memory>
#include <mutex>
#include <condition_variable>using namespace std;template<typename T>
class threadQueue
{
public:threadQueue(): mutx(), condVar(), stackQueue(){};~threadQueue(){};void push(T value){lock_guard<mutex> lk(mutx);stackQueue.push(value);condVar.notify_one();}void waitAndpop(T &stackQueue_){unique_lock<mutex> lk(mutx);/*wait函数第二个参数确保队列有数据才进行条件变量后续操作,相当于增加一层判断,更加准确*/condVar.wait(lk, [this](){return !stackQueue.empty();});stackQueue_ = stackQueue.top();stackQueue.pop();}shared_ptr<T> waitAndPop(){unique_lock<mutex> lk(mutx);condVar.wait(lk, [this](){return !stackQueue.empty();});shared_ptr<T> res = make_shared<T>(stackQueue.top());stackQueue.pop();return res;}bool tryPop(T &value){lock_guard<mutex> lk(mutx);if(stackQueue.empty()){return false;}value = stackQueue.top();stackQueue.pop();return true;}shared_ptr<T> tryPop(){lock_guard<mutex> lk(mutx);if (stackQueue.empty())return std::shared_ptr<T>();shared_ptr<T> res = make_shared<T>(stackQueue.top());stackQueue.pop();return res;}private:mutable mutex mutx;condition_variable condVar;stack<T> stackQueue;
};mutex mutxThread;template<typename T>
void funcProducer(threadQueue<T> &thdQueue)
{for(;;){for(size_t i = 0; i < __LONG_MAX__; i++){thdQueue.push(i);lock_guard<mutex> lk(mutxThread);cout<<"funcProducer:"<<i<<endl;this_thread::sleep_for(chrono::milliseconds(100));}}
}template<typename T>
void funcWorker1(threadQueue<T> &thdQueue)
{for(;;){auto data = thdQueue.waitAndPop();lock_guard<mutex> lk(mutxThread);cout<<"funcWorker1 waitAndpop:"<<*(data)<<endl;// this_thread::sleep_for(chrono::milliseconds(500));}
}template<typename T>
void funcWorker2(threadQueue<T> &thdQueue)
{for(;;){auto data = thdQueue.tryPop();if(data != nullptr){lock_guard<mutex> lk(mutxThread);cout<<"funcWorker2 waitAndpop:"<<*(data)<<endl;}// this_thread::sleep_for(chrono::milliseconds(500));}
}int main()
{threadQueue<int> thdQueue;thread t1(funcProducer<int>, ref(thdQueue));thread t2(funcWorker1<int>, ref(thdQueue));thread t3(funcWorker2<int>, ref(thdQueue));t1.join();t2.join();t3.join();
}

二.C++ 并发三剑客

        简单介绍一下future, promise和async三者之间的关系。其具体实现请看本章节第四小节

1.async

        std::async 是一个用于异步执行函数的模板函数,它返回一个 std::future 对象,该对象用于获取函数的返回值。其主要应用于I/O密集型任务,如网络请求或文件读写,其中操作可能需要等待,但CPU不需要做太多工作.(thread适用于CPU密集型任务)

        async可选填参数:

        指定launch::async:表示任务执行在另一线程。

        指定launch::deferred:表示延迟执行任务,调用get或者wait时才会执行,不会创建线程,惰性执行在当前线程。

        不填写或同时指定:属于未定义行为,编译器会根据实际情况决定采用哪种策略(通常创建线程)

1.1 async异步任务

        async启用异步执行后,在其内部会创建一个线程,任务完成后线程自动销毁,不需要join等操作。


#include <iostream>
#include <thread>
#include <memory>
#include <future>using namespace std;void func()
{int i = 0;for(;;){cout<<"func"<<endl;this_thread::sleep_for(chrono::seconds(1));if(++i == 10) {break;}}cout<<" thread end"<<endl;
}int main()
{std::future<void> the_answer=std::async(launch::async, func);//deferredfor(int i = 0; i < 15; ++i){cout<<"main run"<<endl;this_thread::sleep_for(chrono::seconds(1));}return 1;
}

  刚运行时可以看到存在两个线程

  线程退出后:

1.2 async延时执行

        当指定launch::deferred表示延迟执行任务,调用get或者wait时才会在调用线程中执行。                 std::future::get() 和 std::future::wait() 是 C++ 中用于处理异步任务的两个方法.

1.2.1 future::get()

        这是一个阻塞调用,用于获取 std::future 对象表示的值或异常。如果异步任务还没有完成,get() 会阻塞当前线程,直到任务完成。如果任务已经完成,get() 会立即返回任务的结果。get() 只能调用一次,因为它会移动或消耗掉 std::future 对象的状态。一旦 get() 被调用,std::future 对象就不能再被用来获取结果。如果调用两次,会出现如下结果:

#include <iostream>
#include <thread>
#include <memory>
#include <future>using namespace std;void func()
{cout<<"func"<<endl;
}int main()
{std::future<void> the_answer=std::async(launch::deferred, func);//deferredcout<<"main run"<<endl;this_thread::sleep_for(chrono::seconds(1));the_answer.get();the_answer.get();return 1;
}

                 

        为避免多次调用get造成异常,尽量使用wait

1.2.2 future::wait()

        std::future::wait() 也是一个阻塞调用,但它与 get() 的主要区别在于 wait() 不会返回任务的结果。它只是等待异步任务完成。如果任务已经完成,wait() 会立即返回。如果任务还没有完成,wait() 会阻塞当前线程,直到任务完成。与 get() 不同,wait() 可以被多次调用,它不会消耗掉 std::future 对象的状态。

#include <iostream>
#include <thread>
#include <memory>
#include <future>using namespace std;int func()
{std::this_thread::sleep_for(std::chrono::milliseconds(1000));cout<<"func"<<endl;return 10;
}int main()
{future<int> the_answer = async(launch::deferred, func);cout<<"main run"<<endl;auto start = std::chrono::high_resolution_clock::now(); the_answer.wait();the_answer.wait();auto end = std::chrono::high_resolution_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); cout << "the_answer.wait run time: " << duration.count() << " ms" << endl;return 1;
}

         

可以看到只运行了1000ms, 当第一个wait运行结束表示任务执行结束,而第二个wait会立马返回。

waitget
阻塞线程,知道任务运行完毕阻塞线程,知道任务运行完毕
可多次调用,当任务执行完毕后wait将不在起作用,会立马返回。不可多次调用,异常
执行任务,不获取结果执行任务,获取结果
  •  也可以通过wait_for(std::chrono::seconds(0)(要求任务立马返回),判断任务是否已经完成
if (the_answer.wait_for(std::chrono::seconds(0)) == std::future_status::ready)
{cout<<"task already over"<<endl;return 0;
}

2.promise和packaged_task

        promise和packaged_task都是C++中的异步编程工具,用于在多线程环境下进行任务的分离和结果的传递。

        promise是一种用于在一个线程中产生结果,并在另一个线程中获取结果的机制。它允许一个线程(称为"提供者")在某个时间点设置一个值或异常,而另一个线程(称为"消费者")在需要时获取该值或异常。

        packaged_task是一种将可调用对象(如函数、函数对象或Lambda表达式)封装为一个可以异步执行的任务的机制。

        两者都是与future结合使用,通过future获取结果或者值。

2.1.future与packaged_task

        packaged_task是一个可调用目标,它包装了一个任务,该任务可以在另一个线程上运行。它可以捕获任务的返回值或异常,并将其存储在future对象中,以便以后使用。

        使用future和packaged_task通常的做法是:

1)  创建一个std::packaged_task对象,该对象包装了要执行的任务。

funciton my_task
packaged_task<int()> task(my_task);

2)  调用packaged_task对象的get_future()方法,该方法返回一个与任务关联的future对象。

future<int> result = task.get_future();

3)  在另一个线程上调用std::packaged_task对象的operator(),以执行任务。

 thread t(move(task));

4)   在需要任务结果的地方,调用与任务关联的future对象的get()方法,以获取任务的返回值或异常。

int value = result.get();

示例:

#include <iostream>
#include <thread>
#include <memory>
#include <future>using namespace std;int my_task() {this_thread::sleep_for( chrono::seconds(5));cout << "my task run 5 s" <<  endl;return 42;
}
int main()
{// 创建一个包装了任务的  packaged_task 对象  packaged_task<int()> task(my_task);// 获取与任务关联的  future 对象  future<int> result = task.get_future();// cout<<"task.get_future()"<<task.get_future().get()<<endl;// 在另一个线程上执行任务  thread t(move(task));t.detach(); // 将线程与主线程分离,以便主线程可以等待任务完成  // 等待任务完成并获取结果  cout << "will get result"<<endl;int value = result.get();cout << "The result is: " << value <<  endl;}

                         

        在上面的示例中,我们创建了一个包装了任务的std::packaged_task对象,并获取了与任务关联的std::future对象。然后,我们在另一个线程上执行任务,并等待任务完成并获取结果。最后,我们输出结果。

2.2.future与promise
2.2.1 设置值

        packaged_task主要是异步过程中任务,promise在异步过程中处理数值。

#include <iostream>
#include <thread>
#include <future>void set_value(std::promise<int>& prom) {std::this_thread::sleep_for(std::chrono::seconds(2));// 设置结果为42prom.set_value(42);
}int main() {std::promise<int> prom;std::future<int> fut = prom.get_future();std::thread t(set_value, std::ref(prom));// 在主线程中等待结果int result = fut.get();std::cout << "Result: " << result << std::endl;t.join();return 0;
}

        值得注意的是在调用fut.get()方法时,如果promise的值还没有被设置,则该方法会阻塞当前线程,直到值被设置为止。

2.2.2 设置异常

        promise还有一个set_exception()方法,用于设置异常。该方法接受std::exception_ptr参数,该参数可以通过调用std::current_exception()方法获取。


#include <iostream>
#include <thread>
#include <future>void set_exception(std::promise<int>& prom) {try {// 模拟一些可能抛出异常的操作throw std::runtime_error("Something went wrong");} catch (...) {// 捕获异常并设置到 promise 中prom.set_exception(std::current_exception());}
}int main() {std::promise<int> prom;std::future<int> fut = prom.get_future();std::thread t(set_exception, std::ref(prom));try {// 在主线程中等待结果int result = fut.get();std::cout << "Result: " << result << std::endl;} catch (const std::exception& e) {std::cout << "Exception caught: " << e.what() << std::endl;}t.join();return 0;
}

        set_exception 函数中,我们模拟了一些可能抛出异常的操作,并使用prom.set_exception (std::current_exception()) 将当前捕获的异常设置到 promise 中。在主线程中,我们使用 fut.get() 等待并获取结果。如果在另一个线程中发生了异常,我们可以在主线程中使用 try-catch 块捕获并处理异常。

3 future与shared_future

        当需要多个线程等待同一个执行结果时,需要使用std::shared_future。

        适合使用std::shared_future的场景,多个线程等待同一个异步操作的结果:

#include <iostream>
#include <thread>
#include <future>void myFunction(std::promise<int>&& promise) 
{std::this_thread::sleep_for(std::chrono::seconds(1));promise.set_value(42); // 设置 promise 的值
}
void threadFunction(std::shared_future<int> future) 
{try {int result = future.get();std::cout << "Result: " << result<<"this thread id:"<<std::this_thread::get_id() << std::endl;}catch (const std::future_error& e) {std::cout << "Future error: " << e.what() << std::endl;}
}
int main() 
{std::promise<int> promise;std::shared_future<int> future = promise.get_future();std::thread myThread1(myFunction, std::move(promise)); // 将 promise 移动到线程中// 使用 share() 方法获取新的 shared_future 对象  std::thread myThread2(threadFunction, future);std::thread myThread3(threadFunction, future);myThread1.join();myThread2.join();myThread3.join();return 0;
}

         

        在这个示例中,创建了一个std::promise<int>对象promise和一个与之关联的std::shared_future<int>对象future。然后,我们将promise对象移动到另一个线程myThread1中,该线程将执行myFunction函数,并在完成后设置promise的值。我们还创建了两个线程myThread2myThread3,它们将等待future对象的结果。如果myThread1成功地设置了promise的值,那么future.get()将返回该值。这些线程可以同时访问和等待future对象的结果,而不会相互干扰。 

4. 自顶向下实现

4.1  用packaged_task实现async

        async异步任务相当于创建线程,不再予以实现。

        async延时执行示例如下:

#include <iostream>
#include <thread>
#include <future>
#include <functional>using Func = std::function<int(int)>;std::future<int> myAsync(Func task, int i)
{std::packaged_task<int(int)> packaged(task);std::future<int> fu = packaged.get_future();std::thread t(std::move(packaged), i);t.detach();return fu;
}int main()
{auto task = [](int i) { std::this_thread::sleep_for(std::chrono::seconds(1)); return i+100; };std::future<int> f = myAsync(task, 5);std::cout << f.get() << std::endl;return 0;
}

         async指定deferred 表示延时进行,其内部与上述内部实现类似,先使用packaged打包需要执行的任务,然后将任务与future绑定,将任务move到线程中,执行完毕后通过future.get获取任务执行结果。如此便满足第二章节图片内容结构。

4.2  使用promise实现packaged_task

          通过promise设置值来完成packaged_task反馈。

#include <iostream>
#include <thread>
#include <future>
#include <functional>//typename关键字在C++中的主要作用是告诉编译器某个名称是一个类型而不是变量或函数。
template<typename Type, typename... Args>
class myPackTask
{
public:myPackTask(std::function<Type(Args...)> _func):func_(_func){}std::future<Type> get_future(){return promise_.get_future();}void operator()(Args... args){Type result = func_(args...);promise_.set_value(result);}private:std::function<Type(Args...)> func_;std::promise<Type> promise_;
};int add(int a, int b) {return a + b;
}int main() {myPackTask<int, int, int> task(add);std::future<int> future = task.get_future(); std::thread thread(std::move(task), 2, 3);  int result = future.get();  std::cout << "Result: " << result << std::endl;thread.join();  // 等待线程结束return 0;
}
 4.3  实现promise

        其主要思想就是通过条件变量通知get所在线程调用的地方。

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>template<typename T>
class Promise {
public:Promise() : value(nullptr), ready(false) {}void set(const T& val) {std::unique_lock<std::mutex> lock(mutex);value = std::make_shared<T>(val);ready = true;condition.notify_all();}T get() {std::unique_lock<std::mutex> lock(mutex);condition.wait(lock, [this] { return ready; });return *value;}private:std::shared_ptr<T> value;bool ready;std::mutex mutex;std::condition_variable condition;
};int main() {Promise<int> promise;std::thread producer([&promise]() {std::this_thread::sleep_for(std::chrono::seconds(2));promise.set(42);});std::thread consumer([&promise]() {int result = promise.get();std::cout << "Got value: " << result << std::endl;});producer.join();consumer.join();return 0;
}

三.并行与函数式编程

以下内容摘自恋恋风尘,博主讲解C++并发不错,力荐

1 快速排序

        先使用C++ 设计一个快速排序方法

#include <iostream>
#include <thread>
#include <memory>
#include <future>template<typename T>
void quick_sort_recursive(T arr[], int start, int end) {if (start >= end) return;T key = arr[start];int left = start, right = end;while(left < right) {while (arr[right] >= key && left < right) right--;while (arr[left] <= key && left < right) left++;std::swap(arr[left], arr[right]);}if (arr[left] < key) {std::swap(arr[left], arr[start]);}quick_sort_recursive(arr, start, left - 1);quick_sort_recursive(arr, left + 1, end);
}
template<typename T>
void quick_sort(T arr[], int len) {quick_sort_recursive(arr, 0, len - 1);
}int main() {int num_arr[] = { 5,3,7,6,4,1,0,2,9,10,8 };int length = sizeof(num_arr) / sizeof(int);quick_sort(num_arr, length );std::cout << "sorted result is ";for (int i = 0; i < length; i++) {std::cout << " " << num_arr[i];}std::cout << std::endl;    
}

2 使用函数式编程快速排序

        函数式编程可以被看作是将函数视为数学中的公式。在函数式编程中,函数被视为一种纯粹的数学映射,它接受输入并产生输出,没有副作用或可变状态。这种思想源于数学中的函数概念,其中函数被定义为将输入映射到输出的关系。

        在第一小节实现快速排序是一种面向过程的编程方式,我们可以通过定义一个函数来实现快速排序算法。这个函数接受一个数组作为输入,并通过一系列的步骤来实现快速排序的逻辑。这些步骤包括选择基准元素、划分数组、递归调用等。

        而使用函数式编程,强调使用纯函数来构建程序,通过函数的组合和转换来处理数据。函数式编程避免使用可变状态和共享状态。

#include <iostream>
#include <thread>
#include <memory>
#include <future>
#include <list>
#include <algorithm>template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input)
{if (input.empty()){return input;}std::list<T> result;//  ① 将input中的第一个元素放入result中,并且将这第一个元素从input中删除result.splice(result.begin(), input, input.begin());  //  ② 取result的第一个元素,将来用这个元素做切割,切割input中的列表。T const& pivot = *result.begin();    //  ③std::partition 是一个标准库函数,用于将容器或数组中的元素按照指定的条件进行分区,// 使得满足条件的元素排在不满足条件的元素之前。// 所以经过计算divide_point指向的是input中第一个大于等于pivot的元素auto divide_point = std::partition(input.begin(), input.end(),[&](T const& t) {return t < pivot; });    // ④ 我们将小于pivot的元素放入lower_part中std::list<T> lower_part;lower_part.splice(lower_part.end(), input, input.begin(),divide_point);  // ⑤我们将lower_part传递给sequential_quick_sort 返回一个新的有序的从小到大的序列//lower_part 中都是小于divide_point的值auto new_lower(sequential_quick_sort(std::move(lower_part)));    // ⑥我们剩余的input列表传递给sequential_quick_sort递归调用,input中都是大于divide_point的值。auto new_higher(sequential_quick_sort(std::move(input)));    //⑦到此时new_higher和new_lower都是从小到大排序好的列表//将new_higher 拼接到result的尾部result.splice(result.end(), new_higher);    //将new_lower 拼接到result的头部result.splice(result.begin(), new_lower);   return result;
}int main() {std::list<int> numlists = { 6,1,0,7,5,2,9,-1 };auto sort_result = sequential_quick_sort(numlists);std::cout << "sorted result is ";for (auto iter = sort_result.begin(); iter != sort_result.end(); iter++) {std::cout << " " << (*iter);}std::cout << std::endl;
}

3 并行处理快速排序

        在快速排序中,我们以某个数字作为基准(一般开头), 分为前半部分和后半部分,然后前半部分和后半部分单独计算。 

        并行排序的思想是将前半部分(或后半部分)单独拷贝一份,置于async中(不指定启动方式,编译器会根据计算次数决定什么时候异步执行什么时候串行),进行并行计算

#include <iostream>
#include <thread>
#include <memory>
#include <future>
#include <list>
#include <algorithm>template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{if (input.empty()){return input;}std::list<T> result;result.splice(result.begin(), input, input.begin());T const& pivot = *result.begin();auto divide_point = std::partition(input.begin(), input.end(),[&](T const& t) {return t < pivot; });std::list<T> lower_part;lower_part.splice(lower_part.end(), input, input.begin(),divide_point);// ①因为lower_part是副本,所以并行操作不会引发逻辑错误,这里可以启动future做排序std::future<std::list<T>> new_lower(std::async(&parallel_quick_sort<T>, std::move(lower_part)));// ②auto new_higher(parallel_quick_sort(std::move(input)));    result.splice(result.end(), new_higher);    result.splice(result.begin(), new_lower.get());    return result;
}int main() 
{std::list<int> numlists = { 6,1,0,7,5,2,9,-1 };auto sort_result = parallel_quick_sort(numlists);std::cout << "sorted result is ";for (auto iter = sort_result.begin(); iter != sort_result.end(); iter++) {std::cout << " " << (*iter);}std::cout << std::endl;
}

相关文章:

C++并发编程 -3.同步并发操作

本文介绍如何使用条件变量控制并发的同步操作、C 并发三剑客&#xff0c;函数式编程 一.条件变量 1.概念 C条件变量&#xff08;condition variable&#xff09;是一种多线程编程中常用的同步机制&#xff0c;用于线程间的通信和协调。它允许一个或多个线程等待某个条件的发生…...

【打工日常】使用docker部署可视化工具docker-ui

一、docker-ui介绍 docker-ui是一个易用且轻量化的Docker管理工具&#xff0c;透过Web界面的操作&#xff0c;方便快捷操作docker容器化工作。 docker-ui拥有易操作化化界面&#xff0c;不须记忆docker指令&#xff0c;仅需下载镜像即可立刻加入完成部署。基于docker的特性&…...

LGAMEFI基于BPL公链开发的第一生态:开启RWA游戏娱乐与DeFi融合的新纪元

在去中心化金融&#xff08;DeFi&#xff09;与游戏娱乐的结合趋势中&#xff0c;BPL公链上的LGAMEFI项目代表了前沿的技术革新和市场领导。这种将web2上成熟页游进行RWA链改&#xff0c;不仅仅是将游戏热门领域融合&#xff0c;更是在寻找一种全新的参与者经验&#xff0c;将玩…...

AI专题:5G-A扬帆风正劲,踏AI增长新浪潮

今天分享的是AI系列深度研究报告&#xff1a;《AI专题&#xff1a;5G-A扬帆风正劲&#xff0c;踏AI增长新浪潮》。 &#xff08;报告出品方&#xff1a;开源证券&#xff09; 报告共计&#xff1a;22页 足立连接&#xff0c;拓展算力&#xff0c;双曲线稳步发力 中兴通讯拥…...

C++Linux网络编程:poll模型和简单使用

文章目录 poll模型pollfd结构体nfds_t的定义 一个简单的poll服务器总结 poll模型 poll模型和select模型类似&#xff0c;都是在指定时间内轮询一定数量的文件描述符&#xff0c;以测试其中是否有就绪者&#xff0c;需要使用头文件poll.h&#xff1a; #include <poll.h>…...

Excel模板2:进度条甘特图

Excel模板2&#xff1a;进度条甘特图 ‍ 今天复刻B站up【名字叫麦兜的狗狗】的甘特图&#xff1a;还在买Excel模板吗&#xff1f;自己做漂亮简洁的甘特图吧&#xff01;_哔哩哔哩_bilibili 阿里网盘永久分享&#xff1a;https://www.alipan.com/s/cXhq1PNJfdm 当前效果&…...

数据结构:4_二叉树

二叉树 一.树概念及结构 1. 树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。把它叫做树是因为它看起来像一棵倒挂的树&#xff0c;也就是说它是根朝上&#xff0c;而叶朝下的。 有一个**特殊的…...

设计模式之:状态模式(State Pattern)

状态模式&#xff08;State Pattern&#xff09; 状态模式是一种行为设计模式&#xff0c;允许一个对象在其内部状态改变时改变它的行为。这种模式通过把状态的变化逻辑分布到State的子类之间&#xff0c;减少了相互间的依赖&#xff0c;使得状态的切换更加清晰。 状态模式的…...

【微服安全】API密钥和令牌与微服务安全的关系

什么是 API 密钥和令牌 API 密钥 API 密钥是一串用于识别应用程序或用户的字符串。它通常用于授权应用程序或用户访问 API。API 密钥可以是公开的&#xff0c;也可以是私有的。公开的 API 密钥可供任何人使用&#xff0c;而私有的 API 密钥只能由授权的应用程序或用户使用。 …...

Mock.js

在开发后端的应用中&#xff0c;我们使用postman来测试接口&#xff0c;观察和验证前后端之间的数据传递是否正常。 在开发前端的应用中&#xff0c;我们使用Mock.js来模拟后端服务&#xff0c;以便进行前端业务逻辑的开发和测试。 一般情况下&#xff0c;个人开发或者小团队开…...

【c++】list详细讲解

> 作者简介&#xff1a;დ旧言~&#xff0c;目前大二&#xff0c;现在学习Java&#xff0c;c&#xff0c;c&#xff0c;Python等 > 座右铭&#xff1a;松树千年终是朽&#xff0c;槿花一日自为荣。 > 目标&#xff1a;熟悉list库 > 毒鸡汤&#xff1a;你的脸上云淡…...

C#面:在.NET中 类 System.Web.UI.Page 可以被继承吗?

可以。 它是 ASP.NET WebForms中的一个重要类&#xff0c;用于表示 Web 页面。通过继承 System.Web.UI.Page 类&#xff0c;可以创建自定义的 Web 页面&#xff0c;并在其中添加自己的逻辑和功能。 继承 System.Web.UI.Page 类的好处是&#xff0c;可以重用和扩展已有的功能。…...

AI:128-基于机器学习的建筑物能源消耗预测

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…...

php基础学习之可变函数(web渗透测试关键字绕过rce和回调函数)

可变函数 看可变函数的知识点之前&#xff0c;蒟蒻博主建议你先去看看php的可变变量&#xff0c;会更加方便理解&#xff0c;在本篇博客中的第五块知识点->php基础学习之变量-CSDN博客 描述 当一个变量所保存的值刚好是一个函数的名字&#xff08;由函数命名规则可知该值必…...

MongoDB聚合操作符:$acos

$acos操作符返回一个值的反余弦。从MongoDB4.2版本开始支持。 语法 { $acos: <expression> }$acos接受任何可被解析为值在-1到1之间的表达式&#xff0c;即&#xff1a;-1 < value < 1$acos返回值以弧度为单位&#xff0c;使用$radiansToDegrees操作符可以把输出…...

开源PDF工具 Apache PDFBox 认识及使用(知识点+案例)

文章目录 前言源码获取一、认识PDFBox二、导入依赖三、基础功能demo1&#xff1a;读取pdf所有内容demo2&#xff1a;读取所有页内容&#xff08;分页&#xff09;demo3&#xff1a;添加页眉、页脚demo4&#xff1a;添加居中45文字水印demo5&#xff1a;添加图片到右上角 参考文…...

微软.NET6开发的C#特性——委托和事件

我是荔园微风&#xff0c;作为一名在IT界整整25年的老兵&#xff0c;看到不少初学者在学习编程语言的过程中如此的痛苦&#xff0c;我决定做点什么&#xff0c;下面我就重点讲讲微软.NET6开发人员需要知道的C#特性&#xff0c;然后比较其他各种语言进行认识。 C#经历了多年发展…...

卷积神经网络的基本结构

卷积神经网络的基本结构 与传统的全连接神经网络一样&#xff0c;卷积神经网络依然是一个层级网络&#xff0c;只不过层的功能和形式发生了变化。 典型的CNN结构包括&#xff1a; 数据输入层&#xff08;Input Layer&#xff09;卷积层&#xff08;Convolutional Layer&#x…...

python:使用GDAL库读取遥感影像指定行列数/经纬度坐标的像素值

作者:CSDN @ _养乐多_ 本文将介绍如何使用GDAL库来读取单波段遥感影像数据,如何获取指定行列位置的像素的经纬度坐标,并根据像素行列数或者经纬度坐标获取像素值。代码由python实现。 文章目录 一、读取影像二、获取指定行列位置的像素坐标三、根据地理坐标获取像素值四、根…...

Redis篇----第一篇

系列文章目录 文章目录 系列文章目录前言一、什么是 Redis?二、Redis 与其他 key-value 存储有什么不同?三、Redis 的数据类型?四、使用 Redis 有哪些好处?五、Redis 相比 Memcached 有哪些优势?前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住…...

C语言-----用二维数组解决菱形的打印问题

1.打印菱形&#xff0c;多组输入&#xff0c;一个整数&#xff08;2~20&#xff09;&#xff0c;表示输出的行数&#xff0c;也表示组成“X”的反斜线和正斜线的长度。 #include <stdio.h>int main() {int n0;while(scanf("%d",&n)! EOF){int i0;int j0;f…...

.NET Core WebAPI中使用swagger版本控制,添加注释

一、效果 二、实现步骤 在代码中添加注释 在项目属性中生成API文档 在Program中注册Swagger服务并配置文档信息 // 添加swagger注释 builder.Services.AddSwaggerGen(x > {x.SwaggerDoc("v1", new OpenApiInfo { Title "Swagger标题", Version "…...

css篇---移动端适配的方案有哪几种

移动端适配 移动端适配是指同一个页面可以在不同的移动端设备上都有合理的布局。主流实现的方案有 响应式布局通过rem或者vw,vh 等实现不同设备有相同的比例而实现适配 首先需要了解viewport 【视口】 视口代表了一个可看见的多边形区域&#xff08;通常来说是矩形&#xff0…...

一、部署Oracle

部署Oracle 一、Docker部署1.Oracle11g1.1 测试环境1.1.1 拉取镜像1.1.2 启动容器1.1.3 配置容器环境变量1.1.4 修改sys、system用户密码1.1.5 创建表空间1.1.6 创建用户并授权1.1.5 使用DBeaver测试连接 二、安装包部署 一、Docker部署 1.Oracle11g 1.1 测试环境 当前只能用…...

11-编写自动化测试

上一篇&#xff1a; 10-通用类型、特质和生命周期 Edsger W. Dijkstra 在 1972 年发表的文章《The Humble Programmer》中说&#xff1a;"程序测试可以非常有效地显示错误的存在&#xff0c;但对于显示错误的不存在却无能为力。这并不意味着我们不应该尽可能多地进行测试&…...

爱上JVM——常见问题(一):JVM组成

1 JVM组成 1.1 JVM由那些部分组成&#xff0c;运行流程是什么&#xff1f; 难易程度&#xff1a;☆☆☆ 出现频率&#xff1a;☆☆☆☆ JVM是什么 Java Virtual Machine Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&…...

C#系列-EF扩展框架AutoMapper应用实例(40)

AutoMapper是一个对象到对象的映射器&#xff0c;它用于将一个对象的属性映射到另一个对象的属性。它主要用于在应用程序的不同层之间传输数据时&#xff0c;自动将数据传输对象&#xff08;DTOs&#xff09;映射到领域模型&#xff0c;或者将领域模型映射到视图模型等。 下面…...

DataX源码分析-插件机制

系列文章目录 一、DataX详解和架构介绍 二、DataX源码分析 JobContainer 三、DataX源码分析 TaskGroupContainer 四、DataX源码分析 TaskExecutor 五、DataX源码分析 reader 六、DataX源码分析 writer 七、DataX源码分析 Channel 八、DataX源码分析-插件机制 文章目录 系列文章…...

容器高级知识: 适配器模式与 Sidecar 模式的区别

适配器模式与 Sidecar 模式的区别 在 Kubernetes 中&#xff0c;适配器模式和 Sidecar 模式都是扩展您的主应用程序容器功能的方法&#xff0c;但它们具有不同的目的和功能&#xff1a; Sidecar 模式&#xff1a; 通用目的&#xff1a; 为主应用程序提供 补充功能&#xff0…...

使用Xdisplay将ipad作为扩展显示器Agent闪退问题

1. 正常流程贴子挺多&#xff0c;可以参考这几篇 https://blog.csdn.net/Shi_Xueqing/article/details/129744496 如何将ipad作为win10的扩展屏&#xff08;使用USB线连接&#xff09;_ipad win usb 上网-CSDN博客 2.在进行pc端软件设置的时候发生闪退 解决方法&#xff1a…...

DVXplorer事件相机入门

DV官方文档&#xff1a;Get Started DV (inivation.gitlab.io) DV事件相机的ROS包&#xff1a;https://github.com/uzh-rpg/rpg_dvs_ros 事件相机的资源汇总&#xff1a;https://github.com/uzh-rpg/event-based_vision_resources 1.DV事件相机ROS包驱动程序安装 注意&#x…...

ubuntu屏幕小的解决办法

1. 安装vmware tools , 再点自适应客户机 执行里面的vmware-install.pl这个文件 &#xff1a;sudo ./vmware-install.pl 执行不了可以放到家目录&#xff0c;我放在了/home/book 里面 最后点这个自适应客户机 然后我这里点不了是因为我点了控制台视图和拉伸客户机&#xff0c…...

黑群晖一键修复:root、AME、DTS、转码、CPU型号等

食用方法&#xff1a;SSH连接群晖使用临时root权限执行 AME3.x激活补丁 只适用于x86_64的&#xff1a;DSM7.x Advanced Media Extensions (AME)版本3.0.1-2004、3.1.0-3005 激活过程需要下载官方的解码包&#xff0c;过程较慢&#xff0c;耐心等待。。。 DSM7.1和7.2的AME版…...

Repo命令使用实例(三十八)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…...

2024年华为OD机试真题-分披萨-Python-OD统一考试(C卷)

题目描述: “吃货”和“馋嘴”两人到披萨店点了一份铁盘(圆形)披萨,并嘱咐店员将披萨按放射状切成大小相同的偶数扇形小块。但是粗心服务员将披萨切成了每块大小都完全不同奇数块,且肉眼能分辨出大小。 由于两人都想吃到最多的披萨,他们商量了一个他们认为公平的分法:从…...

找负环(图论基础)

文章目录 负环spfa找负环方法一方法二实际效果 负环 环内路径上的权值和为负。 spfa找负环 两种基本的方法 统计每一个点的入队次数&#xff0c;如果一个点入队了n次&#xff0c;则说明存在负环统计当前每个点中的最短路中所包含的边数&#xff0c;如果当前某个点的最短路所…...

无人机飞控算法原理基础研究,多旋翼无人机的飞行控制算法理论详解,无人机飞控软件架构设计

多旋翼无人机的飞行控制算法主要涉及到自动控制器、捷联式惯性导航系统、卡尔曼滤波算法和飞行控制PID算法等部分。 自动控制器是无人机飞行控制的核心部分&#xff0c;它负责接收来自无人机传感器和其他系统的信息&#xff0c;并根据预设的算法和逻辑&#xff0c;对无人机的姿…...

关于内存相关的梳理

1 关键字 总结 &#xff08;lowmemory&#xff0c;anr in&#xff09; 2 知识储备 虚拟机原理 垃圾回收算法 又包含标记 和清除两种算法 标记&#xff1a;程序计数器-已过时&#xff0c;可达性分析 具体可见 http://help.eclipse.org/luna/index.jsp?topic%2Forg.ec…...

7.JS里表达式,if条件判断,三元运算符,switch语句,断点调试

表达式和语句的区别 表达式就是可以被求值的代码比如什么a 1 语句就是一段可以执行的代码比如什么if else 直接给B站的黑马程序员的老师引流一波总结的真好 分支语句 就是基本上所有的语言都会有的if else 语句就是满足不同的条件执行不同的代码&#xff0c;让计算机有条件…...

RK3568平台开发系列讲解(存储篇)文件句柄与文件描述符介绍

🚀返回专栏总目录 文章目录 一、什么是文件句柄二、什么是文件描述符2.1、files_struct 结构体2.2、fdtable 结构体三、数据结构关系图沉淀、分享、成长,让自己和他人都能有所收获!😄 一、什么是文件句柄 用户空间的进程通过open系统调用打开一个文件之后,内核返回的就是…...

【C++】类和对象(五)友元、内部类、匿名对象

前言&#xff1a;前面我们说到类和对象是一个十分漫长的荆棘地&#xff0c;今天我们将走到终点&#xff0c;也就是说我们对于&#xff23;算是正式的入门了。 &#x1f496; 博主CSDN主页:卫卫卫的个人主页 &#x1f49e; &#x1f449; 专栏分类:高质量&#xff23;学习 &…...

攻防世界 CTF Web方向 引导模式-难度1 —— 1-10题 wp精讲

目录 view_source robots backup cookie disabled_button get_post weak_auth simple_php Training-WWW-Robots view_source 题目描述: X老师让小宁同学查看一个网页的源代码&#xff0c;但小宁同学发现鼠标右键好像不管用了。 不能按右键&#xff0c;按F12 robots …...

Docker之MongoDB安装、创建用户及登录认证

Docker之MongoDB安装、创建用户及登录认证 文章目录 Docker之MongoDB安装、创建用户及登录认证1. 拉取镜像2. 创建宿主机容器数据卷3. 运行mongodb容器1. 运行容器2. 创建用户3. 创建数据库并设置密码 1. 拉取镜像 docker pull mongo:4.2.212. 创建宿主机容器数据卷 运行docke…...

紫微斗数双星组合:天机天梁在辰戌

文章目录 前言内容总结 前言 紫微斗数双星组合&#xff1a;天机天梁在辰戌 内容 紫微斗数双星组合&#xff1a;天机天梁在辰戌 性格分析 在紫微斗数命盘中&#xff0c;天梁星是一颗“荫星”&#xff0c;能够遇难呈祥&#xff0c;化解凶危&#xff0c;主寿&#xff0c;主贵。…...

N-144基于微信小程序在线订餐系统

开发工具&#xff1a;IDEA、微信小程序 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 前端技术&#xff1a;vue、ElementUI、 Vant Weapp 服务端技术&#xff1a;springbootmybatisredis 本系统分微信小程序和…...

[UI5 常用控件] 09.IconTabBar,IconTabHeader,TabContainer

文章目录 前言1. IconTabBar1.1 简介1.2 基本结构1.3 用法1.3.1 颜色&#xff0c;拖放&#xff0c;溢出1.3.2 Icons Only , Inner Contents1.3.3 showAll,Count,key,IconTabSeparator 1.3.4 Only Text1.3.5 headerMode-Inline1.3.6 design,IconTabSeparator-icon1.3.7 DensityM…...

CCF编程能力等级认证GESP—C++5级—20231209

CCF编程能力等级认证GESP—C5级—20231209 单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09;判断题&#xff08;每题 2 分&#xff0c;共 20 分&#xff09;编程题 (每题 25 分&#xff0c;共 50 分)小杨的幸运数烹饪问题 答案及解析单选题判断题编程题1编程题2 单…...

【论文精读】GPT2

摘要 在单一领域数据集上训练单一任务的模型是当前系统普遍缺乏泛化能力的主要原因&#xff0c;要想使用当前的架构构建出稳健的系统&#xff0c;可能需要多任务学习。但多任务需要多数据集&#xff0c;而继续扩大数据集和目标设计的规模是个难以处理的问题&#xff0c;所以只能…...

10-k8s中pod的探针

一、探针的概念 一般时候&#xff0c;探针的设置&#xff0c;都是为了优化业务时&#xff0c;需要做的事情&#xff1b;属于后期工作&#xff1b; 1&#xff0c;探针的分类 1&#xff0c;健康状态检查探针&#xff1a;livenessProbe 当我们设置了这个探针之后&#xff0c;检查…...

【Langchain Agent研究】SalesGPT项目介绍(二)

【Langchain Agent研究】SalesGPT项目介绍&#xff08;一&#xff09;-CSDN博客 上节课&#xff0c;我们介绍了SalesGPT他的业务流程和技术架构&#xff0c;这节课&#xff0c;我们来关注一下他的项目整体结构、poetry工具和一些工程项目相关的设计。 项目整体结构介绍 我们把…...