当前位置: 首页 > news >正文

【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. 检查安全性:在每次资源请求和释放之后,系统需要进行安全性检查,判断系统是否处于安全状态。如果系统处于安全状态,则继续执行下一个进程的资源请求,否则,进程需要等待。

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

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

相关文章:

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

目录 线程概念 线程优点 线程缺点 线程异常 线程系统编程接口 线程创建及终止 线程等待 使用线程系统接口封装一个小型的C线程库并实现一个抢票逻辑 线程互斥 互斥量的接口 线程互斥实现原理 使用系统加锁接口封装LockGuard 实现自动化加锁 线程安全和可重入函数 …...

测试需求分析

测试需求是什么&#xff1f; --需求文档 测试需求主要解决**“测什么”的问题&#xff0c;一般来自需求规格说明书中原始需求 测试需求应全部覆盖已定义的业务流程&#xff0c;以及功能和非功能**方面的需求 功能&#xff1a;基本用户需求–优先 非功能&#xff1a;界面&#…...

Qt 翻译工具:使用 tr() 函数实现多语言支持

引言 在开发跨平台应用程序时&#xff0c;支持多语言是一个常见需求。Qt 提供了一套完整的国际化工具&#xff0c;帮助开发者轻松实现应用程序的本地化。本文将介绍如何在 Qt 中使用 tr() 函数进行翻译&#xff0c;并总结一些常见的困难和解决方法。 使用 tr() 函数进行翻译 …...

使用 kustomize 对 kubernetes 对象进行声明式管理

补丁实战 策略合并补丁 基准文件&#xff1a;/test/bases/deploy.yml apiVersion: apps/v1 kind: Deployment metadata:namespace: sharkname: my-nginx spec:selector:matchLabels:run: my-nginxreplicas: 2template:metadata:labels:run: my-nginxspec:containers:- name:…...

Android Studio开发学习(六)———TableLayout(表格布局)、FrameLayout(帧布局)

目录 前言 一、Tablelayout &#xff08;一&#xff09;Tablelayout的相关简介 &#xff08;二&#xff09;TableLayout使用方法 1. 当TableLayout下面写控件、则控件占据一行的大小。(自适应一行&#xff0c;不留空白) 2.多个组件占据一行&#xff0c;则配合TableRow实现…...

c++ override关键字

在C11及之后的标准中&#xff0c;override是一个关键字&#xff0c;用于表示派生类中的成员函数覆盖了基类中的虚函数。 使用override关键字的好处在于它提供了一种明确的方式来指示编译器&#xff1a;该函数打算覆盖基类中的虚函数。如果使用了override关键字&#xff0c;但该…...

卫星影像联合无人机实现农业保险全生命周期监管监测

随着科技的进步&#xff0c;农业保险监管系统的发展日新月异。特别是近年来&#xff0c;随着卫星技术与无人机技术的结合&#xff0c;为农业保险监管系统带来了前所未有的革新。本文将深入探讨如何利用卫星与无人机方案构建高效的农业保险监管系统&#xff0c;并结合实例进行说…...

ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型

ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型 文章目录 ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型一、介绍二、使用方式1、环境安装2、代码调用3、从本地加载模型 4、API 部署 三、低成本部署1、模型量化2、CPU 部署3、Mac 部署4、…...

JAVA的学习日记DAY6

文章目录 数组例子数组的使用数组的注意事项和细节练习数组赋值机制数组拷贝数组反转数组添加 排序冒泡排序 查找多维数组 - 二维数组二维数组的使用二维数组的遍历杨辉三角二维数组的使用细节和注意事项练习 开始每日一更&#xff01;得加快速度了&#xff01; 数组 数组可以…...

Grafana告警(邮件)自定义模板配置

一年前给客户部署配置过grafana&#xff0c;告警配置也是用的原始的&#xff0c;客户在使用过程中只需要一些核心点信息&#xff0c;想要实现这个就需要用Grafana的自定义告警模板以及编辑邮件模板。 通知模板 模板信息的配置中查阅了相关资料&#xff0c;自己组装了一套&…...

大话设计模式——六大基本设计原则(SOLID原则)

设计模式 定义&#xff1a;软件开发中&#xff0c;在特定上下文中解决一类常见问题的被证明为有效的最佳实践。可供其他开发者重复使用解决相似问题。 好处&#xff1a; 提高代码的可重用性&#xff0c;减少重复代码。提高代码的可维护性&#xff0c;使代码更易于理解和修改。…...

Qt | Q_PROPERTY属性和QVariant 类

一、属性基础 1、属性与数据成员相似,但是属性可使用 Qt 元对象系统的功能。他们的主要差别在于存取方式不相同,比如属性值通常使用读取函数(即函数名通常以 get 开始的函数)和设置函数(即函数名通常以 set 开始的函数)来存取其值,除此种方法外,Qt 还有其他方式存取属性值…...

力扣207.课程表

你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出&#xff0c;其中 prerequisites[i] [ai, bi] &#xff0c;表示如果要学习课程 ai 则 必须 先学习课程 bi 。 例如…...

十五届web模拟题整理

模拟赛一期 1.动态的Tab栏 请在 style.css 文件中补全代码。 当用户向下滚动的高度没有超过标题栏&#xff08;即 .heading 元素&#xff09;的高度时&#xff0c;保持 Tab 栏在其原有的位置。当滚动高度超过标题栏的高度时&#xff0c;固定显示 Tab 栏在网页顶部。 /* TODO…...

ubuntu20.04 安裝PX4 1.13

step1_install_depenences.sh #!/bin/bash #install gazebo 11 #install protobuf 3.19.6python3 -m pip install --upgrade pip python3 -m pip install --upgrade Pillow# 將 empy 的版本調整爲3.3.4 pip3 uninstall empy pip3 install empy3.3.4sudo apt-get update sudo ap…...

大型网站系统架构演化

大型网站质量属性优先级&#xff1a;高性能 高可用 可维护 应变 安全 一、单体架构 应用程序&#xff0c;数据库&#xff0c;文件等所有资源都在一台服务器上。 二、垂直架构 应用和数据分离&#xff0c;使用三台服务器&#xff1a;应用服务器、文件服务器、数据服务器 应用服…...

探索Java中的栈:Stack与Deque(ArrayDeque和LinkedList)

文章目录 1. 栈&#xff08;Stack&#xff09;1.1 定义方式1.2 特点1.3 栈的层次结构 2. 双端队列&#xff08;Deque&#xff09;2.1 定义方式及继承关系2.2 特点&#xff1a;2.3 ArrayDeque2.4 LinkedList2.5 Deque 的各种方法2.6 如何选择ArrayDeque和LinkedList 3. 如何选择…...

实践笔记-03 docker buildx 使用

docker buildx 使用 1.启用docker buildx2.启用 binfmt_misc3.从默认的构建器切换到多平台构建器3.1创建buildkitd.toml文件&#xff08;私有仓库是http没有证书的情况下&#xff0c;需要配置&#xff09;3.2创建构建器并使用新创建的构建器 4.构建多架构镜像并推送至harbor仓库…...

【数据结构与算法】之8道顺序表与链表典型编程题心决!

个人主页&#xff1a;秋风起&#xff0c;再归来~ 数据结构与算法 个人格言&#xff1a;悟已往之不谏&#xff0c;知来者犹可追 克心守己&#xff0c;律己则安&#xff01; 目录 1、顺序表 1.1 合并两个有序数组 1.2 原地移除数组中所有的元素va…...

Go 源码之旅-开篇

欢迎来到《Go 源码之旅》专栏&#xff01;在这个专栏中&#xff0c;我们将深入探索 Go 编程语言的内部数据结构的工作原理&#xff0c;一起踏上一段令人兴奋的源码之旅。 我们将一步步解析关键的数据结构底层工作原理以及一些常用框架的设计原理及其源码。 无论你是初学者还是…...

spring的事件推送

本质上是设计模式中的观察者模式。 一、什么是观察者模式 观察者模式是一种行为型设计模式&#xff0c;它定义了一种一对多的依赖关系&#xff0c;当一个对象的状态发生改变时&#xff0c;其所有依赖者都会收到通知并自动更新。 二、什么是spring的事件推送 在 Spring 的事…...

计算机网络—HTTPS协议详解:工作原理、安全性及应用实践

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;ヒューマノイド—ずっと真夜中でいいのに。 1:03━━━━━━️&#x1f49f;──────── 5:06 &#x1f504; ◀️ ⏸…...

卫星遥感影像在农业方面的应用及评价

一、引言 随着科技的进步&#xff0c;卫星遥感技术在农业领域的应用越来越广泛。卫星遥感技术以其宏观、快速、准确的特点&#xff0c;为农业生产和管理提供了有力的技术支撑。本文将对卫星遥感在农业方面的应用进行详细介绍&#xff0c;并通过具体案例进行说明。 二、…...

docker pull镜像的时候指定arm平台

指定arm平台 x86平台下载arm平台的镜像包 以mysql镜像为例 docker pull --platform linux/arm64 mysqldocker images查看镜像信息 要查看Docker镜像的信息&#xff0c;可以使用docker inspect命令。这个命令会返回镜像的详细信息&#xff0c;包括其元数据和配置。 docker i…...

如何通过OceanBase V4.2 动态采样优化查询性能

OceanBase v4.2 推出了优化器动态采样的功能&#xff0c;在SQL运行过程中&#xff0c;该功能会收集需要的统计信息&#xff0c;协助优化器制定出更好的执行计划&#xff0c;进一步提升了查询性能。 影响查询性能的因素是什么&#xff1f;为何你的优化器效果不佳&#xff1f; …...

Vue3---基础1(认识,创建)

变化 相对于Vue2&#xff0c;Vue3的变化&#xff1a; 性能的提升 打包大小减少 41% 初次渲染快 55%&#xff0c;更新渲染快133% 内存减少54% 源码的升级 使用 proxy 代替 defineProperty 实现响应式 重写虚拟 DOM 的实现和 Tree-shaking TypeScript Vue3就可以更好的支持TypeSc…...

JAVA集合ArrayList

目录 ArrayList概述 add(element) 用法 add(index, element)用法 remove&#xff08;element&#xff09;用法 remove&#xff08;index&#xff09;用法 get(index)用法 set(index,element) 练习 test1 定义一个集合&#xff0c;添加字符串&#xff0c;并进行遍历&…...

Bitmap OOM

老机器Bitmap预读仍然OOM&#xff0c;无奈增加一段&#xff0c;终于不崩溃了。 if (Build.VERSION.SDK_INT < 21)size 2; 完整代码&#xff1a; Bitmap bitmap; try {//Log.e(Thread.currentThread().getStackTrace()[2] "", surl);URL url new URL(surl);…...

基于深度学习的人脸表情识别系统(PyQT+代码+训练数据集)

基于深度学习的人脸表情识别系统&#xff08;PyQT代码训练数据集&#xff09; 前言一、数据集1.1 数据集介绍1.2 数据预处理 二、模型搭建三、训练与测试3.1 模型训练3.2 模型测试 四、PyQt界面实现 前言 本项目是基于mini_Xception深度学习网络模型的人脸表情识别系统&#x…...

Qt 中的项目文件解析和命名规范

&#x1f40c;博主主页&#xff1a;&#x1f40c;​倔强的大蜗牛&#x1f40c;​ &#x1f4da;专栏分类&#xff1a;QT❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、Qt项目文件解析 1、.pro 文件解析 2、widget.h 文件解析 3、main.cpp 文件解析 4、widget.cpp…...

免费下载软件的网站/广告联盟论坛

我是极简主义者&#xff0c;崇尚简洁明快的代码风格&#xff0c;这也可能是我不喜欢Java全家桶的原因……当然我说的简洁是要建立在不降低可读性的前提下&#xff0c;即不影响代码本身的表现力。如果为求代码精简而让代码晦涩艰深同样不可取。本文会介绍10个条款&#xff0c;后…...

手机网站制作价格/百度云资源链接分享群组

7.表类型(存储引擎)的选择7.1 Mysql存储引擎概述 mysql支持插件式存储引擎&#xff0c;即存储引擎以插件形式存在于mysql库中。 mysql支持的存储引擎包括&#xff1a;MyISAM、InnoDB、BDB、MEMORY、MERGE、EXAMPLE、NDB Cluster、ARCHIVE、CSV、BLACKHOLE、FEDERATED等。 其中&…...

网站换域名了怎么做301重定向/怎么样自己创建网站

原文地址&#xff1a;Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)&#xff08;Linux-3.0 ARMv7&#xff09; 作者&#xff1a;tekkamanninja 转自&#xff1a;http://blog.chinaunix.net/uid-25909619-id-4938395.html 前面粗略分析start_kernel函数&#x…...

wordpress后台不提醒更新/百度推广代理商名单

变量赋值是我们在日常开发中经常会遇到的一个问题&#xff0c;本文主要给大家介绍的是关于python将函数赋值给变量时需要注意的一些问题&#xff0c;分享出来供大家参考学习&#xff0c;下面话不多说了&#xff0c;来一起看看详细的介绍&#xff1a;见过两种函数赋值给变量的形…...

wordpress多用户模版/市场营销案例100例

marker on google Maps Page 186 在谷歌地图上增加你的地理坐标。 更多有关Google Maps JavaScript API http://code.google.com/apis/maps/documentation/javascript/ 使用方法如下。 需要在原来的基础上增加一个方法在上一篇博客的showMap()函数中。是加入 function addMarke…...

做短租类型的网站/关键词优化工具

在第一次使用TortoiseSVN从服务器CheckOut的时候&#xff0c;会要求输入用户名和密码&#xff0c;这时输入框下面有个选项是保存认证信息&#xff0c;如果选了这个选项&#xff0c;那么以后就不用每次都输入一遍用户名密码了。 不过&#xff0c;如果后来在服务器端修改了用户名…...