Rust模块std::thread
【图书介绍】《Rust编程与项目实战》-CSDN博客
《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)
Rust到底值不值得学,之一 -CSDN博客
Rust到底值不值得学,之二-CSDN博客
Rust多线程编程概述-CSDN博客
12.3.2 等待所有线程完成
在前面的实例中,主线程没等到派生线程执行完毕就结束了,从而整个进程就会结束。那么怎么让派生线程执行完毕呢?答案是通过joinHandle结构体来等待所有线程完成。要了解派生线程何时完成,有必要捕获thread::spawn函数返回的JoinHandle,该结构体声明如下:
pub struct JoinHandle<T>(_);
该结构体通常由thread::spawn函数返回,或者由thread::Builder::spawn函数返回。JoinHandle在关联线程被丢弃时分离该线程,这意味着该线程不再有任何句柄,也无法对其进行连接。由于平台限制,无法克隆此句柄:加入线程的能力是唯一拥有的权限。
该结构体提供了一个函数join,允许调用方(比如主线程)等待派生线程(比如子线程)完成,该函数声明如下:
pub fn join(self) -> Result<T>
该函数等待相关线程完成。如果相关线程已经完成,则函数将立即返回。就原子内存排序而言,相关线程的完成与此函数返回同步。换句话说,该线程执行的所有操作都发生在 join 返回之后发生的所有操作之前。join的返回值通常是子线程执行的结果。
join函数的用法如下:
use std::thread;let thread_join_handle = thread::spawn(|| {//子线程执行的代码});//主线程执行的代码let res = thread_join_handle.join(); //等待子线程结束
thread_join_handle存放joinHandle结构,然后调用join方法,可以等待对应的线程执行完成。调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结join方法返回一个线程结果值,如果线程崩溃,则返回错误码,否则返回Ok。
res将得到子线程执行的结果,我们甚至可以在创建线程时,在子线程执行的代码处直接放一个数值或字符串,从而让res得到这个数值或字符串。
如果希望join调用失败时报错一下,可以这样:
thread_join_handle.join().expect("Couldn't join on the associated thread"); //若有问题就会有提示
下面先看一个简单的实例,得到子线程的结果,相当于实现了子线程传递一个值给主线程。
【例12.2】 子线程传递值给主线程
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;fn main() {let other_thread = thread::spawn(|| {"hello" //这里就写了一个字符串,相当于子线程的执行结果就是字符串"hello"});let res = other_thread.join().unwrap(); //得到子线程执行结果,即"hello"println!("{}",res);
}
保存文件并运行,运行结果如下:
hello
如果有兴趣,还可以把"hello"改为一个整数,那么res就得到这个整数值。我们甚至可以把一个函数返回值作为子线程结果传递给主线程,下面来看一个实例。
【例12.3】 把函数返回值传递给主线程
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;
fn thfunc(n: u32) -> u32 { //这个函数是线程函数,后面会讲到return n+1;
}
fn main() {let child = thread::spawn(|| {let f = thfunc(30); //调用线程函数f //返回子线程结果,这里也就是函数thfunc的返回值});let res = child.join().expect("Could not join child thread");println!("{}",res);
}
函数thfunc把参数n加1后再返回,并存于f中,然后把f作为子线程的结果,这样主线程通过join函数就可以得到f的值,也就是函数thfunc的返回值。
保存文件并运行,运行结果如下:
31
下面再看一个稍复杂点的实例,加一些循环打印。
【例12.4】 等待子线程执行完毕
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::thread;
use std::time::Duration;fn main() {let handle = thread::spawn(|| { //返回一个 JoinHandle 类型的值for i in 1..10 {println!("hi number {} from the spawned thread!", i);thread::sleep(Duration::from_millis(1));}});for i in 1..5 {println!("hi number {} from the main thread!", i);thread::sleep(Duration::from_millis(1));}handle.join().unwrap(); //阻止当前线程(主线程)执行,并等待子线程执行完毕
}
thread::spawn返回一个JoinHandle类型的值,可以将它存放到变量中。这个类型相当于子线程的句柄,用于连接线程。如果忽略它,就没有办法等待线程。在主线程main函数的结尾,我们调用了join方法来等待子线程执行完毕,即调用handle的join方法会阻止当前运行线程的执行,直到handle所表示的这些线程终结。unwrap 是一个方法,它用于从Option或Result类型中提取值。
保存文件并运行,运行结果如下:
hi number 1 from the main thread!
hi number 2 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 4 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
可以看到,子线程中的for循环全部执行完毕了。
不和主线程交互的子线程是非常少见的。Rust语言和其他语言不一样的地方是,如果线程中直接使用了其他线程定义的数据,则会报错。这里所说的外部变量就是其他线程中定义的变量。比如下面的代码,子线程中直接使用主线程定义的字符串就会报错:
use std::thread;fn main() {let data = String::from("hello world");let thread = std::thread::spawn(||{println!("{}", data);});thread.join();
}
如果编译就会报错:
error[E0373]: closure may outlive the current function, but it borrows `data`, which is owned by the current function
线程中使用了其他线程的变量是不合法的,必须使用move表明线程拥有data的所有权,我们可以使用move关键字把data的所有权转到子线程内,代码如下:
use std::thread;
fn main() {let data = String::from("hello world");let thread = std::thread::spawn(move ||{ //使用move 把data的所有权转到线程内println!("{}", data);});thread.join();
}
这个时候,就能正确输出结果“hello world”了。
move闭包通常和thread::spawn函数一起使用,它允许用户使用其他线程的数据,这样在创建新线程时,可以把其他线程中的变量的所有权从一个线程转移到另一个线程,然后就可以使用该变量了。下面来看一个实例,一个常见的应用模式是使用多线程访问列表中的元素来执行某些运算。
【例12.5】 多个子线程使用主线程中的数据
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::thread;fn main() {let v = vec![1, 3, 5, 7, 9];let mut childs = vec![];for n in v {let c = thread::spawn(move || {println!("{}", n * n);});childs.push(c);};for c in childs { //等待所有子线程结束c.join().unwrap();}
}
这里的move是必要的,否则没法保证主线程中的v会不会在子线程结束之前被销毁。使用move之后,所有权转移到了子线程内,从而使得不会出现因为生命周期造成的数据无效的情况。子线程的执行周期可能比主线程还长。因此,很可能出现结果还没有完全打印出来,就已经结束的情况。为了防止这个情况,我们存下每个线程句柄,并在最后使用 join 阻塞主线程。
保存文件并运行,运行结果如下:
1
9
25
49
81
12.3.4 线程函数
现在我们知道了,thread::spawn 函数接受一个闭包作为参数,闭包中的代码会在子线程中执行,比如:
let handle = thread::spawn(|| {//子线程执行的代码});
但如果子线程执行的代码比较长,我们通过会另外写一个函数来封装这些代码,这样在thread::spawn中只需写一个函数调用即可,这个在新线程中执行的函数通常称为线程函数。下面来看一个实例,我们设计了一个线程函数,并且它是一个递归函数,为了模拟长时间运行。
【例12.6】 使用一个递归的线程函数
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::thread;fn fibonacci(n: u32) -> u32 { //这个函数是线程函数,并且是一个递归函数,用于求斐波那契数列if n == 0 {return 0;} else if n == 1 {return 1;} else {return fibonacci(n - 1) + fibonacci(n - 2);}
}
fn main() {let child = thread::spawn(|| {let f = fibonacci(30); //调用线程函数println!("Hello from a thread!. fibonacci(30) = {}", f); //打印数列结果f //返回子线程结果,这里也就是函数fibonacci的返回值});println!("Hello, world!"); //主线程中执行的代码let v = child.join().expect("Could not join child thread"); //等待子线程结束,并得到子线程结果println!("value: {:?}", v);
}
我们把子线程中要执行的代码(这里是求斐波那契数列)单独放在一个函数fibonacci中,这样在thread::spawn函数中只需要调用该函数(fibonacci(30);)即可,这样代码简洁多了,而且方便模块化开发,比如可以让算法工程师专门实现斐波那契数列函数,而其他程序员只需要调用即可,这样可以做到并行开发,提高了效率。斐波那契数列函数执行时间较长,主线程末尾一定要等待子线程结束,也就是调用join函数,join返回一个Result,可以使用 expect 方法来获取返回值。这样主线程就会等待子线程完成,而不会先结束程序,这样就可以看到我们想要的结果。
保存文件并运行,运行结果如下:
Hello, world!Hello from a thread!. fibonacci(30) = 832040value: 832040
12.3.5 available_parallelism返回默认并行度
available_parallelism函数返回程序应使用的默认并行度的估计值。该函数声明如下:
pub fn available_parallelism() -> Result<NonZeroUsize>
并行性是一种资源,一台给定的机器提供了一定的并行能力,即它可以同时执行的计算数量的限制。这个数字通常对应CPU或计算机的数量,但在各种情况下可能会有所不同。诸如VM或容器编排器之类的主机环境可能希望限制其中的程序可用的并行量。这样做通常是为了限制(无意中)resource-intensive程序对同一台机器上运行的其他程序的潜在影响。
提供此函数的目的是提供一种简单且可移植的方式来查询程序应使用的默认并行度。它不公开有关NUMA区域的信息,不考虑(协)处理器能力的差异,并且不会修改程序的全局状态以更准确地查询可用并行度的数量。资源限制可以在程序运行期间更改,因此不会缓存该值,而是在每次调用此函数时重新计算,不应从热代码中调用它。
此函数返回的值应被视为在任何给定时间可用的实际并行量的简化近似值。要更详细或更准确地了解程序可用的并行量,用户可能还希望使用特定平台的API。以下平台限制当前适用于available_parallelism。
(1)在Windows上:它可能低估了具有超过64个逻辑CPU的系统上可用的并行量。但是,程序通常需要特定的支持才能利用超过64个逻辑CPU,并且在没有此类支持的情况下,此函数返回的数字准确地反映了程序默认可以使用的逻辑CPU的数量。它可能会高估受process-wide关联掩码或作业对象限制的系统上可用的并行量。
(2)在Linux上:当受process-wide关联掩码限制或受cgroup限制影响时,它可能会高估可用的并行量。
在具有CPU使用限制的VM(例如过度使用的主机)中运行时,可能会高估可用的并行量。此函数将在以下情况下(但不限于)返回错误:如果目标平台的并行量未知,或者程序没有权限查询可用的并行量。
【例12.7】 得到当前系统的默认并行度
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。
在main.rs中,添加代码如下:
use std::{io, thread};fn main() -> io::Result<()> {let count = thread::available_parallelism()?.get();assert!(count >= 1_usize);println!("{},{}",count,1_usize);Ok(())}
1_usize的值就是1。我们把得到的默认并行度存于count中,如果count小于1,那就抛出异常,否则打印结果。
保存文件并运行,运行结果如下:
2,1
可见,当前系统的默认并行度是2。
当前线程的属性包括线程id、名称。获取它们的函数被定义在std::thread:: Thread这个结构体中,所以我们首先要获取这个结构体,也就是获取当前线程的Thread结构体,这个函数是current,声明如下:
pub fn current() -> Thread
Thread其实是std::thread的一个私有结构(Struct),这个结构体对外提供了一些函数,以此来获取线程的id、名称等属性。
有了当前线程的Thread结构,就可以得到名称,该函数声明如下:
pub fn name(&self) -> Option<&str>
该函数返回字符串。
【例12.8】 获取和设置线程名称
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;fn main() {let thr0 = thread::current(); //获取当前线程的Thread结构let thread_name = thr0.name().unwrap_or("unknown"); //获取当前线程的名称println!("当前线程的名称:{}", thread_name); //打印输出名称}
unwrap_or是用于从Result对象中获取值的宏。当Result对象是Ok时,两者都会返回Ok中的值。但是当Result对象是Err时,unwrap_or将返回一个默认值。这个默认值是宏的参数,在调用unwrap_or时就已经确定了。所以,当你想要在Result对象是Err时使用固定的默认值时,就可以使用unwrap_or。
保存文件并运行,运行结果如下:
当前线程的名称:main
下面再看获得当前线程的id,线程id用于区分不同的线程,id号是唯一的。在Rust中,获得当前线程id的函数声明如下:
pub fn id(&self) -> ThreadId
该函数的返回值是一个结构体ThreadId,该结构体的大小是8字节。ThreadId是一个不透明的对象,它唯一地标识在进程生存期内创建的每个线程。线程ID保证不会被重用,即使在线程终止时也是如此。ThreadId受Rust的标准库控制,ThreadId和底层平台的线程标识符概念之间可能没有任何关系。因此,这两个概念不能互换使用。ThreadId可以从结构体Thread上的id函数中得到结果。
【例12.9】 得到线程id
打开VS Code,单击菜单Terminal→New Termanal,执行命令cargo new myrust来新建一个Rust工程,工程名是myrust。在main.rs中,添加代码如下:
use std::thread;fn main() {let child_thread = thread::spawn(|| { //创建线程thread::current().id() //在子线程中执行的代码得到的就是当前子线程的id}); let child_thread_id = child_thread.join().unwrap();//得到子线程的idassert!(thread::current().id() != child_thread_id);//如果表达式为假,则触发断言println!("thread::current().id()={:?},child_thread_id={:?}", thread::current().id(),child_thread_id);}
在代码中,我们不仅得到了当前线程id,还创建了另一个线程,并且比较了这两个线程id。assert!宏用于检查一个表达式是否为真(true),如果表达式为假(false),则会触发断言,程序会终止运行。
保存文件并运行,运行结果如下:
thread::current().id()=ThreadId(1),child_thread_id=ThreadId(2)
可见,主线程和子线程的id不一样。
相关文章:

Rust模块std::thread
【图书介绍】《Rust编程与项目实战》-CSDN博客 《Rust编程与项目实战》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com) Rust到底值不值得学,之一 -CSDN博客 Rust到底值不值得学,之二-CSDN博客 Rust多线程编程概述-CSDN博客 12.…...
Leetcode Day20 打家劫舍
198 最基础 class Solution:def rob(self, nums: List[int]) -> int:dp1 [0] * len(nums)dp2 [0] * len(nums)# dp1指第i天偷了, dp2指第i天没有偷dp1[0] nums[0]for i in range(1, len(nums)):dp1[i] dp2[i - 1] nums[i]dp2[i] max(dp1[i - 1], dp2[i - 1])return m…...

云计算之数据库
目录 一、RDS产品介绍及排障思路 1.1 云RDS数据库及其特点 1.2 云RDS数据库-规格 1.3 云RDS数据库-存储 1.4 云RDS数据库-安全 1.5 云RDS数据库-整体架构 1.6 RDS常见问题排查 1.6.1 如何解决无法链接RDS实例的问题 1.6.2 RDS实例存储空间使用率高,怎…...
开发软件,什么类型的重要信息的日志要存到数据库表里面
在开发软件时,选择将哪些类型的重要信息日志存储到数据库表里面,主要取决于这些日志的用途、查询需求、性能考虑以及系统架构。以下是一些通常会选择存储到数据库表中的重要信息日志类型: 1. 业务日志: 交易记录:记录…...
websocket和轮询的区别?
问: websocket和轮询的区别? 回答: WebSocket 和定时轮询(每隔几秒发送一次请求)是两种不同的实时通信方法,各有优缺点,适用于不同的场景。以下是它们的主要区别及适用场景: WebSo…...

2024 年全国大学生数学建模竞赛(国赛)浅析
需要完整资料,请关注WX:“小何数模”! (需要完整B、C和E题资料请关注WX:“小何数模”,获取资料链接!) 本次万众瞩目的全国大学生数学建模赛题已正式出炉,无论是赛题难度…...

持续集成与持续部署(CI/CD)的深入探讨
在现代软件开发中,持续集成(CI)和持续部署(CD)已成为不可或缺的实践。这些方法旨在加快软件交付的速度,同时提高软件的质量和稳定性。通过CI/CD,开发团队可以频繁地将代码更改集成到主分支&…...

hyperf json-rpc
安装 安装docker hyperf 安装 hyperf-rpc-server-v8 (服务端) docker run --name hyperf-rpc-server-v8 \ -v /www/docker/hyperf-rpc-server:/data/project \ -w /data/project \ -p 9508:9501 -it \ --privileged -u root \ --entrypoint /bin/sh \…...

基于SpringBoot的外卖点餐系统
你好呀,我是计算机学姐码农小野!如果有相关需求,可以私信联系我。 开发语言:Java 数据库:MySQL 技术:SpringBootJSP 工具:IDEA/Eclipse、Navicat、Maven、Tomcat 系统展示 首页 用户管理界…...

网络编程day02(字节序、TCP编程)
目录 【1】字节序 1》大小端转换 2》端口转换 3》IP地址转换 主机字节序转换为网络字节序 (小端序->大端序) 网络字节序转换为主机字节序(大端序->小端序) 【2】TCP编程 1》流程 2》函数接口 1> socket 2> …...
萌新6:临场发挥(区间dp)
题目描述 小x和室友总共 nnn 人,组团去打一款游戏,总共有 nnn 台电脑供他们使用,一人一台,最开始,第 iii 个人使用第 iii 台电脑。 小x评估了每个人的能力值和临场发挥值。 第 iii 个人的能力值为 aia_iai。 而他们…...

《数字信号处理》学习04-离散时间系统中的线性时不变系统
目录 一,系统及离散时间系统 二,离散时间系统中的线性时不变系统 1,线性系统 1) 可加性 2) 比例性(齐次性) 3)叠加原理(叠加性质) 2,时不变系统(移不变系统) 通过前几篇文章的学习,此时我对序列的相关概…...

ABAP 调试宏DEFINE
文章目录 调试过程完整程序 调试过程 完整程序 REPORT Z_TEST_DEFINE.TYPES: BEGIN OF GTY_DATA,NAME TYPE STRING,AGE TYPE I,END OF GTY_DATA. DATA: GS_DATA TYPE GTY_DATA,GT_DATA TYPE TABLE OF GTY_DATA. DEFINE D_TEST.GS_DATA-NAME &1.GS_DATA-AGE &2.APPE…...

Golang | Leetcode Golang题解之第393题UTF-8编码验证
题目: 题解: const mask1, mask2 1 << 7, 1<<7 | 1<<6func getBytes(num int) int {if num&mask1 0 {return 1}n : 0for mask : mask1; num&mask ! 0; mask >> 1 {nif n > 4 {return -1}}if n > 2 {return n}r…...

HarmonyOS开发实战( Beta5.0)DevEco Device Tool开发环境搭建实践
通常在嵌入式开发中,很多开发者习惯于使用Windows进行代码的编辑,比如使用Windows的Visual Studio Code进行OpenHarmony代码的开发。但当前阶段,大部分的开发板源码还不支持在Windows环境下进行编译,如Hi3516、Hi3518系列开发板。…...

WIFI贴项目到底是不是“骗局”呢?由我来揭秘!
各位亲爱的朋友们,大家好!我是你们的老朋友鲸天科技千千,一直在这片互联网的热土上耕耘。相信你们对我都不会陌生,因为我常常分享一些互联网上的新奇项目和实用技巧。如果你对我的内容感兴趣,别忘了点个关注哦…...

C++ string类—string修饰符、操作、非成员函数
一、Modifiers(修饰符): 1、operator 这个成员函数给一个string类类型的对象进行追加,在现有的string后面追加string类、字符串或者字符; 代码示例: void test1() {std::string s1("Hello ");…...

PVN3D(一)代码框架
在windows上配置pvn3d的环境一直配不成功,主要卡在了与C联合编译上,不知道如何处理了。索性先看看代码,竟然发现与论文中的代码对应上了。希望这一段时间把环境配置好。 1.论文中的网络结构 1.RGB图像特征,通过CNN提取特征。深度…...

「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究
「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究 文章目录 「OC」剪不断,理还乱——UIResponder、UIGestureRecognizer、UIControl的响应优先级探究前言介绍UIResponderUIGestureRecognizerUIControl 正文UIGestur…...
GitHub Copilot的详细介绍
目录 主要功能: 示例用法: GitHub Copilot 的优缺点: 优点: 缺点: 如何使用 GitHub Copilot? 总结: GitHub Copilot 是一种基于人工智能的编程助手,由 GitHub 和 OpenAI 联合…...
QMC5883L的驱动
简介 本篇文章的代码已经上传到了github上面,开源代码 作为一个电子罗盘模块,我们可以通过I2C从中获取偏航角yaw,相对于六轴陀螺仪的yaw,qmc5883l几乎不会零飘并且成本较低。 参考资料 QMC5883L磁场传感器驱动 QMC5883L磁力计…...
ssc377d修改flash分区大小
1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...
条件运算符
C中的三目运算符(也称条件运算符,英文:ternary operator)是一种简洁的条件选择语句,语法如下: 条件表达式 ? 表达式1 : 表达式2• 如果“条件表达式”为true,则整个表达式的结果为“表达式1”…...
多模态商品数据接口:融合图像、语音与文字的下一代商品详情体验
一、多模态商品数据接口的技术架构 (一)多模态数据融合引擎 跨模态语义对齐 通过Transformer架构实现图像、语音、文字的语义关联。例如,当用户上传一张“蓝色连衣裙”的图片时,接口可自动提取图像中的颜色(RGB值&…...

令牌桶 滑动窗口->限流 分布式信号量->限并发的原理 lua脚本分析介绍
文章目录 前言限流限制并发的实际理解限流令牌桶代码实现结果分析令牌桶lua的模拟实现原理总结: 滑动窗口代码实现结果分析lua脚本原理解析 限并发分布式信号量代码实现结果分析lua脚本实现原理 双注解去实现限流 并发结果分析: 实际业务去理解体会统一注…...
【RockeMQ】第2节|RocketMQ快速实战以及核⼼概念详解(二)
升级Dledger高可用集群 一、主从架构的不足与Dledger的定位 主从架构缺陷 数据备份依赖Slave节点,但无自动故障转移能力,Master宕机后需人工切换,期间消息可能无法读取。Slave仅存储数据,无法主动升级为Master响应请求ÿ…...

Unsafe Fileupload篇补充-木马的详细教程与木马分享(中国蚁剑方式)
在之前的皮卡丘靶场第九期Unsafe Fileupload篇中我们学习了木马的原理并且学了一个简单的木马文件 本期内容是为了更好的为大家解释木马(服务器方面的)的原理,连接,以及各种木马及连接工具的分享 文件木马:https://w…...
Python 包管理器 uv 介绍
Python 包管理器 uv 全面介绍 uv 是由 Astral(热门工具 Ruff 的开发者)推出的下一代高性能 Python 包管理器和构建工具,用 Rust 编写。它旨在解决传统工具(如 pip、virtualenv、pip-tools)的性能瓶颈,同时…...

三分算法与DeepSeek辅助证明是单峰函数
前置 单峰函数有唯一的最大值,最大值左侧的数值严格单调递增,最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值,最小值左侧的数值严格单调递减,最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

基于PHP的连锁酒店管理系统
有需要请加文章底部Q哦 可远程调试 基于PHP的连锁酒店管理系统 一 介绍 连锁酒店管理系统基于原生PHP开发,数据库mysql,前端bootstrap。系统角色分为用户和管理员。 技术栈 phpmysqlbootstrapphpstudyvscode 二 功能 用户 1 注册/登录/注销 2 个人中…...