rust 创建多线程web server

news2024/11/24 5:35:44

创建一个 http server,处理 http 请求。

创建一个单线程的 web 服务

web server 中主要的两个协议是 http 和 tcp。tcp 是底层协议,http 是构建在 tcp 之上的。

通过std::net库创建一个 tcp 连接的监听对象,监听地址为127.0.0.1:8080.

use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        println!("connected!");
    }
}

运行cargo run,在浏览器中访问http://127.0.0.1:8080,可以看到控制台输出。

浏览器中显示链接被重置,无法被访问,因为没有响应任何数据。通过listener.incoming()方法返回一个迭代器,它是客户端与服务端之间打开的连接。称之为stream流,可以用来处理请求、响应。

首先处理请求,需要读取请求的参数,通过std::io库处理流信息,引入std::io::prelude::*包含一些读写流需要的特定 trait。

use std::io::{prelude::*, BufReader};
use std::net::TcpListener;

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    for stream in listener.incoming() {
        let mut stream = stream.unwrap();

        // 处理请求
        let buf_reader = BufReader::new(&stream);

        let http_request: Vec<_> = buf_reader
            .lines()
            .map(|result| result.unwrap())
            .take_while(|line| !line.is_empty())
            .collect();

        println!("requrest:{:#?}", http_request);
    }
}

BufReader 实现了BufReadtrait,提供了lines方法,通过换行符切割数据流返回一个Result<String,std::io::Error>迭代器。通过map获取到每一个结果值,take_while处理值直到为空结束,然后collect收集结果值。

http_request必须指定类型Vec<_>来收集。在闭包那一节中,迭代器适配器,必须调用消费适配器获取结果。

request-info.png

现在尝试给请求作出一个响应,响应状态码200表示成功响应。一个简单的响应头包括了协议、协议版本、响应状态、状态语句。

let res = "HTTP/1.1 200 OK\r\n\r\n";

stream.write_all(res.as_bytes()).unwrap();

重新启动,再次浏览器访问地址,可以看到空白页面,F12查看网络请求,可以看到请求成功

server-200.png

可以增加请求路径http://127.0.0.1:8080/home或增加参数看看请求信息的不同。将请求处理、响应处理放到一个函数中handle_request

接着可以返回一个html文件,这样页面就有了基础的展示效果。新建一个index.html文件

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=<device-width>, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <p>hello world</p>
  </body>
</html>

读取index.html文件,并将文件内容作为响应返回

let res_status = "HTTP/1.1 200 OK\r\n";

let contents = fs::read_to_string("index.html").unwrap();

let len = contents.len();

let res = format!("{res_status}Content-Length:{len}\r\n\r\n{contents}");

stream.write_all(res.as_bytes()).unwrap();

再次运行,浏览器访问可以看到页面上已经展示信息。现在只要是所有的请求访问都会返回index.html文件,通常我们会根据访问路径来处理响应,比如http://127.0.0.1:8080/home

限制如果有请求路径或者是参数,则响应一个404.html页面,获取http_request第一个元素匹配GET / HTTP/1.1,响应 200,其他访问都是返回 404.

fn handle_request(mut stream: TcpStream) {
    // 处理请求
    let buf_reader = BufReader::new(&stream);

    let http_request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    if http_request[0] == "GET / HTTP/1.1" {
        let res_status = "HTTP/1.1 200 OK\r\n";

        let contents = fs::read_to_string("index.html").unwrap();

        let len = contents.len();

        let res = format!("{res_status}Content-Length:{len}\r\n\r\n{contents}");

        stream.write_all(res.as_bytes()).unwrap();
    } else {
        let res_status = "HTTP/1.1 404 NOT FOUND\r\n";

        let contents = fs::read_to_string("404.html").unwrap();

        let len = contents.len();

        let res = format!("{res_status}Content-Length:{len}\r\n\r\n{contents}");

        stream.write_all(res.as_bytes()).unwrap();
    }
}

优化一下if else里的代码,只有响应状态、响应的文件不一样,其他逻辑都一样。

let (res_status, file_name) = if http_request[0] == "GET / HTTP/1.1" {
    ("HTTP/1.1 200 OK\r\n", "index.html")
} else {
    ("HTTP/1.1 404 NOT FOUND\r\n", "404.html")
};
let contents = fs::read_to_string(file_name).unwrap();

let len = contents.len();

let res = format!("{res_status}Content-Length:{len}\r\n\r\n{contents}");

stream.write_all(res.as_bytes()).unwrap();

main方法中的简化,调用处理请求的函数。

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    for stream in listener.incoming() {
        let mut stream = stream.unwrap();
        // 处理请求
        handle_request(stream);
    }
}

现在一个简易的 web 服务就好了,可以处理请求、可以处理响应。在这过程出现的错误我们都用unwrap方法处理,只要遇到错误,直接停止程序,而在真实环境中,需要处理这些错误,避免程序的不可访问。

创建多线程 server 服务

已经构建了单线程的服务,但是它每次只能处理一个请求,只要完成上一个请求之后才能处理下一个连接。如果请求很多,则需要等待,这种表现使得服务性能很差。

首先,来模拟演示一下单线程的堵塞行为,通过线程休眠模拟慢请求

use std::thread::{self};
use std::time::Duration;


fn handle_request(mut stream: TcpStream) {
    // ...

    // 将if部分改为match匹配,增加/sleep 路径匹配,用以堵塞线程
    let (res_status, file_name) = match &http_request[0][..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK\r\n", "index.html"),
        "GET /sleep HTTP/1.1" => {
            // 线程堵塞5s
            thread::sleep(Duration::from_secs(5));
            ("HTTP/1.1 200 OK", "index.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };
    // ...
}

然后我们打开两个浏览器的 tab 页,访问不同的地址带路径/sleep和不带路径/的,先访问带路径的,可以看到浏览器正在加载,再访问不带路径的也发现浏览器正在加载。等 5 秒过后,全部加载完成,如果直接访问不带路径/则瞬间访问成功。

为了处理这种情况,我们尝试为每一个请求都分配一个线程独立去处理请求任务。

构建一个线程池,当程序每收到新请求时,分配一个线程去处理该请求;其余线程等待处理其他接收到的请求,当线程处理完请求后,返回到线程池等待处理新的请求。这样我们就可以并发处理请求,这样就是服务的吞吐量。

线程池的线程数不易过多,以固有数量的线程等待处理请求。这可以防止拒绝式服务攻击DOS

除了多线程处理服务,还有其他方法改善服务吞吐量,fork/join模型、单线程异步 I/O 模型、多线程异步 I/O 模型。

修改main方法,thread::spawn会创建一个新线程并运行闭包里的代码。

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    for stream in listener.incoming() {
        let stream = stream.unwrap();

        thread::spawn(|| {
            handle_request(stream);
        });
        // handle_request(stream);
    }
}

现在可以再次尝试请求/sleep/,可以发现/瞬间就响应了,/sleep还需要等待 5s。如果有上千、上万个请求,我们就要开同等数量的线程,在占用完所有资源后,就会使系统奔溃。

通过线程池,创建有限的线程数量。在处理请求时,内部执行的方法execute会检测空闲的线程并执行之后的请求任务,如果请求超过线程池线程数量,则排队等待。

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    // 创建线程池
    let threadPool = ThreadPool::new(4);
    for stream in listener.incoming() {
        let stream = stream.unwrap();

        // thread::spawn(|| {
        //     handle_request(stream);
        // });
        threadPool.execute(|| {
            handle_request(stream);
        })
        // handle_request(stream);
    }
}

实现ThreadPool线程池类型

ThreadPool 类型并不存在于 rust 库中,需要我们自己实现ThreadPool

rust-lib项目中,新建库thread_pool, 在src/lib.rs中,通过new函数实现创建ThreadPool实例,它接受一个参数size为线程的数量;通过定义execute函数接受一个闭包参数,闭包作为参数可以使用三个不同的 traitFn\FnMut\FnOnce,要决定用哪个取决于最终的调用,最终是要调用thread::spawn()的,它是使用了FnOnce的,还需要Send来将闭包从一个线程转移到另一个线程,绑定生命周期'static是因为不知道线程会执行多久。

pub struct ThreadPool;

impl ThreadPool {
    /// 创建线程池
    ///
    /// 线程池中线程的数量
    pub fn new(size: usize) -> ThreadPool {
        ThreadPool
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
    }
}

定义完之后,回到项目rust-web项目,引入依赖,在Cargo.toml,

[dependencies]
thread_pool = {path="../rust-lib/thread_pool"}

然后在src/main.rs使用依赖use thread_pool::ThreadPool;, 运行程序cargo run,没有报错正常运行。

new方法中要保证初始化的线程数是一个有效的值,即size不能为分数或等于 0.这没有意义。然后初始化 vector 实例来存储线程实例,thread::spawn()执行后返回的类型为thread::JoinHandle,它可以管理并等待创建的线程完成任务。

use std::thread;

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

impl ThreadPool {
    /// 创建线程池
    ///
    /// 线程池中线程的数量
    ///
    /// # Panics
    ///
    /// `new`函数在size为0 时panicthread_pool
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        let mut threads = Vec::with_capacity(size);

        for _ in 0..size {
            // 创建对应数量的线程,并把它们存储到vec中
        }

        ThreadPool { threads }
    }
    // ...
}

之前一直在使用thread::spawn()来创建线程,并执行任务。现在在线程池中,需要提前创建线程,等待任务传入后再执行。标准的 rust 库中没有这样的定义,仍需要自己实现,可以称之为Worker数据结构,这样我们在ThreadPool存储的是Worker实例,在 worker 实例中存储一个单独的JoinHandle<()>实例,并赋予该实例一个唯一的id,方便日志和调用栈区分。

同样的,在ThreadPoolsrc/lib.rs 定义结构体Worker类型,对于外部 worker 类型是私有的,不需要pub定义。

use std::thread;

pub struct ThreadPool {
    // threads: Vec<thread::JoinHandle<()>>,
    workers: Vec<Worker>,
}
impl ThreadPool {
    /// 创建线程池
    ///
    /// 线程池中线程的数量
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        // let mut threads = Vec::with_capacity(size);
        let mut workers = Vec::with_capacity(size);

        for id in 0..size {
            // 创建对应数量的线程,并把它们存储到vec中
            workers.push(Worker::new(id))
        }

        ThreadPool { workers }
    }
    // ...
}

struct Worker {
    id: usize,
    thread: thread::JoinHandle<()>,
}

impl Worker {
    fn new(id: usize) -> Worker {
        let thread = thread::spawn(|| {});

        Worker { id, thread }
    }
}

运行我们的代码,正常运行。现在需要解决的是向创建的线程传递要处理的请求任务,通过之前文章中学过的channel信道来传递信息.

ThreadPool中存在一个信道实例充当发送者;并新建一个Job结构体存放用于向信道发送的闭包;execute方法会发送期望执行的任务。

use std::{sync::mpsc, thread};

pub struct ThreadPool {
    // threads: Vec<thread::JoinHandle<()>>,
    workers: Vec<Worker>,
    sender: mpsc::Sender<Job>,
}

struct Job;

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        // let mut threads = Vec::with_capacity(size);
        let mut workers = Vec::with_capacity(size);

        // 创建信道实例,提供一个发送者、接收者
        let (sender, receiver) = mpsc::channel();

        for id in 0..size {
            // 创建对应数量的线程,并把它们存储到vec中
            workers.push(Worker::new(id,receiver))
        }

        ThreadPool { workers, sender }
    }
}

ThreadPool实例存储信道发送者对象sender,需要将接受者实例receiver传递给Worker用于接收传递的信息。

impl Worker {
    fn new(id: usize, receiver: mpsc::Receiver<Job>) -> Worker {
        let thread = thread::spawn(|| {
            receiver;
        });

        Worker { id, thread }
    }
}

这会有一个错误信息,因为在 rust 中信道实现是多生产者、单消费者,不能将receiver接受者传递多个 work 实例。我们希望有一个任务列表,每个任务只允许处理一次。这在之前的文章中

rust 自动化测试、迭代器与闭包、智能指针、无畏并发

已经解决过在线程间共享状态,通过线程安全智能指针Arc<Mutex<T>>,多个线程共享所有权并允许线程修改其值。Arc使得多个 worker 拥有接受端,而Mutex确保一次只有一个 worker 能接收到任务。

use std::{
    sync::{mpsc, Arc, Mutex},
    thread,
};


impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
        assert!(size > 0);

        // let mut threads = Vec::with_capacity(size);
        let mut workers = Vec::with_capacity(size);

        let (sender, receiver) = mpsc::channel();

        // 通过`Arc<T>`创建多所有者,Mutex<T>共享数据
        let receiver = Arc::new(Mutex::new(receiver));

        for id in 0..size {
            // 创建对应数量的线程,并把它们存储到vec中
            workers.push(Worker::new(id, Arc::clone(&receiver)))
        }

        ThreadPool { workers, sender }
    }
}

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        // ...
    }
}

最后处理execute方法,它接受的闭包需要分配给空闲的线程并执行,修改Job结构体,它不是一个结构体,是接受execute方法接受的闭包类型的类型别名。

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

execute方法被调用后,新建Job实例,将任务从信道发送端发出,因为发送可能会失败,所以需要unwrap处理错误的发生。

impl ThreadPool {
    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        self.sender.send(job).unwrap();
    }
}

继续优化接受端执行任务的逻辑,在接收到任务后,通过lock获取互斥器来锁定资源,防止其他地方使用资源。通过unwrap处理错误时的情况,在获取了互斥器锁定了资源后,调用recv()方法接受任务Job,这会阻塞当前线程,所有如果当前线程没有任务,则会一直等待直到有用的任务。Mutex<T>可以确保一次只有一个 Worker 线程请求任务。

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!("开始执行任务{id}");

            job();
        });

        Worker { id, thread }
    }
}

通过loop循环执行闭包,一直向信道的接受端请求任务,并在得到任务时执行它们。

现在执行cargo run,并在浏览器中打开多个 tab 请求地址,可以看到打印输出

multi-web-service.png

不能使用其他循环,比如while let \ if let \ match是因为它们循环时相关的代码块结束都不会丢弃临时值,导致锁守护的资源不能释放,不能被访问。

程序停止与清理

当我们终止程序后,如何去处理未执行完的任务,如何清理资源。

ThreadPool实现Drop,当线程池被丢弃时,应该join所有线程以确保任务完成。

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("stop worker {}", worker.id);

            worker.thread.join().unwrap();
        }
    }
}

这里会有一个错误,不能编译,提示没有 worker 所有权,因为我们只得到了一个可变借用,不能调用join来消费线程。通过修改来使得thread实例成为一个Option值,这样就可以通过take方法来获取到其中Some成员值进行处理。清理时可以直接将thread赋值为None

struct Worker {
    id: usize,
    // thread: thread::JoinHandle<()>,
    thread: Option<thread::JoinHandle<()>>,
}

通过 rust 代码检测提示信息来修改其他需要调整的地方。Workernew 方法创建实例时,接收thread使用Some(thread)

在停止程序,清理时,通过take()获取到成员值后,再调用join()方法等待线程执行结束。

impl Drop for ThreadPool {
    fn drop(&mut self) {
        for worker in &mut self.workers {
            println!("stop worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

正常逻辑来说调用了join()之后会关闭线程,但是由于之前的线程逻辑是循环闭包调用等待接受任务,也就是会导致线程一直不会执行完毕,导致阻塞。一直阻塞在第一个线程结束上。

通过修改ThreadPoolDrop方法来显式丢弃sender。为了转移sender所有权,同样的使用Option类型来传递

pub struct ThreadPool {
    // threads: Vec<thread::JoinHandle<()>>,
    workers: Vec<Worker>,
    sender: Option<mpsc::Sender<Job>>,
}

impl ThreadPool {
    pub fn new(size: usize) -> ThreadPool {
    // ...
    // ...
    ThreadPool {
        workers,
        sender: Some(sender),
    }
    }

    pub fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let job = Box::new(f);

        //  self.sender.send(job).unwrap();
        self.sender.as_ref().unwrap().send(job).unwrap();
    }
}

impl Drop for ThreadPool {
    fn drop(&mut self) {
        // 显示的丢弃sender
        drop(self.sender.take());

        for worker in &mut self.workers {
            println!("stop worker {}", worker.id);

            if let Some(thread) = worker.thread.take() {
                thread.join().unwrap();
            }
        }
    }
}

Drop()方法调用显示的丢弃sender后,这会关闭信道,表明了后续不会有消息发送,这时在Worker中无限循环调用接受消息的方法都会返回错误,此时可以修改逻辑在遭遇错误后退出循环。

impl Worker {
    fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
        let thread = thread::spawn(move || loop {
            let message = receiver.lock().unwrap().recv();

            match message {
                Ok(job) => {
                    println!("开始执行任务{id}");

                    job();
                }
                Err(_) => {
                    println!("worker {id} disconnected");
                    break;
                }
            }
        });

        Worker {
            id,
            thread: Some(thread),
        }
    }
}

现在可以正常清理、停机了,如果希望在服务停止前再处理几个请求,通过take()方法模拟只两个请求进行处理,来验证停机的逻辑。它是Iteratortrait

fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    // 创建线程池
    let pool = ThreadPool::new(4);
    for stream in listener.incoming().take(2) {
        let stream = stream.unwrap();

        pool.execute(|| {
            handle_request(stream);
        })
    }
}

现在运行程序cargo run,同时在浏览器请求三次,看看控制台如何打印信息,第三个请求不会被执行。

multi-web-service-stop.png

可以看到只执行完了两次请求,在第一次请求处理完成后,调用了Drop方法显示的丢弃了信道发送者sender,这样整个就导致所有 worker 关闭连接。

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

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

相关文章

轻松合并多个TXT文本,实现一键文件整理!

亲爱的读者们&#xff0c;您是否曾经需要将多个TXT文本文件合并成一个文件&#xff0c;却苦于无从下手&#xff1f;现在&#xff0c;我们向您介绍一个全新的TXT文本合并工具&#xff0c;让您轻松实现一键文件整理&#xff01; 首先&#xff0c;在首助编辑高手的主页面板块栏里…

数据库分库分表的原则

目录 1、数据库分库分表是什么 2、为什么要对数据库分库分表 3、何时选择分库分表 4、⭐分库分表遵循的原则 5、分库分表的方式 6、数据存放在表和库中的规则&#xff08;算法&#xff09; 7、分库分表的架构模式 8、分库分表的问题 小结 1、数据库分库分表是什么 数…

嵌入式学习笔记(64)指针带来的一些符号的理解

我们写的代码是给编译器看的&#xff0c;代码要想达到你想象的结果&#xff0c;就必需要编译器对你的代码的理解和你自己对代码的理解一样。编译器理解代码就是理解的符号&#xff0c;所以我们要正确理解C语言中的符号&#xff0c;才能像编译器一样思考程序、理解代码。 3.2.1…

如何入门学习黑客技术?如何选择编程语言?如何选择适合黑客的操作系统?

‘ 一 ’ 了解黑客技术的基础知识 学习黑客技术需要对网络安全和计算机系统有一定的了解。可以通过参加安全培训班、阅读专业书籍和学术论文、浏览网络安全博客和论坛等方式获取基础知识。涉及的内容包括网络协议、操作系统原理、计算机网络和编程等。 如果你对网络安全入门…

C语言 每日一题 PTA 10.28 day6

1.求奇数分之一序列前N项和 本题要求编写程序&#xff0c;计算序列 1 1 / 3 1 / 5 ... 的前N项之和。 输入格式 : 输入在一行中给出一个正整数N。 输出格式 : 在一行中按照“sum S”的格式输出部分和的值S&#xff0c;精确到小数点后6位。题目保证计算结果不超过双精度范围…

基于 Python 的豆瓣电影分析、可视化系统,附源码

文章目录 1 简介2 技术栈具体实现1.设计豆瓣电影自动化爬虫程序&#xff0c;自动获取电影数据2.对爬取到的数据进行清洗和预处理&#xff0c;包括多维度数据字段清洗和扩充3.将清洗好的数据存储到MySQL数据库中 4 具体效果图5 推荐阅读 1 简介 基于Python flask 的豆瓣电影分析…

搭建产品使用说明书,方法很简单,只要这个工具

产品使用说明书&#xff0c;它应该既包含产品外观及内容的客观介绍&#xff0c;又包括对业务操作流程的详细讲解。例如&#xff1a;公司介绍、产品背景、使用场景、产品功能、特色、亮点、内容逻辑 ...... 使用工具搭建 当下业内很多人士都会通过类似于HelpLook这样的工具来搭…

Ant Design Vue UI框架的基础使用,及通用后台管理模板的小demo【简单】

一、创建 VUE 项目 npm create vuelatest二、安装使用 ant-design-vue 安装脚手架工具 $ npm install -g vue/cli # OR $ yarn global add vue/cli使用组件 # 安装 $ npm i --save ant-design-vue4.x全局完整注册 import { createApp } from vue; import Antd from ant-de…

CANOE 仿真+测试

仿真测试 CANoe的自动化测试系统简介Canoe TFS常用函数测试判别函数测试架构函数测试报告函数检测函数 创建自动化测试工程其他常用函数 CANoe的自动化测试系统简介 基于CANoe的自动化测试系统架构&#xff0c;根据ECU的测试环境和测试规范&#xff0c;搭建基于CANoe的测试系统…

深入了解 Elasticsearch 8.1 中的 Script 使用

一、什么是 Elasticsearch Script&#xff1f; Elasticsearch 中的 Script 是一种灵活的方式&#xff0c;允许用户在查询、聚合和更新文档时执行自定义的脚本。这些脚本可以用来动态计算字段值、修改查询行为、执行复杂的条件逻辑等等。 二、支持的脚本语言有哪些 支持多种脚本…

用已安装好的系统级别PsychoPy软件配置Python虚拟环境

原创内容&#xff0c;仅供参考&#xff0c;欢迎大家批评指正&#xff01; 本人在使用PsychoPy软件开发实验系统的时候遇到一个问题&#xff1a;我已经在win10系统安装了PsychoPy软件&#xff0c;同时基于友好的图形化界面开发了大部分系统功能&#xff0c;但我需要在我anaconda…

FreeRTOS深入教程(任务创建的深入和任务调度机制分析)

文章目录 前言一、深入理解任务的创建二、任务的调度机制1.FreeRTOS中任务调度的策略2.FreeRTOS任务调度策略实现的核心3.FreeRTOS内部链表源码解析4.如何通过就绪链表管理任务的执行顺序 三、一个任务能够运行多久1.高优先级任务可抢占低优先级任务一直运行2.相同优先级的任务…

深入浅出排序算法之基数排序

目录 1. 前言 1.1 什么是基数排序⭐⭐⭐ 1.2 执行流程⭐⭐⭐⭐⭐ 2. 代码实现⭐⭐⭐ 3. 性能分析⭐⭐ 3.1 时间复杂度 3.2 空间复杂度 1. 前言 一个算法&#xff0c;只有理解算法的思路才是真正地认识该算法&#xff0c;不能单纯记住某个算法的实现代码&#xff01; 1.…

黑盒测试、白盒测试详解

前言 对于很多刚开始学习软件测试的小伙伴来说&#xff0c;如果能尽早将黑盒、白盒测试弄明白&#xff0c;掌握两种测试的结论和基本原理&#xff0c;将对自己后期的学习有较好的帮助。今天&#xff0c;我们就来聊聊黑盒、白盒测试的相关话题。 同时&#xff0c;我也为大家准备…

SparkSQL综合案例-省份维度的销售情况统计分析

一、项目背景 二、项目需求 &#xff08;1&#xff09;需求 ①各省销售指标&#xff0c;每个省份的销售额统计 ②TOP3销售省份中&#xff0c;有多少家店铺日均销售额1000 ③TOP3省份中&#xff0c;各个省份的平均单价 ④TOP3省份中&#xff0c;各个省份的支付类型比例 &#x…

基于jquery+html开发的json格式校验工具

json简介 JSON是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。 它基于JavaScript Programming Language, Standard ECMA-262 3rd Edition - December 1999的一个子集。 JSON采用完全独立于语言的文本格式&#xff0c;但是也使用了类似于C语言家族…

打破信息孤岛,如何从API、数据中台突围

“烟囱”林立&#xff0c;零售企业“数据孤岛”现象突出 所谓数据孤岛&#xff0c;是指零售企业不同组织机构之间、不同部门之间或不同软件之间的数据无法连接互动&#xff0c;数据信息不能共享&#xff0c;设计、管理、生产的数据不能相互交流&#xff0c;数据出现脱节的现象…

tomcat必要的配置

tomcat要配置两个&#xff0c;不然访问不了localhost:8080 名&#xff1a;CATALINA_HOME 值&#xff1a;D:\software\computer_software\Tomcat\tomcat8.5.66

C/C++版数据结构和算法知识概要

数据结构和算法是计算机科学领域中的重要基础知识&#xff0c;无论您是初学者还是有经验的程序员&#xff0c;都必须深入了解这些概念。本篇博客将为您提供关于数据结构、抽象数据类型、算法、算法分析以及面向对象编程的综合概述&#xff0c;每个部分都将附有具体的代码示例。…

技术栈 业务架构 插件库

大前端 技术栈 业务架构 插件库