【Linux】线程概念及线程互斥

目录

线程概念

线程优点

线程缺点

线程异常

线程系统编程接口

线程创建及终止

线程等待

使用线程系统接口封装一个小型的C++线程库并实现一个抢票逻辑

线程互斥

互斥量的接口

线程互斥实现原理

使用系统加锁接口封装LockGuard 实现自动化加锁

线程安全和可重入函数

常见线程安全和可重入情况

死锁

银行家算法简介


线程概念

线程是在进程内部,并且比进程更加轻量化的一种执行流。

线程是 CPU 调度的基本单位,而进程是承担系统资源的基本实体

一个进程内部可以包含多个线程,而在 LIinux 中,没有强制性的划分进程和线程的概念,线程也被叫做轻量级进程。

传统进程内部只有一个执行流;而进程内部创建了线程之后,内核中就有多个执行流了。

进程与线程的关系:

合理的使用多线程,能提高CPU执行密集型程序的执行效率和用户对IO密集型程序的使用体验!

CPU密集型程序指的是需要大量计算和处理的任务,涉及大量的数学运算、逻辑判断、数据处理等,对CPU的计算能力要求较高;IO密集型程序主要是指执行过程中需要大量的 IO 操作(大型文件多线程下载,涉及到大量的网络I/O操作和磁盘I/O操作,需要从网络中读取文件数据,并将其写入到本地磁盘中)。

CPU 是通过 PCB 对进程调度的,既然线程叫轻量级进程,那么在 Linux 中,CPU 调度线程的方式也是这样。但 进程 = 内核数据结构 + 代码和数据,相比于进程,线程就没有这么多资源了,线程共享进程的所有数据,但也有自己的数据:线程ID,一组寄存器,,errno,信号屏蔽字调度优先级,所以相比于进程,它的创建更简单。

各进程之间共享的进程资源和环境有:文件描述符表每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)当前工作目录用户id和组id。

线程在进程内部运行,本质是在进程地址空间内运行,所以对应调度所需要的寄存器少,且在轮转调度的时候不需要反复更新自己的线程上下文,从不需要更新缓存(cache)。

透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。

虚拟地址通过页表化为物理地址详细过程

虚拟地址的32个比特位,前20个比特位是 “页表索引” ,这前10个比特位,形成了 2^10 - 1个页目录,这 2^10 - 1个页目录中,每个页目录中又存着 2^10 - 1个 “后10个比特位” 形成的数,这样计算的,页表一共可以映射 2 ^ 20 (1048576)个数,而这些数(页表码),每个数对应一个地址标识,,一个IO文件的基本大小 4KB(2^10 * 2^2),对这 2^10 个基本IO文件编号(叫页内偏移),所以这 2^20 个页表码,每个都对应着 2^10 个基本 IO 文件的大小(4KB),2^20 次方个 基本IO 文件的大小,加起来也就是我们常说的 4GB

因此,页表码 + 页内偏移 就能知道对应的文件位置! 

对这 1048576 个页码建立数据结构,那么,对页表的管理,就变成了对数据结构的增删查改!

有了这种页表划分逻辑,进程就可以将地址空间合理的分配给线程!

线程优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程缺点

  • 性能损失

一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

  • 健壮性降低

编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

  • 缺乏访问控制

进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

  • 编程难度提高

编写与调试一个多线程程序比单线程程序困难得,这里的难度主要指考虑一段代码需要考虑的情况更复杂。

线程异常

单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃

线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程系统编程接口

线程创建及终止

  • 创建一个新的线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *
(*start_routine)(void*), void *arg);

thread:输出型参数,将线程ID作为参数返回

attr:设置线程的属性,attr为NULL表示使用默认属性

start_routine:是个函数指针,线程启动后要执行的函数

arg:传给线程启动函数的参数

返回值:成功返回0;失败返回错误码

  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);

pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面的线程ID属于进程调度的范畴,叫 LWP,只在系统内部使用,当一个进程中只有一个线程时,这个线程的 LWP 就等于进程的 PID 。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。


pthread_ create 函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的

  • 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。

2. 线程可以调用pthread_ exit终止自己。

3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_exit 函数可以获得进程的退出信息,存储在 value_ptr 指针中返回

void pthread_exit(void *value_ptr);

取消一个进程,成功返回0,失败返回错误码 

int pthread_cancel(pthread_t thread);// thread 为要终止线程的 id

线程等待

为什么要进行线程等待?

  • 线程终止后,内核资源并没有被释放,只有线程被等待成功后,内核资源才能被释放。
  • 等待可以得到线程的退出信息

pthread_join 接口可以等待线程结束

int pthread_join(pthread_t thread, void **value_ptr);

线程以不同的方式终止,用 pthread_join 接口得到的终止状态是不同的!

1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。

2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。

3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数。

4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

使用线程系统接口封装一个小型的C++线程库并实现一个抢票逻辑

#include <pthread.h>
#include <functional>
#include <string>
#include <iostream>
template <class T>
using func_t = std::function<void(T)>;template <class T>
class Thread
{
public:Thread(const std::string threadname, func_t<T> func, T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}// 因为pthread 里的 函数指针 规定只能有一个 void* 类型的参数, 如果不定义为 static(类内方法)的话, 第一个参数永远是this,那么就不止一个参数了static void* Pthread_Routine(void* args)  {Thread* ts = static_cast<Thread*>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, Pthread_Routine, this);//将 this 指针传过去, 方便一会调用函数指针使用传进来的 pthread 数据if(n == 0){_isrunning = true;return true;}else return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid, nullptr);if(n == 0) //等待成功{_isrunning = true;return true;}return false;}bool IsRunning(){return _isrunning;}std::string ThreadName(){return _threadname;}~Thread(){}
private:pthread_t _tid;std::string _threadname;bool _isrunning;func_t<T> _func;T _data;
};

实现一个抢票逻辑

#include <iostream>
#include "pthread.hpp"
#include <cstdio>
#include <unistd.h>
std::string GetThreadName()
{static int Number = 0;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", ++Number);return buffer;
}
void Print(int i)
{std::cout << i << std::endl;
}
int ticket = 10000;
void GetTicket(std::string name)
{while(true){if(ticket > 0){usleep(100);printf("%s get a ticket %d\n", name.c_str(), ticket);ticket--;        }else break;}
}
int main()
{std::string name1 = GetThreadName();Thread<std::string> th1(name1, GetTicket, name1);std::string name2 = GetThreadName();Thread<std::string> th2(name2, GetTicket, name2);std::string name3 = GetThreadName();Thread<std::string> th3(name3, GetTicket, name3);std::string name4 = GetThreadName();Thread<std::string> th4(name4, GetTicket, name4);th1.Start();th2.Start();th3.Start();th4.Start();th1.Join();th2.Join();th3.Join();th4.Join();return 0;
}

运行结果:确实使用多线程并发完成了抢票,但依然出现了一些问题:

票竟然被抢到了 0 和负数,这是这些线程并发访问公共资源的时候产生的数据不一致问题

  • 为什么可能无法获得争取结果?
  • if 语句判断条件为真以后,代码可以并发的切换到其他线程
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • --ticket 操作本身就不是一个原子操作(需要三步原子操作,这当中线程可能会被切换)

要解决上述问题,需要做到一下三点:  

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

本质就是系统需要一把锁将公共代码保护起来,Linux 将这把锁叫互斥量。

线程互斥

临界资源:在多线程编程中,被多个线程共享的一段代码或数据结构。这些资源在同一时间只能由一个线程访问。

临界区:在进程中访问临界资源的代码。

互斥:在任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,对临界资源起保护作用。

原子性:不会被任何调度机制打断的操作,改操作只有两态,要么完成,要么未完成。

互斥量的接口

  • 初始化互斥量

初始化互斥量有两种方法:

方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);

mutex:要初始化的互斥量

attr:NULL

  • 销毁互斥量

销毁互斥量需要注意:

使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁

不要销毁一个已经加锁的互斥量,已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误号

调用 pthread_ lock 时,可能会遇到以下情况:

互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功

发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

对互斥量加锁后,就能改进抢票系统了。

#include <iostream>
#include "pthread.hpp"#include <cstdio>
#include <unistd.h>class ThreadData
{
public:ThreadData(const std::string name, pthread_mutex_t* pmutex):_ThreadName(name),_mutex(pmutex){}
public:std::string _ThreadName;pthread_mutex_t* _mutex;
};
std::string GetThreadName()
{static int Number = 0;char buffer[64];snprintf(buffer, sizeof(buffer), "thread-%d", ++Number);return buffer;
}
void Print(int i)
{std::cout << i << std::endl;
}
int ticket = 10000;
void GetTicket(ThreadData* td)
{while(true){pthread_mutex_lock(td->_mutex);if(ticket > 0){usleep(10);printf("%s get a ticket %d\n",td->_ThreadName.c_str(),ticket);ticket--;        pthread_mutex_unlock(td->_mutex);}else {pthread_mutex_unlock(td->_mutex);break;}}
}
int main()
{pthread_mutex_t* mutex = new pthread_mutex_t;pthread_mutex_init(mutex, nullptr);std::string name1 = GetThreadName();ThreadData td1(name1, mutex);Thread<ThreadData*> th1(name1, GetTicket, &td1);std::string name2 = GetThreadName();ThreadData td2(name2, mutex);Thread<ThreadData*> th2(name2, GetTicket, &td2);std::string name3 = GetThreadName();ThreadData td3(name3, mutex);Thread<ThreadData*> th3(name3, GetTicket, &td3);std::string name4 = GetThreadName();ThreadData td4(name4, mutex);Thread<ThreadData*> th4(name4, GetTicket, &td4);th1.Start();th2.Start();th3.Start();th4.Start();th1.Join();th2.Join();th3.Join();th4.Join();return 0;
}

这样,当这段公共区代码被加锁后,解锁前其他的线程就不能访问这段代码了!

线程互斥实现原理

其实总结为一句话:当一个线程拿到开锁的钥匙之后,这个钥匙就通过 swap 变为了这个线程自己的上下文,就算线程被切换了,钥匙就被线程用上下文带走了,其他的线程依然无法打开这把锁!

使用系统加锁接口封装LockGuard 实现自动化加锁

#include <pthread.h>
class Mutex
{public:Mutex(pthread_mutex_t* lock):_lock(lock){}void Lock(){pthread_mutex_lock(_lock);}void UnLock(){pthread_mutex_unlock(_lock);}~Mutex(){}private:pthread_mutex_t* _lock;
};class LockGuard
{public:LockGuard(pthread_mutex_t* lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.UnLock();}private:Mutex _mutex;
};

只需要在使用时定义一个 LockGuard 对象,就不在用加锁和解锁了,作用有点像智能指针,通过对象的建立和销毁来控制加锁和解锁!

线程安全和可重入函数

线程安全

  • 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

可重入

  • 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

可重入与线程安全联系

函数是可重入的,那就是线程安全的;函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题;如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

可重入与线程安全区别

可重入函数是线程安全函数的一种;线程安全不一定是可重入的,而可重入函数则一定是线程安全的。如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

常见线程安全和可重入情况

常见线程安全情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

死锁四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

要破坏死锁,只需要破坏死锁的几个必要条件之一即可!

避免死锁:在写代码时加锁顺序一致,尽量避免锁未释放的场景,尽量资源一次性分配。

资源一次性分配(One-time Allocation of Resources)是指在程序运行过程中,某个特定的资源只会被分配一次,并在之后不会再次分配。这种分配方式意味着一旦资源被分配给了一个实体(如进程或线程),它将一直保持分配状态,直到被释放。

还有一些可以避免死锁的算法:如死锁检测算法、银行家算法

银行家算法简介

银行家算法是一种用于避免死锁的资源分配算法,它是由Edsger W. Dijkstra在1965年提出的。银行家算法的目标是通过合理的资源分配来避免系统陷入死锁的状态。

银行家算法基于以下假设:

  1. 每个进程在开始执行之前必须声明其最大资源需求量。
  2. 系统中的资源数量是固定的。
  3. 系统能够检测到每个进程的资源请求和释放情况。

算法步骤:

  1. 初始化:为每个进程分配它所需要的最大资源量、已分配资源量和还需要资源量。同时初始化系统的可用资源量。
  2. 请求资源:当一个进程需要申请一定数量的资源时,首先检查系统的可用资源是否大于等于请求资源的数量,如果是,则进一步检查分配给该进程资源后是否仍然能够避免死锁。如果是,则分配资源给该进程,更新系统的可用资源量和进程的已分配和还需资源量。如果否,则进程需要等待。
  3. 执行任务:当一个进程执行完毕后,释放所有已分配资源,并将这些资源回收到系统的可用资源池中。
  4. 检查安全性:在每次资源请求和释放之后,系统需要进行安全性检查,判断系统是否处于安全状态。如果系统处于安全状态,则继续执行下一个进程的资源请求,否则,进程需要等待。

银行家算法的核心思想是,每个进程在申请资源时,系统需要先判断是否能够保证分配资源后,系统仍然处于安全状态。如果无法保证安全性,则不分配资源给进程,以避免死锁的发生。

银行家算法的优点是能够有效地避免死锁的发生,缺点是需要事先知道每个进程的最大资源需求量,且需要保持资源分配表的实时更新。同时,该算法可能导致资源利用率较低,因为只有当系统处于安全状态时,才会分配资源给进程。

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

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

相关文章

HTML1:html基础

HTML 冯诺依曼体系结构 运算器 控制器 存储器 输入设备 输出设备 c/s(client客户端) 客户端架构软件 需要安装,更新麻烦,不跨平台 b/s(browser浏览器) 网页架构软件 无需安装,无需更新,可跨平台 浏览器 浏览器内核: 处理浏览器得到的各种资源 网页: 结构 HTML(超…

【Rust】生命周期

Rust 生命周期机制是与所有权机制同等重要的资源管理机制。 之所以引入这个概念主要是应对复杂类型系统中资源管理的问题。 引用是对待复杂类型时必不可少的机制&#xff0c;毕竟复杂类型的数据不能被处理器轻易地复制和计算。 但引用往往导致极其复杂的资源管理问题&#x…

Elasticsearch:我们如何演化处理二进制文档格式

作者&#xff1a;来自 Elastic Sean Story 从二进制文件中提取内容是一个常见的用例。一些 PDF 文件可能非常庞大 — 考虑到几 GB 甚至更多。Elastic 在处理此类文档方面已经取得了长足的进步&#xff0c;今天&#xff0c;我们很高兴地介绍我们的新工具 —— 数据提取服务&…

SCI一区 | Matlab实现NGO-TCN-BiGRU-Attention北方苍鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测

SCI一区 | Matlab实现NGO-TCN-BiGRU-Attention北方苍鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测 目录 SCI一区 | Matlab实现NGO-TCN-BiGRU-Attention北方苍鹰算法优化时间卷积双向门控循环单元融合注意力机制多变量时间序列预测预测效果基本介绍模型…

解决JavaWeb中IDEA2023新版本无法创建Servlet的问题

出现问题&#xff1a;IDEA右键创建Servlet时&#xff0c;找不到选项 原因分析&#xff1a;IDEA的2023版的已经不支持Servlet了&#xff0c;如果还要使用的话&#xff0c;需要自己创建模板使用 创建模板 右击设置&#xff0c;选择&#xff08;File and Code Templates&#x…

Selenium的简单防反爬和浏览器配置

# Selenium的简单使用&#xff1a;https://zhuanlan.zhihu.com/p/557463669 # 防反爬参考&#xff1a;https://blog.csdn.net/weixin_51368459/article/details/125462178 from selenium import webdriver from selenium.webdriver.edge.options import Options# 设置浏览器驱动…

UNIAPP(小程序)每十个文章中间一个广告

三十秒刷新一次广告 ad-intervals"30" <template><view style"margin: 30rpx;"><view class"" v-for"(item,index) in 100"><!-- 广告 --><view style"margin-bottom: 20rpx;" v-if"(inde…

vue3和vue2项目中如何根据不同的环境配置基地址?

在不同环境下取出的变量的值是不同的, 像这样的变量称为环境变量 为什么要使用环境变量呢? 开发环境生产环境下的接口地址有可能是不一样的&#xff0c;所以我们需要根据环境去配置不同的接口基地址 1、vue2环境变量配置 在根目录创建&#xff1a;.env.development和.env.p…

全栈的自我修养 ———— react中router入门+路由懒加载

router 下载router配置view创建目录配置index.js 下载router npm install react-router-dom配置view 如下将组件倒出 const Login () > {return <div>这是登陆</div> } export default Login创建目录 配置index.js React.lazy有路由懒加载的功能&#xff0…

LLMOps快速入门,轻松开发部署大语言模型

大家好&#xff0c;如今我们能够与ChatGPT进行轻松互动&#xff1a;只需输入提示&#xff0c;按下回车&#xff0c;就能迅速得到回应。然而&#xff0c;这个无缝互动的底层&#xff0c;是一系列复杂而有序的自动执行步骤&#xff0c;即大型语言模型运营&#xff08;LLMOps&…

SpringBoot快速入门笔记(4)

文章目录 一、Vue框架1、前端环境准备2、简介3、快速开始4、事件绑定 二、Vue组件化开发1、NPM2、Vue Cli3、组件化开发4、SayHello自定义组件5、Movie自定义组件 一、Vue框架 1、前端环境准备 编码工具&#xff1a;VSCode 依赖管理&#xff1a;NPM 项目构建&#xff1a;VueCl…

JDI LCD 驱动时序介绍

1. 引言 绝大多数 STM32 MCU 都可应用于图形界面&#xff0c;而且很多系列还内置图形硬件 LTDC&#xff0c;支持驱动 RGB 接口的 LCD。但是存在少数类型的 LCD&#xff0c;如 JDI 屏&#xff0c;除了 STM32L4P5 支持外&#xff0c;其它 MCU 只能根据屏的接口时序要求&#xff…

数学与人工智能:共舞于数字时代的奥秘

数学&#xff0c;这一源远流长的学科&#xff0c;长久以来一直为人类社会的发展与进步提供了坚实的基础。与此同时&#xff0c;随着科技的迅猛发展&#xff0c;人工智能这一新兴领域正逐渐改变着我们的生活方式。这两者之间&#xff0c;似乎存在着一种难以言喻的紧密联系。本文…

hbuilderX创建的uniapp项目转移到vscode

场景&#xff1a;一直使用hbuilderX开发的朋友想转移到vscode获取更好的TypeScript支持&#xff0c;所以想把整个项目目录拖到vscode进行开发&#xff0c;但发现运行不了&#xff0c;提示没有package.json等&#xff0c;并且不能执行pnpm命令 首先&#xff0c;我们先来看一下h…

软件测试(测试用例详解)(三)

1. 测试用例的概念 测试用例&#xff08;Test Case&#xff09;是为了实施测试而向被测试的系统提供的一组集合。 测试环境操作步骤测试数据预取结果 测试用例的评价标准&#xff1a; 用例表达清楚&#xff0c;无二义性。。用例可操作性强。用例的输入与输出明确。一条用例只有…

力扣---反转链表 II ***

给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5], left 2, right 4 输出&#xff1a;[1,4,3,…

【MATLAB】GA_BP神经网络时序预测算法

有意向获取代码&#xff0c;请转文末观看代码获取方式~ 1 基本定义 GA_BP神经网络时序预测算法是一种结合了遗传算法(GA)和反向传播(BP)神经网络的时序预测方法。它利用了遗传算法的全局搜索和优化能力&#xff0c;以及BP神经网络的学习和逼近能力&#xff0c;可以更有效地预…

Ubuntu中文输入法设置指南:从添加输入源到自定义设置的完整步骤

文章目录 Ubuntu设置中文输入法教程步骤1&#xff1a;打开“设置”应用程序步骤2&#xff1a;进入“区域与语言”设置步骤3&#xff1a;添加中文输入源步骤4&#xff1a;选择中文输入法步骤5&#xff1a;设置默认输入源&#xff08;可选&#xff09;步骤6&#xff1a;使用中文输…

C++算法 —— 前缀和

一、【模版】前缀和 1.链接 【模板】前缀和_牛客题霸_牛客网 (nowcoder.com) 2.描述 3.思路 前缀和的思想其实就是一种简单的动态规划&#xff0c;以i位置记录从头位置到i位置的和&#xff0c;然后间接的求一段连续区间的数组和&#xff0c;时间复杂度是O&#xff08;n&…