现代C++并行与并发笔记 附C++17线程池实现项目实战

news2024/10/5 21:19:14

文章目录

  • 让程序在特定时间休眠
  • 启动和停止线程
  • 互斥量(mutex)
  • 进行延迟初始化——std::call_once
  • 将执行的程序推到后台——std::async
  • 信号量(condition_variable)
  • C++11 线程池
    • 前置知识
      • 返回值类型推导 result_of 和 invoke_result
      • packaged_task
    • 线程池代码
    • 测试函数

C++11 之前,C++原生不支持并发和并发。但这并不意味着无法对线程进行操作, 只不过需要使用系统库的API进行操作(因为线程与操作系统是不可分开的)。随着C++11标准的完成,我们有了 std::thread ,其能给予我们可以在所有操作系统 上可移植的线程操作。为了同步线程,C++11也添加了互斥量,并且对一些RAII类 型的锁进行了封装。另外,std::condition_variable 也能够灵活的在线程间,进行 唤醒操作。另一些有趣的东西就是 std::asyncstd::future ——我们可以将普通的函数封装 到 std::async 中,可以在后台异步的运行这些函数。包装后函数的返回值则 用 std::future 来表示,函数的结果将会在运行完成后,放入这个对象中,所以可以在函数完成前,做点别的事情。

让程序在特定时间休眠

C++11中对于线程的控制非常优雅和简单。在 this_thread 的命名空间中,包含了只 能被运行线程调用的函数。其包含了两个不同的函数,让线程睡眠一段时间,这样就不需要使用任何额外的库,或是操作系统依赖的库来执行这个任务。
本节中,我们将关注于如何将线程暂停一段时间,或是让其休眠一段时间。

#include <chrono>
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono_literals;
int main() {
  cout << "sleep for 5s + 300ms";
  this_thread::sleep_for(5s + 300ms);

  cout << "sleep for 3s";
  this_thread::sleep_until(chrono::high_resolution_clock::now() + 3s);
}


这是可以运行的程序,其中sleep_forsleep_until 函数都已经在 C++11 中加入,存放于std::this_thread命名空间中。其能对当前线程进行限时阻塞(并不是整个程序或整个进程)。线程被阻塞时不会消耗CPU时间,操作系统会将其标记挂起的状态,时间到了后线程会自动醒来。这种方式的好处在于,不需要知道操作系统对我们运行的程序做了什么,因为 STL会将其中的细节进行包装。
this_thread::sleep_for 函数能够接受一个 chrono::duration 值。最简单的方式就是 1s5s+300ms。为了使用这种非常简洁的字面时间表示方式,我们需要对命名空间进行声明 using namespace std::chrono_literals;this_thread::sleep_until 函数能够接受一个 chrono::time_out 参数。这就能够简单的指定对应的壁挂钟时间,来限定线程休眠的时间。
对于那种新型写法,我查了一下大概是 c++ 11 引进得 User-defined literals , 允许用户自定义后缀产生相应的对象,具体可以看这个网址:https://en.cppreference.com/w/cpp/language/user_literal

启动和停止线程

C++11中添加了 std::thread 类,并能使用简洁的方式能够对线程进行启动或停止, 线程相关的东西都包含在STL中,并不需要额外的库或是操作系统的实现来对其进行支持。
本节中,我们将实现一个程序对线程进行启动和停止。如果是第一次使用线程的话,就需要了解一些细节。

#include <chrono>
#include <iostream>
#include <thread>
using namespace std;
using namespace chrono_literals;

static void thread_with_param(int i) {
  this_thread::sleep_for(1ms * i);
  cout << "线程 " << i << "开始 \n";
  this_thread::sleep_for(1s * i);
  cout << "线程 " << i << "结束\n";
}

int main() {
  cout << thread::hardware_concurrency()
       << "个线程在当前所使用的系统中能够同时运行.\n";
  thread t1{thread_with_param, 1};
  thread t2{thread_with_param, 2};
  thread t3{thread_with_param, 3};
  t1.join();
  t2.join();
  t3.detach();
  cout << "Threads joined.\n";
}

  1. 主函数中,会先了解在所使用的系统中能够同时运行多少个线程,使用 std::thread::hardware_concurrency 进行确定。这个数值通常依赖于机器上有 多少个核,或是STL实现中支持多少个核。这也就意味着,对于不同机器,这 个函数会返回不同的值.
  2. 这里我们启动三个线程。 我们使用实例化线程的代码行为 thread t {f, x},这就等于在新线程中调用 f(x) 。这样,在不同的线程中就可以给于 thread_with_param 函数不同的参数。
  3. 当启动线程后,我们就需要在其完成其工作后将线程进行终止,使用 join 函数来停止线程。调用 join 将会阻塞调用线程,直至对应的线程终止为止
  4. 另一种方式终止的方式是分离。如果不以 join 或 detach 的方式进行终止,那 么程序只有在 thread 对象析构时才会终止。通过调用 detech ,我们将告诉3号线程,即使主线程终止了,你也可以继续运行。这个是cppreference 的意思,可是经过我的测试和查询,join 和 detach 仅仅只是回收资源的方式不一样,join 是由主线程回收,detach 则是由运行时库负责清理被调线程相关的资源,主线程结束后,所有相关的子线程都会结束。

image.png

互斥量(mutex)

image.png
线程对互斥量上锁之后,很多事情都变的非常简单,我们只需要上锁、访问、解锁 三步就能完成我们想要做的工作。不过对于有些比较健忘的开发者来说,在上锁之 后,很容易忘记对其进行解锁,或是互斥量在上锁状态下抛出一个异常,如果要对 这个异常进行处理,那么代码就会变得很难看。最优的方式就是程序能够自动来处理这种事情。
内存管理部分,我们有 unique_ptrshared_ptrweak_ptr 。这些辅助类可以很完美帮我们避免内存泄漏。互斥量也有类似的帮手,最简单的一个就是 std::lock_guard 。使用方式如下:

void critical_function() {
    
  lock_guard<mutex> l{some_mutex};
    
  // critical section
    
}

lock_guard 的构造函数能接受一个互斥量,其会立即自动调用 lock ,构造函数会直到获取互斥锁为止。当实例进行销毁时,其会对互斥量再次进行解锁。这样互斥量就很难陷入到 lock/unlock 循环错误中。
C++17 STL提供了如下的RAII辅助锁。其都能接受一个模板参数,其与互斥量的类型相同(在C++17中,编译器可以自动推断出相应的类型):
image.png
scoped_lock 能够避免死锁的发生
std::scoped_lock的预防死锁策略很简单,假设要对 n 个 mutex(mutex1, mutex2, …, mutexn)上锁,那么每次只尝试对一个 mutex 上锁,只要上锁失败就立即释放获得的所有锁(方便让其他线程获得锁),然后重新开始上锁,处于一个循环当中,直到对 n 个 mutex 都上锁成功。这种策略是基本上是有效的,虽然有极小的概率出现“活锁”,例如ABBA死锁中,线程1释放锁A的同一时刻时线程2又释放了锁B,然后这两个线程又同时分别获得了锁A和锁B,如此循环。

进行延迟初始化——std::call_once

假如我们将完成一个程序,我们使用多线程对同一段代码进行执行。他们执行的是相同的代码,但是我们的初始化函数只需要运行一次:

#include <iostream>
#include <mutex>
#include <thread>
#include <vector>
using namespace std;
// 在对指定函数使用  call_once 时,需要对所有线程进行同步,这是同步标志
once_flag callflag;
// 只需要执行一次的函数
static void once_print() { cout << '!'; }

static void print(size_t x) {
  // 只调用一次的写法
  std::call_once(callflag, once_print);
  cout << x;
}
int main() {
  vector<thread> v;
  for (size_t i{0}; i < 10; ++i) {
    v.emplace_back(print, i);
  }
  for (auto &t : v) {
    t.join();
  }
  cout << '\n';
}

std::call_once 工作原理和栅栏类似:第一个线程达到 call_once 的线程会执行对应的函数,其他线程到这就阻塞。当第一个线程从准备函数中返回后,其他线程才结束阻塞。我们可以对这个过程进行安排,让一个变量决定其他线程能否运行,线程则必须对这个变量进行等待,直到这个变量准备好了,所有变量才能运行。这个变量就 是 once_flag callflag;。每一个 call_once 都需要一个 once_flag 实例作为参数,来
表明预处理函数是否运行了一次。
另一个细节是:如果 call_once 执行失败了(因为准备函数抛出了异常),那么下一个线程则会再去尝试执行。

将执行的程序推到后台——std::async

前面说过的 t1.join(),并不会给你返回值,为了获取返回值通常只能运用共享内存的方法。
C++11之后, std::async 能帮我们完成这项任务。我们将写一个简单的程序,并使用异步函数,让线程在同一时间内做很多事情。 std::async 其实很强大,让我们先来了解其一方面。

#include <algorithm>
#include <future>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <map>
#include <string>
using namespace std;
// 对字符串中的字符进行统计
static map<char, size_t> histogram(const string &s) {
  map<char, size_t> m;
  for (char c : s) {
    m[c] += 1;
  }
  return m;
}
// 返回一个排序后的副本
static string sorted(string s) {
  sort(begin(s), end(s));
  return s;
}
// 是否元音字母
static bool is_vowel(char c) {
  char vowels[]{"aeiou"};
  return end(vowels) != find(begin(vowels), end(vowels), c);
}
// 对传入的字符串中元音字母进行计数

static size_t vowels(const string &s) {
  return count_if(begin(s), end(s), is_vowel);
}
int main() {
  // 测试变量
  string input = "wakwkdawfkwekf";
  // 函数一
  auto hist(async(launch::async, histogram, input));

  // 函数二
  auto sorted_str(async(launch::async, sorted, input));

  // 函数三 
  auto vowel_count(async(launch::async, vowels, input));

  // 获取函数一的返回值
  for (const auto &[c, count] : hist.get()) {
    cout << c << ": " << count << '\n';
  }
    
  // 获取函数 二 三 的返回值
  cout << "Sorted string: " << sorted_str.get() << '\n'
       << "Total vowels: " << vowel_count.get() << '\n';
}

通过 std::async 会返回一个 future 类型对象。这个对象表示在未来某个时间点上,对象将会获取返回值。通过对 future 对象使 用 .get() ,我们将会阻塞主函数,直到相应的值返回
我们将三个函数使用 async(launch::async, ...) 进行包装。这样三个函数都不会由主函数来完成。此外, async 会启动新线程,并让线程并发的完成这几个函数。这样我们只需要启动一个线程的开销,就能将对应的工作放在后台进行,而后可以继续执行其他代码。
launch::async 是一个用来定义执行策略的标识。其有两种独立方式和一种组合方式:

策略选择意义
std::launch::async运行新线程,以异步执行任务
std::launch::deferred在调用线程上执行任务(惰性求值)。在对 future 调用 get 和 wait 的时候,才进行执行。如果什么都没有发生,那么执行函数就没有运行。

不使用策略参数调用 async(f, 1, 2, 3) , async 的实现可以自由的选择策略。这也就意味着,我们不能确定任务会执行在一个新的线程上,还是执行在当前线程上。

还有件事情我们必须要知道,假设我们写了如下的代码:

async(launch::async, f); 
async(launch::async, g);

这就会让 f 和 g 函数并发执行(这个例子中,我们并不关心其返回值)。运行这段代码时,代码会阻塞在这两个调用上,这并不是我们想看到的情况。
所以,为什么会阻塞呢? async 不是非阻塞式、异步的调用吗?没错,不过这里有点特殊:当对一个 async 使用 launch::async 策略时,获取一个 future 对象,之后其析构函数将会以阻塞式等待函数结束运行。
这也就意味着,这两次调用阻塞的原因就是, future 生命周期只有一行的时间!我们可以以接收其返回值的方式,来避免这个问题,从而让 future 对象的生命周期更长。

信号量(condition_variable)

下面是例子(生产者消费者模型):

#include <condition_variable>
#include <iostream>
#include <queue>
#include <thread>
#include <tuple>
using namespace std;
using namespace chrono_literals;
queue<size_t> q;
mutex mut;
condition_variable cv;
bool finished{false};
// 生产者
static void producer(size_t items) {
  for (size_t i{0}; i < items; ++i) {
    this_thread::sleep_for(100ms);
    {
      lock_guard<mutex> lk{mut};
      q.push(i);
    }
    cv.notify_all();
  }
  {
    lock_guard<mutex> lk{mut};
    finished = true;
  }
  cv.notify_all();
}
// 消费者
static void consumer() {
  while (!finished) {
    unique_lock<mutex> l{mut};
    cv.wait(l, [] { return !q.empty() || finished; });
    while (!q.empty()) {
      cout << "Got " << q.front() << " from queue.\n";
      q.pop();
    }
  }
}

int main() {
  thread t1{producer, 10};
  thread t2{consumer};
  t1.join();
  t2.join();
  cout << "finished!\n";
}

我们只启动了两个线程。第一个线程会生产一些商品,并放到队列中。另 一个则是从队列中取走商品。当其中一个线程需要对队列进行访问时,其否需要对公共互斥量 mut 进行上锁,这样才能对队列进行访问。这样,我们就能保证两个线程不能再同时对队列进行操作。

cv.wait(lock, predicate) 将会等到 predicate() 返回true时,结束等待。不过其不会对 lock 持续的进行解锁与上锁的操作。为了将使用 wait 阻塞的线程唤醒,我们就需要使用 condition_variable 对象,另一个线程会对同一个对象调用 notify_one()notify_all() 。等待中的线程将从休眠中醒来,并检查 predicate() 条件是否成立。
这样的操作很方便我们使用,因为一般情况下需要多行代码,当消费者被 notify 起来后,可能不止你一个消费者被激活,为保证条件仍就能够合理消费,需要对临界条件再次检查,该 wait函数的用法浓缩了这几行代码。

C++11 线程池

前置知识

返回值类型推导 result_of 和 invoke_result

result_of 是在 C++11 中引入的,同时由于自身原因于C++20 中移除,替代品为 C++17 中引入的 invoke_resultresult_of我觉得很难用,从下面例子可以看出来,泛型的类别我很难写,下面那个 ri 函数,我想着直接用函数的类型,但是不行,会报错,要以下面那种奇怪的写法写出来,而invoke_result就很合理,所以接下来的线程池会采用 invoke_result 来参与返回值获取的实现.

#include <iostream>
using namespace std;
int r1() { return 1; }

result_of<decltype(r1)&(void)>::type i = 1;

invoke_result<decltype(r1)>::type s = 1;

int main() { cout << i; }

packaged_task

类模板 std::packaged_task 包装任何可调用目标(函数、 lambda 表达式、 bind 表达式或其他函数对象),使得能异步调用它。其返回值或所抛异常被存储于能通过 std::future 对象访问的共享状态中。

#include <iostream>
#include <cmath>
#include <thread>
#include <future>
#include <functional>
 
// 避免对 std::pow 重载集消歧义的独有函数
int f(int x, int y) { return std::pow(x,y); }
 
void task_lambda()
{
    std::packaged_task<int(int,int)> task([](int a, int b) {
        return std::pow(a, b); 
    });
    std::future<int> result = task.get_future();
 
    task(2, 9);
 
    std::cout << "task_lambda:\t" << result.get() << '\n';
}
 
void task_bind()
{
    std::packaged_task<int()> task(std::bind(f, 2, 11));
    std::future<int> result = task.get_future();
 
    task();
 
    std::cout << "task_bind:\t" << result.get() << '\n';
}
 
void task_thread()
{
    std::packaged_task<int(int,int)> task(f);
    std::future<int> result = task.get_future();
 
    std::thread task_td(std::move(task), 2, 10);
    task_td.join();
 
    std::cout << "task_thread:\t" << result.get() << '\n';
}
 
int main()
{
    task_lambda();
    task_bind();
    task_thread();
}

线程池代码

#include <condition_variable>
#include <functional>
#include <future>
#include <memory>
#include <mutex>
#include <queue>
#include <stdexcept>
#include <thread>
#include <vector>
class ThreadPool {
public:
  ThreadPool(size_t);
  template <class F, class... Args>
  auto enqueue(F &&f, Args &&...args)
      -> std::future<typename std::invoke_result<F>::type>;
  ~ThreadPool();

private:
  // need to keep track of threads so we can join them
  std::vector<std::thread> workers;
  // the task queue
  std::queue<std::function<void()>> tasks;

  // synchronization
  std::mutex queue_mutex;
  std::condition_variable condition;
  bool stop;
};

// the constructor just launches some amount of workers
inline ThreadPool::ThreadPool(size_t threads) : stop(false) {
  for (size_t i = 0; i < threads; ++i)
    workers.emplace_back([this] {
      for (;;) {
        std::function<void()> task;

        {
          std::unique_lock<std::mutex> lock(this->queue_mutex);
          // 等生产者生产
          this->condition.wait(
              lock, [this] { return this->stop || !this->tasks.empty(); });
          // 若是因为内存池停止,或是虚假唤醒 直接 return
          if (this->stop && this->tasks.empty())
            return;
          // 拿任务
          task = std::move(this->tasks.front());
          this->tasks.pop();
        }

        task();
      }
    });
}

// add new work item to the pool
template <class F, class... Args>
auto ThreadPool::enqueue(F &&f, Args &&...args)
    -> std::future<typename std::invoke_result<F>::type> {
  // std::invoke_result 用于推导可调用对象的放回值类型
  using return_type = typename std::invoke_result<F>::type;
  // 将获取 packaged_task
  auto task = std::make_shared<std::packaged_task<return_type()>>(
      std::bind(std::forward<F>(f), std::forward<Args>(args)...));

  std::future<return_type> res = task->get_future();
  {
    std::unique_lock<std::mutex> lock(queue_mutex);

    // don't allow enqueueing after stopping the pool
    if (stop)
      throw std::runtime_error("enqueue on stopped ThreadPool");

    tasks.emplace([task]() { (*task)(); });
  }
  condition.notify_one();
  return res;
}

// the destructor joins all threads
inline ThreadPool::~ThreadPool() {
  {
    std::unique_lock<std::mutex> lock(queue_mutex);
    stop = true;
  }
  condition.notify_all();
  for (std::thread &worker : workers)
    worker.join();
}

测试函数

#include <iostream>
#include <vector>
#include <chrono>

#include "ThreadPool.h"

int main()
{
    
    ThreadPool pool(4);
    std::vector< std::future<int> > results;

    for(int i = 0; i < 8; ++i) {
        results.emplace_back(
            pool.enqueue([i] {
                std::cout << "hello " << i << std::endl;
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << "world " << i << std::endl;
                return i*i;
            })
        );
    }

    for(auto && result: results)
        std::cout << result.get() << ' ';
    std::cout << std::endl;
    
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/157409.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

天翼物联获中国信通院2022 AIoT先锋企业

近日&#xff0c;由中国信息通信研究院组织开展的2022 AIoT先锋企业评选活动成果发布&#xff0c;中国电信天翼物联凭借为AIoT发展作出的积极贡献获“2022 AIoT先锋企业”&#xff0c;是唯一获得该奖项的通信企业。 2022 AIoT先锋企业评选活动由中国信息通信研究院组织开展&…

IDEA 下载依赖包源码报错Sources not found for: org.springframework.cloud:XXX

IDEA 在使用某些类方法想看下源码时&#xff0c;由于只有 class 反编译的类文件&#xff0c;没有原始 Java 文件&#xff0c;想要将源码下载下来&#xff0c;右下角一直报一个错误 Cannot download sources Sources not found for:XXX&#xff0c;很是烦恼&#xff0c;怎么解决…

数据结构---线性表课后习题详解(朱昌杰编著)

刘佳瑜*&#xff0c;王越 *, 黄扬* , 张钊* (淮北师范大学计算机科学与技术学院&#xff0c;安徽 淮北) *These authors contributed to the work equllly and should be regarded as co-first authors. &#x1f31e;欢迎来到数据结构的世界 &#x1f308;博客主页&#xff1…

【Docker】docker部署前后端分离项目( 前:nginx + vue 后:springboot+ redis + mysql)

目录一.安装docker二.docker安装和配置nginx1.拉取nginx2.创建临时nginx容器3.从nginx容器复制 nginx.conf 文件到宿主机4.删除临时nginx容器5.启动真正的nginx容器6.查看是否挂载成功7.配置nginx.conf 和 vue的包放到指定位置三 docker安装部署redis1.安装redis2.部署redis四 …

如何计算结构体的大小?结构体内存对齐【C语言】

今天我们来讲讲结构体的大小如何来计算 其中涉及到一个结构体中的热门考点&#xff1a;结构体内存对齐 话不多说&#xff0c;开始学习&#xff01; 要想计算结构体的大小&#xff0c;首先要了解结构体的对齐规则。 目录 结构体内存对齐规则 举例 为什么存在内存对齐? 如…

测试用例该怎么设计?—— 日常加更篇(上)

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

火山引擎 DataTester 升级:降低产品上线风险,助力产品敏捷迭代

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;并进入官方交流群 在企业竞争加剧的今天&#xff0c;精益开发和敏捷迭代已成为产品重要的竞争力。如何保障每一次 Feature 高效迭代与安全&#xff0c;如何快速实现面对不同用户的精细化运营…

Java设计模式——单例模式

目录 一、设计模式介绍 二、设计模式类型 三、单例设计模式介绍 单例设计模式八种方式 &#xff08;一&#xff09;饿汉式&#xff08;静态常量&#xff09; &#xff08;二&#xff09;饿汉式&#xff08;静态代码块&#xff09; &#xff08;三&#xff09; 懒汉式(线程…

【Flink系列】部署篇(二):独立部署高可用Flink集群实战

服务器操作系统&#xff1a;centos7本机操作系统&#xff1a;MacFlink version: 1.15JDK version: java11HA service: ZookeeperFile System: NFS 资源分配&#xff1a; iphostnamerole10.250.0.1main0JM10.250.0.2main1JM10.250.0.3main2JM10.250.0.4worker1TM10.250.0.5wor…

Spring Cloud Eureka的使用

Spring Cloud Eureka &#x1f43b; 一个服务注册与发现的组件 &#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;ZT&#x1f604;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;&#x1f43b;…

SAP S/4HANA 采购订单处理操作详解

SAP S 4HANA Cloud 被 IDC 评为全球 SaaS 和云 ERP 系统领导者。SAP S4HANA Cloud是一套接近于零配置的系统&#xff0c;基于最佳业务实践的配置已经内嵌在标准版本中&#xff0c;可以让购买企业在第一时间内获得最全面的解决方案。本文就以其中最为常见的采购订单创建及处理流…

一页PPT自动生成短视频的研究

希望通过一些技术&#xff0c;将以前自己讲过的PPT转换成有解说的短视频&#xff0c;从而进行一些分发 旁白到语音 从文字转换成语音我们首先想到的就是TTS&#xff0c;这其中我也是用了各式各样的TTS&#xff0c;发现发音电子音非常强&#xff0c;听听起来很不舒服。后来发现…

Spring 事务和事务的传播机制

1.Spring 中事务的实现方式Spring 中的操作主要分为两类: 编程式事务 (了解)声明式事务编程式事务就是手写代码操作事务, 而声明式事务是利用注解来自动开启和提交事务. 并且编程式事务用几乎不怎么用. 这就好比汽车的手动挡和自动挡, 如果有足够的的钱, 大部分人应该都会选择自…

NPDP认证|如何实现产品的组合管理?

随着企业中研发项目类型和数量的增多,涉及的范围越来越宽广,内容越来越复杂,时效性也越来越强,传统的分散式的项目管理思想已经很难满足企业的需求。 为了使技术和资源能够得到有限的配置和利用,企业就需要把各种类型的研发项日进行有机的结合。 组合管理很重要吗? 答案是勿庸…

Vue知识点

Vue基础语法 插值操作 Mustache语法 可以直接写变量&#xff0c;也可以写简单的表达式 {{firstName lastName}}’ {{firstName lastName}} {{firstName}} {{lastName}} 其他指令使用 v-noce&#xff1a; <h2 v-once>{{message}}</h2> 某些情况下&#xff…

shell 脚本实现 k8s 集群环境下指定 ns 资源的 yaml 文件备份

shell 脚本实现 k8s 集群环境下指定 ns 资源的 yaml 文件备份需求说明功能实现shell 脚本实现shell 使用方式前置工具环境安装dump-k8s-yaml.sh 使用方式输入命令 bash ./dump-k8s-yaml.shdump-k8s-yaml.sh 应用举例dump-k8s-yaml.sh 输出日志信息参考文档需求说明 在基于 k8s…

【Java寒假打卡】Java基础-字符流

【Java寒假打卡】Java基础-字符流编码表字符串中的编码和解码问题字节流读取文本文件出现乱码的原因字符流读取中文的过程字符流写出数据字符流输出数据注意事项flush和close方法字符流读取数据案例-保存键盘录入的数据字符缓冲输入流字符缓冲输出流缓冲流的特有方法案例-读取文…

【算法】广度优先遍历 (BFS)

目录1.概述2.代码实现3.应用1.概述 &#xff08;1&#xff09;广度优先遍历 (Breadth First Search)&#xff0c;又称宽度优先遍历&#xff0c;是最简便的图的搜索算法之一。 &#xff08;2&#xff09;已知图 G (V, E) 和一个源顶点 start&#xff0c;宽度优先搜索以一种系…

让我用Python自制软件,看视频畅通无阻

前言 一个账号只能登录一台设备&#xff1f;涨价就涨价&#xff0c;至少还能借借朋友的&#xff0c;谁还没几个朋友&#xff0c;搞限制登录这一出&#xff0c;瞬间不稀罕了 这个年头谁还不会点技术了&#xff0c;直接拿python自制一个可以看视频的软件… 话不多说&#xff0…

终于弄懂了 非极大抑制 NMS

NMS的作用就是有效地剔除目标检测结果中多余的检测框&#xff0c;保留最合适的检测框。 以YOLOv5为例&#xff0c;yolov5模型的输入三个feature map的集合&#xff0c;加上batch的维度&#xff0c;也就是三维张量&#xff0c;即[batch&#xff0c;(p0∗p0p1∗p1p2∗p2)∗3&…