【Rust自学】20.2. 最后的项目:多线程Web服务器

说句题外话,这篇文章非常要求Rust的各方面知识,最好看一下我的【Rust自学】专栏的所有内容。这篇文章也是整个专栏最长(4762字)的文章,需要多次阅读消化,最好点个收藏,免得刷不到了。
请添加图片描述

喜欢的话别忘了点赞、收藏加关注哦(加关注即可阅读全文),对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)

20.2.1. 回顾

我们在上一篇文章中写了一个简单的本地服务器,但是这个服务器是单线的,也就是说请求一个一个进去之后我们得一个一个地处理,如果某个请求处理得慢,那后面的都得排队等着。这种单线程外部服务器的性能是非常差的。

20.2.2. 慢速请求

我们用代码来模拟慢速请求:

use std::{fs,io::{prelude::*, BufReader},net::{TcpListener, TcpStream},thread,time::Duration,
};
// ...fn handle_connection(mut stream: TcpStream) {// ...let (status_line, filename) = match &request_line[..] {"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),"GET /sleep HTTP/1.1" => {thread::sleep(Duration::from_secs(5));("HTTP/1.1 200 OK", "hello.html")}_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),};// ...
}

省略了一些原代码,但是不影响。我们增加的语句是如果用户访问的是127.0.0.1:7878/sleep时会调用thread::sleep(Duration::from_secs(5));,这句话使代码的执行休眠5秒,也就是模拟的慢速请求。

然后打开两个浏览器窗口:一个用于http://127.0.0.1:7878/另一个为http://127.0.0.1:7878/sleep。如果像以前一样,您会看到它快速响应。但是如果你输入/sleep然后加载 ,你会看到一直等到 sleep在加载前已经休眠了整整5秒。

如何改善这种情况呢?这里我们使用线程池技术,也可以选择其它技术比如fork/join模型单线程异步 I/O 模型多线程异步I/O模型

20.2.3. 使用线程池提高吞吐量

线程池是一组分配出来的线程,它们被用于等待并随时可能的任务。当程序接收到一个新任务时,它会给线程池里边一个线程分配这个任务,其余线程与此同时还可以接收其它任务。当任务执行完后,这个线程就会被重新放回线程池。

线程池通过允许并发处理连接的方式增加了服务器的吞吐量。

如何为每个连接都创建一个线程呢?看代码:

fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();for stream in listener.incoming() {let stream = stream.unwrap();thread::spawn(|| {handle_connection(stream);});}
}

迭代器每迭代一次就创建一个新线程来处理。

这样写的缺点在于线程数量没有限制,每一个请求就创建一个新线程。如果黑客使用DoS(Denial of Service,拒绝服务攻击),我们的服务器就会很快瘫掉。

所以在上边代码的基础上我们进行修改,我们使用编译驱动开发编写代码(不是一个标准的开发方法论,是开发者之间的一种戏称,不同于TDD测试驱动开发):把期望调用的函数或是类型写上,再根据编译器的错误一步步修改。

使用编译驱动开发

我们把我们想写的代码直接写上,先不论对错

fn main() {  let listener = TcpListener::bind("127.0.0.1:7878").unwrap();  let pool = ThreadPool::new(4);  for stream in listener.incoming() {  let stream = stream.unwrap();  pool.execute(|| {  handle_connection(stream);  })  }  
}

虽然说并没有ThreadPool这个类型,但是根据编译驱动开发编写代码的逻辑,我觉得应该这么写就先写上,不管对错。

使用cargo check检查一下:

error[E0433]: failed to resolve: use of undeclared type `ThreadPool`--> src/main.rs:11:16|
9  |     let pool = ThreadPool::new(4);|                ^^^^^^^^^^ use of undeclared type `ThreadPool`For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error

这个错误告诉我们我们需要一个ThreadPool类型或模块,所以我们现在就构建一个。

我们在lib.rs中写ThreadPool的相关代码,一方面保持了main.rs足够简洁,另一方面也使ThreadPool相关代码能更加独立地存在。

打开lib.rs,写下ThreadPool的简单定义:

pub struct ThreadPool;

main.rs里把ThreadPool引入作用域:

use web_server::ThreadPool;

使用cargo check检查一下:

error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope--> src/main.rs:10:28|
10 |     let pool = ThreadPool::new(4);|                            ^^^ function or associated item not found in `ThreadPool`

这个错误表明接下来我们需要创建一个名为的关联函数 ThreadPoolnew 。我们还知道new需要有一个参数,该参数可以接受4作为参数,并且应该返回一个ThreadPool实例。让我们实现具有这些特征的最简单的new函数:

pub struct ThreadPool;impl ThreadPool {pub fn new(size: usize) -> ThreadPool {ThreadPool}
}

使用cargo check检查一下:

error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope--> src/main.rs:17:14|
15 |         pool.execute(|| {|         -----^^^^^^^ method not found in `ThreadPool`For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error

现在发生错误是因为我们在ThreadPool上没有execute方法。那就补充一个方法:

pub fn execute<F>(&self, f: F)  
where  F: FnOnce() + Send + 'static,  
{  
}
  • execute函数的参数除了self的应用还有一个闭包参数,运行请求的线程只会调用闭包一次,所以使用FnOnce()()表示它是返回单位类型()的闭包。同时我们需要Send trait将闭包从一个线程传输到另一个线程,而'static是因为我们不知道线程执行需要多长时间。

  • 也可以这么想:我们使用它替代的是原代码的thread::spawn函数,所以修改时就可以借鉴它的函数签名,它的签名如下。我们主要借鉴的是泛型F和它的约束,所以excute函数的泛型约束就可以按照F来写。

pub fn spawn<F, T>(f: F) -> JoinHandle<T>
whereF: FnOnce() -> T,F: Send + 'staticT: Send + 'static

使用cargo check检查没有错误,但是使用cargo run依旧会报错,因为executenew都没有实现实际需要的效果,只是满足了编译器的检查。

你可能听说过关于具有严格编译器的语言(例如 Haskell 和 Rust)的一句话是“if the code compiles, it works.如果代码可以编译,它就可以工作”。但这句话并不普遍正确。我们的项目可以编译,但它什么也没做。如果我们正在构建一个真实的、完整的项目,那么这是开始编写单元测试以检查代码是否编译并具有我们想要的行为的好时机(也就是TDD测试驱动开发)

修改new函数 Pt.1

我们先修改new函数使其具有实际意义:

impl ThreadPool {/// Create a new ThreadPool.////// The size is the number of threads in the pool.////// # Panics////// The `new` function will panic if the size is zero.pub fn new(size: usize) -> ThreadPool {assert!(size > 0);ThreadPool}// ...
}
  • 我们使用assert!函数来判断new函数的参数要大于0,因为等于0时没有任何意义。
  • 添加了一些文档注释,这样在运行cargo doc --open时就能看到文档解释:
    请添加图片描述

修改ThreadPool类型

new函数的修改遇到瓶颈了:ThreadPool类型都没有具体字段我们实现不了创建具体线程数量的目标。所以接下来我们研究一下如何在ThreadPool里存储线程,代码如下:

use std::thread;  pub struct ThreadPool{  threads: Vec<thread::JoinHandle<()>>,  
}

ThreadPool下有threads字段,类型是Vec<thread::JoinHandle<()>>

  • Vec<>是因为我们要存储多个线程,但是具体数量又未知,所以使用Vector
  • 之前我们看过thread::spawn函数的函数签名,其返回值是JoinHandle<T>,依葫芦画瓢,我们就也使用thread::JoinHandle<>来存储线程。
    JoinHandle<T>T是因为thread::spawn的线程有可能会有返回值,不知道具体什么类型,所以用泛型来表示。而我们的代码是确定没有返回值的,所以就写thread::JoinHandle<()>()是单元类型。

修改new函数 Pt.2

修改完ThreadPool的定义之后我们再返回来修改new函数:

pub fn new(size: usize) -> ThreadPool {  assert!(size > 0);  let mut threads = Vec::with_capacity(size);  for _ in 0..size {  // create some threads and store them in the vector  }  ThreadPool { threads }  
}
  • Vec::with_capacity函数传进去size来创建一个预分配好空间的Vector
  • 写了一个从0到size的循环(不包括size),里面的逻辑暂时还没写,总之这个循环是准备用来创建线程并存到Vector里的
  • 最后返回ThreadPool类型即可,threads字段的值就是这个函数中的threads

接下来我们来研究一下thread::spawn函数一遍我们更好写new里的循环。thread::spawn在线程创建后立即获取线程应运行的代码执行。然而,在我们的例子中,我们想要创建线程并让它们等待我们稍后发送的代码。标准库的线程实现不包含任何方法来做到这一点,所以我们必须手动实现它。

使用Worker数据结构

我们使用一种新的数据结构来实现这个效果,叫做Worker ,这是池实现中的常用术语。 Worker拾取需要运行的代码并在Worker的线程中运行代码。想象一下在餐厅厨房工作的人:工人们等待顾客下单,然后负责接受并履行这些订单。我们通过Worker来管理和实现我们所要的行为。

我们来创建Worker这个结构体及必要的方法:

struct Worker {  id: usize,  thread: thread::JoinHandle<()>,  
}impl Worker {  fn new(id: usize) -> Worker {  let thread = thread::spawn(|| {});  Worker { id, thread }  }  
}
  • Worker一共有两个字段,一个是id,类型为usize,表示标识;还有一个thread字段,类型是thread::JoinHandle<()>,存储一个线程
  • new函数创建了Worker实例,id字段的值就是它的参数

PS:外部代码(如main.rs中的服务器)不需要知道有关在ThreadPool中使用Worker结构的实现细节,因此我们将Worker结构及其new函数设为私有。

接下来在ThreadPool里使用Worker

pub struct ThreadPool {  workers: Vec<Worker>,  
}

ThreadPool上的new函数和excute函数也需要修改,这里先修改new函数,excute等一下修改:

pub fn new(size: usize) -> ThreadPool {  assert!(size > 0);  let mut workers = Vec::with_capacity(size);  for id in 0..size {  workers.push(Worker::new(id));  }  ThreadPool { workers }  
}
  • threads相关的代码改为Workers即可
  • 由于ThreadPoolWorker字段是被Vector包裹的,所以使用Vectorpush方法即可以往Vector里添加新元素
  • 在循环中使用到了Worker上的new函数,创建了Worker实例,id字段的值就是传进去的参数

PS:如果操作系统由于没有足够的系统资源而无法创建线程, thread::spawn将会出现恐慌。我们在这个例子中不考虑这种情况,但在实际编写时最好考虑到这点,使用std::thread::builder,它会返回Result<JoinHandle<T>>

通过通道向线程发送请求

完成了线程的创建,接下来就要考虑如何接收任务了。这时就需要通道这个技术。重构一下代码:

use std::thread;  
use std::sync::mpsc;  pub struct ThreadPool {  workers: Vec<Worker>,  sender: mpsc::Sender<Job>,  
}  struct Job;
  • 使用use std::sync::mpsc;mpsc引入作用域以便后文使用
  • ThreadPool新建了一个字段sender,类型是mpsc::Sender<Job>(Job是一个结构体,表示要执行的工作),用于存储发送端

我们在ThreadPoolnew方法上创建通道:

impl ThreadPool {// ...pub fn new(size: usize) -> ThreadPool {  assert!(size > 0);  let (sender, receiver) = mpsc::channel();  let mut workers = Vec::with_capacity(size);  for id in 0..size {  workers.push(Worker::new(id, receiver));  }  ThreadPool { workers, sender }  }// ...
}
// ...
impl Worker {  fn new(id: usize, receiver: Receiver<Job>) -> Worker {  let thread = thread::spawn(|| {  receiver;  });  Worker { id, thread }  }  
}
  • 使用mpsc::channel()函数创建通道,发送端和接收端分别命名为senderreceiver
  • sender赋给返回值的sender字段,就相当于线程池持有通道的发送端了
  • 接收者应该是Worker,所以我们把Workernew函数也要相应的更改,增加了receiver这个参数

这时候运行cargo check试试:

error[E0382]: use of moved value: `receiver`--> src/lib.rs:26:42|
22 |         let (sender, receiver) = mpsc::channel();|                      -------- move occurs because `receiver` has type `std::sync::mpsc::Receiver<Job>`, which does not implement the `Copy` trait
...
25 |         for id in 0..size {|         ----------------- inside of this loop
26 |             workers.push(Worker::new(id, receiver));|                                          ^^^^^^^^ value moved here, in previous iteration of loop|
note: consider changing this parameter type in method `new` to borrow instead if owning the value isn't necessary--> src/lib.rs:45:33|
45 |     fn new(id: usize, receiver: Receiver<Job>) -> Worker {|        --- in this method       ^^^^^^^^^^^^^ this parameter takes ownership of the value
help: consider moving the expression out of the loop so it is only moved once|
25 ~         let mut value = Worker::new(id, receiver);
26 ~         for id in 0..size {
27 ~             workers.push(value);|

报错是因为该代码尝试将一个receiver传递给多个Worker实例,这是行不通的,因为接收端只能有一个。

我们希望所有的线程都共享一个receiver,从而能在线程间分发任务。此外,从通道队列中取出receiver涉及改变 receiver ,因此线程需要一种安全的方式来共享和修改receiver 。否则,我们可能会遇到竞争条件。

针对多线程多重所有权的要求,可以使用Arc<T>Rc<T>只能用于单线程);针对多线程避免数据竞争的要求,可以使用互斥锁Mutex<T>

这下只需要在原本的receiver上套Arc<T>Mutex<T>就行了:

impl ThreadPool {  /// ...  pub fn new(size: usize) -> ThreadPool {  assert!(size > 0);  let (sender, receiver) = mpsc::channel();  let mut workers = Vec::with_capacity(size);  let receiver = Arc::new(Mutex::new(receiver));  for id in 0..size {  workers.push(Worker::new(id, Arc::clone(&receiver)));  }  ThreadPool { workers, sender }  }  //...
}  
//...impl Worker {  fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {  let thread = thread::spawn(|| {  receiver;  });  Worker { id, thread }  }  
}
  • 重新声明receiver,把它用Arc<T>Mutex<T>包裹
  • 在循环中使用Arc::clone(&receiver)传给每个Worker
  • Workernew方法的receiver参数的类型需要改为Arc<Mutex<mpsc::Receiver<Job>>>

修改Job

我们的Job暂时还是一个空结构体,没有任何的实际效果,所以我们把它改为类型别名(详见19.5. 高级类型):

type Job = Box<dyn FnOnce() + Send + 'static>;

Job是一个闭包,在一个线程中只被调用一次,没有返回值(或者叫返回值是单元类型()),所以得满足FnOnce();并且这个闭包还要能够在线程间传递,所以得满足Send trait。'static是因为我们不知道线程执行需要多长时间,只好把它声明为静态生命周期。

修改execute函数

接下来我们来修改execute函数:

pub fn execute<F>(&self, f: F)  
where  F: FnOnce() + Send + 'static,  
{  let job = Box::new(f);  self.sender.send(job).unwrap();  
}
  • 因为Job的最外层是Box<T>封装,所以想把闭包f发送出去就得先用Box::new函数来封装
  • 使用selfsender字段作为发送端把job发送出去

修改Worker下的new函数

excute方法这么改了,那么作为接收端的Worker下的new函数也得改:

impl Worker {  fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {  let thread = thread::spawn(move || loop {  let job = receiver.lock().unwrap().recv().unwrap();  println!("Worker {} got a job; executing.", id);  job();  });  Worker { id, thread }  }  
}
  • 使用lock锁定了receiver(receiver被封装在互斥锁Mutex<T>里),获取互斥体,unwrap错误处理
  • 再使用recv方法从通道接收传过来的内容,再使用unwrap错误处理
  • 打印一下是哪个Worker在工作
  • 当调用job();时,编译器会自动将job解引用为其内部的闭包类型,然后调用FnOnce或其他相应的trait实现的call方法。这是因为Box<dyn FnOnce()>实现了FnOnce。也就是说,job();(*job)();的语法糖。

版本差异

我使用的是1.84.0的Rust,在早期(大概是1.0版本附近)时不能直接使用job();,也不能使用(*job)();,因为当时编译器不直接知道动态大小类型所占用的内存大小,所以不能直接解码。在后来的 Rust RFC 127(实现于 Rust 1.20,发布于 2017 年) 之后,Rust 为Box<dyn Trait>等类型添加了直接调用trait方法的能力,这背后利用了自动解引用及调用调度逻辑

总而言之,如果你写成上文代码那样要报错的话要么就升级Rust版本,要么就增加并修改一些代码:

trait FnBox {fn call_box(self: Box(self))
}impl<F: FnOnce()> FnBox for F {fn call_box(self: Box<F>) {(*self)();}
}type Job = Box<FnBox + Send + 'static>
  • FnBox trait这个方法使得我们可以在类型的Box上调用了
  • FnOnce()写了call_box的具体实现(因为Job实现了FnOnce()),这样就可以获得Box里边东西的所有权,从而调用
  • Job的类型从FnOnce()改成FnBox,这样其它代码就可以不用修改,所有实现了FnBox的类型肯定同时实现了FnBox

20.2.4. 试运行

终于改完了,让我们试运行一下:
请添加图片描述

如果你在浏览器里多刷新几次界面就能看到其它不同id的Worker在工作。

20.2.5. 总结

main.rs:

use std::{  io::{prelude::*, BufReader},  net::{TcpListener, TcpStream},  fs,  
};  
use web_server::ThreadPool;  fn main() {  let listener = TcpListener::bind("127.0.0.1:7878").unwrap();  let pool = ThreadPool::new(4);  for stream in listener.incoming() {  let stream = stream.unwrap();  pool.execute(|| {  handle_connection(stream);  })  }  
}  fn handle_connection(mut stream: TcpStream) {  let buf_reader = BufReader::new(&stream);  let request_line = buf_reader.lines().next().unwrap().unwrap();  let (status_line, filename) = if request_line == "GET / HTTP/1.1" {  ("HTTP/1.1 200 OK", "hello.html")  } else {  ("HTTP/1.1 404 NOT FOUND", "404.html")  };  let contents = fs::read_to_string(filename).unwrap();  let length = contents.len();  let response =  format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");  stream.write_all(response.as_bytes()).unwrap();  
}

lib.rs:

use std::{  sync::{mpsc, Arc, Mutex},  thread,  
};  pub struct ThreadPool {  workers: Vec<Worker>,  sender: mpsc::Sender<Job>,  
}  type Job = Box<dyn FnOnce() + Send + 'static>;  impl ThreadPool {  /// Create a new ThreadPool.  ///    /// The size is the number of threads in the pool.    ///    /// # Panics  ///    /// The `new` function will panic if the size is zero.    pub fn new(size: usize) -> ThreadPool {  assert!(size > 0);  let (sender, receiver) = mpsc::channel();  let mut workers = Vec::with_capacity(size);  let receiver = Arc::new(Mutex::new(receiver));  for id in 0..size {  workers.push(Worker::new(id, Arc::clone(&receiver)));  }  ThreadPool { workers, sender }  }  pub fn execute<F>(&self, f: F)  where  F: FnOnce() + Send + 'static,  {  let job = Box::new(f);  self.sender.send(job).unwrap();  }  
}  struct Worker {  id: usize,  thread: thread::JoinHandle<()>,  
}  impl Worker {  fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {  let thread = thread::spawn(move || loop {  let job = receiver.lock().unwrap().recv().unwrap();  println!("Worker {} got a job; executing.", id);  job();  });  Worker { id, thread }  }  
}

hello.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  <meta charset="utf-8">  <title>Hello!</title>  
</head>  
<body>  
<h1>Hello!</h1>  
<p>Hi from Rust</p>  
</body>  
</html>

404.html:

<!DOCTYPE html>  
<html lang="en">  
<head>  <meta charset="utf-8">  <title>Hello!</title>  
</head>  
<body>  
<h1>Oops!</h1>  
<p>Sorry, I don't know what you're asking for.</p>  
</body>  
</html>

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Android学习21 -- launcher

1 前言 之前在工作中&#xff0c;第一次听到launcher有点蒙圈&#xff0c;不知道是啥&#xff0c;当时还赶鸭子上架去和客户PK launcher的事。后来才知道其实就是安卓的桌面。本来还以为很复杂&#xff0c;毕竟之前接触过windows的桌面&#xff0c;那叫一个复杂。。。 后面查了…

[创业之路-276]:从燃油汽车到智能汽车:工业革命下的价值变迁

目录 前言&#xff1a; 从燃油汽车到智能汽车&#xff1a;工业革命下的价值变迁 前言&#xff1a; 燃油汽车&#xff0c;第一次、第二次工业革命&#xff0c;机械化、电气化时代的产物&#xff0c;以机械和电气自动化为核心价值。 智能汽车&#xff0c;第三次、第四次工业革…

Spring Boot - 数据库集成07 - 数据库连接池

数据库连接池 文章目录 数据库连接池一&#xff1a;知识准备1&#xff1a;什么是数据库连接池&#xff1f;2&#xff1a;数据库连接池基本原理 二&#xff1a;HikariCP连接池1&#xff1a;简单使用2&#xff1a;进一步理解2.1&#xff1a;是SpringBoot2.x默认连接池2.2&#xf…

Python-基于PyQt5,Pillow,pathilb,imageio,moviepy,sys的GIF(动图)制作工具

前言&#xff1a;在抖音&#xff0c;快手等社交平台上&#xff0c;我们常常见到各种各样的GIF动画。在各大评论区里面&#xff0c;GIF图片以其短小精悍、生动有趣的特点&#xff0c;被广泛用于分享各种有趣的场景、搞笑的瞬间、精彩的动作等&#xff0c;能够快速吸引我们的注意…

使用线性回归模型逼近目标模型 | PyTorch 深度学习实战

前一篇文章&#xff0c;计算图 Compute Graph 和自动求导 Autograd | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 使用线性回归模型逼近目标模型 什么是回归什么是线性回归使用 PyTorch 实现线性回归模型代码执行结…

【蓝桥杯嵌入式】2_LED

1、电路图 74HC573是八位锁存器&#xff0c;当控制端LE脚为高电平时&#xff0c;芯片“导通”&#xff0c;LE为低电平时芯片“截止”即将输出状态“锁存”&#xff0c;led此时不会改变状态&#xff0c;所以可通过led对应的八个引脚的电平来控制led的状态&#xff0c;原理图分析…

尝试在Office里调用免费大语言模型的阶段性进展

我个人觉得通过api而不是直接浏览器客户端聊天调用大语言模型是使用人工智能大模型的一个相对进阶的阶段。 于是就尝试了一下。我用的是老师木 袁进辉博士新创的硅基流动云上的免费的大模型。——虽然自己获赠了不少免费token&#xff0c;但测试阶段用不上。 具体步骤如下&am…

LabVIEW自定义测量参数怎么设置?

以下通过一个温度采集案例&#xff0c;说明在 LabVIEW 中设置自定义测量参数的具体方法&#xff1a; 案例背景 ​ 假设使用 NI USB-6009 数据采集卡 和 热电偶传感器 监测温度&#xff0c;需自定义以下参数&#xff1a; 采样率&#xff1a;1 kHz 输入量程&#xff1a;0~10 V&a…

理解 C 与 C++ 中的 const 常量与数组大小的关系

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 &#x1f4af;前言&#x1f4af;数组大小的常量要求&#x1f4af;C 语言中的数组大小要求&#x1f4af;C 中的数组大小要求&#x1f4af;为什么 C 中 const 变量可以作为数组大小&#x1f4af;进一步的…

【Elasticsearch】文本分类聚合Categorize Text Aggregation

响应参数讲解: key &#xff08;字符串&#xff09;由 categorization_analyzer 提取的标记组成&#xff0c;这些标记是类别中所有输入字段值的共同部分。 doc_count &#xff08;整数&#xff09;与类别匹配的文档数量。 max_matching_length &#xff08;整数&#xff09;从…

基于SpringBoot的信息技术知识赛系统的设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

Windows Docker笔记-安装docker

安装环境 操作系统&#xff1a;Windows 11 家庭中文版 docker版本&#xff1a;Docker Desktop version: 4.36.0 (175267) 注意&#xff1a; Docker Desktop 支持以下Windows操作系统&#xff1a; 支持的版本&#xff1a;Windows 10&#xff08;家庭版、专业版、企业版、教育…

《Kettle保姆级教学-界面介绍》

目录 一、Kettle介绍二、界面介绍1.界面构成2、菜单栏详细介绍2.1 【文件F】2.2 【编辑】2.3 【视图】2.4 【执行】2.5 【工具】2.6 【帮助】 3、转换界面介绍4、作业界面介绍5、执行结果 一、Kettle介绍 Kettle 是一个开源的 ETL&#xff08;Extract, Transform, Load&#x…

新型智慧城市建设方案-1

智慧城市建设的背景与需求 随着信息技术的飞速发展&#xff0c;新型智慧城市建设成为推动城市现代化、提升城市管理效率的重要途径。智慧城市通过整合信息资源&#xff0c;优化城市规划、建设和管理&#xff0c;旨在打造更高效、便捷、宜居的城市环境。 智慧城市建设的主要内容…

【Java计算机毕业设计】基于Springboot的物业信息管理系统【源代码+数据库+LW文档+开题报告+答辩稿+部署教程+代码讲解】

源代码数据库LW文档&#xff08;1万字以上&#xff09;开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统&#xff1a;Window操作系统 2、开发工具&#xff1a;IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

ollama部署deepseek实操记录

1. 安装 ollama 1.1 下载并安装 官网 https://ollama.com/ Linux安装命令 https://ollama.com/download/linux curl -fsSL https://ollama.com/install.sh | sh安装成功截图 3. 开放外网访问 1、首先停止ollama服务&#xff1a;systemctl stop ollama 2、修改ollama的servic…

Agentic Automation:基于Agent的企业认知架构重构与数字化转型跃迁---我的AI经典战例

文章目录 Agent代理Agent组成 我在企业实战AI Agent企业痛点我构建的AI Agent App 项目开源 & 安装包下载 大家好&#xff0c;我是工程师令狐&#xff0c;今天想给大家讲解一下AI智能体&#xff0c;以及企业与AI智能体的结合&#xff0c;文章中我会列举自己在企业中Agent实…

图论常见算法

图论常见算法 算法prim算法Dijkstra算法 用途最小生成树&#xff08;MST&#xff09;&#xff1a;最短路径&#xff1a;拓扑排序&#xff1a;关键路径&#xff1a; 算法用途适用条件时间复杂度Kruskal最小生成树无向图&#xff08;稀疏图&#xff09;O(E log E)Prim最小生成树无…

手机上运行AI大模型(Deepseek等)

最近deepseek的大火&#xff0c;让大家掀起新一波的本地部署运行大模型的热潮&#xff0c;特别是deepseek有蒸馏的小参数量版本&#xff0c;电脑上就相当方便了&#xff0c;直接ollamaopen-webui这种类似的组合就可以轻松地实现&#xff0c;只要硬件&#xff0c;如显存&#xf…

Java进阶学习之路

Java进阶之路 提示&#xff1a;这里可以添加系列文章的所有文章的目录&#xff0c;目录需要自己手动添加 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 Java进阶之路前言一、Java入门 Java基础 1、Java概述 1.1 什…