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

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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

20240405,数据类型,运算符,程序流程结构

是我深夜爆炸&#xff0c;不能再去补救C了&#xff0c;真的来不及了&#xff0c;不能再三天打鱼两天晒网了&#xff0c;真的来不及了呜呜呜呜 我实在是不知道看什么课&#xff0c;那黑马吧……MOOC的北邮的C正在进行呜呜 #include <iostream> using namespace std; int…

【微服务】------核心组件架构选型

1.微服务简介 微服务架构&#xff08;Microservice Architecture&#xff09;是一种架构概念&#xff0c;旨在通过将功能分解到各个离散的服务中以实现对解决方案的解耦&#xff0c;从而降低系统的耦合性&#xff0c;并提供更加灵活的服务支持。 2.微服务技术选型 区域内容…

根据mysql的执行顺序来写select

过滤顺序指的是mysql的逻辑执行顺序&#xff0c;个人觉得我们可以按照执行顺序来写select查询语句。 目录 一、执行顺序二、小tips三、案例第一轮查询&#xff1a;统计每个num的出现次数第二轮查询&#xff1a;计算**最多次数**第三轮查询&#xff1a;找到所有出现次数为最多次…

Linux:Centos9:配置固定ip

centos9的网卡位置移动到了 /etc/NetworkManager/system-connections/ 下面 查看网卡 ifconfig 当前有两块网卡&#xff0c;我要去配置ens160的一个固定的ip&#xff0c;让其ip为192.168.6.20/24&#xff0c;网关为192.168.6.254.dns为&#xff1a;1.1.1.1 vim /etc/Netwo…

conda修改默认安装python版本为指定版本

1.查看conda中当前的python版本号: 打开Anaconda Powershell Prompt 输入python -V 回车会输出版本号 2.查看conda所支持的python版本,并选择指定版本安装 选择一个3.9.13版本的进行安装 安装命令: conda install python3.9.13 如果一直卡在这个画面,请使用管理员权限运行…

【Python】无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称解决方案

【Python】无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称解决方案 大家好 我是寸铁&#x1f44a; 总结了一篇【Python】无法将“pip”项识别为 cmdlet、函数、脚本文件或可运行程序的名称解决方案✨ 喜欢的小伙伴可以点点关注 &#x1f49d; 前言 今天寸铁…

06-kafka及异步通知文章上下架

kafka及异步通知文章上下架 1)自媒体文章上下架 需求分析 2)kafka概述 消息中间件对比 特性ActiveMQRabbitMQRocketMQKafka开发语言javaerlangjavascala单机吞吐量万级万级10万级100万级时效性msusmsms级以内可用性高&#xff08;主从&#xff09;高&#xff08;主从&#…

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

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

速成axios

Axios 大家好,又到了我们学习新东西的时候了,今天我们来了解一下现在市场上最主流的发送ajax请求的插件咯 了解一个插件的第一步肯定是去它的官网逛逛咯 从它的主页就可以看出axios是基于promise异步,适用于浏览器和node.js ajax的前世今生 对于我们来说忘什么都不能忘本呐…

Ps:预览调整 32 位 HDR 图像

由于 HDR 图像的动态范围超出了标准计算机显示器的显示范围。在 Photoshop 中打开 HDR 图像时&#xff0c;图像可能会非常暗或出现褪色现象。 Photoshop 提供了专门的预览调整功能&#xff0c;以使标准显示器显示的 HDR 图像的高光和阴影不会太暗或出现褪色现象。 预览调整设置…

【VASP学习】在Ubuntu系统安装vasp.5.4.4的全过程(包括VASP官方学习资料、安装过程中相关编辑器的配置、VASP的编译及VASP的测试)

在Ubuntu系统安装vasp.5.4.4的全过程 VASP的简介与相关学习资料安装前的准备工作及说明安装过程intel编译器的安装VASP的编译VASP的测试 参考来源 VASP的简介与相关学习资料 VASP(Vienna Ab initio Simulation Package)是基于第一性原理对原子尺度的材料进行模拟计算的软件。比…

学习 Git 基础知识 - 日常开发任务手册

欢迎来到我关于 Git 的综合指南&#xff0c;Git 是一种分布式版本控制系统&#xff0c;已经在软件开发中彻底改变了协作和代码管理方式。 无论你是经验丰富的开发者还是刚开始编程之旅的新手&#xff0c;理解 Git 对于正确掌控代码、高效管理项目和与他人合作至关重要。 在本…

LeetCode:1026. 节点与其祖先之间的最大差值(DFS Java)

目录 1026. 节点与其祖先之间的最大差值 题目描述&#xff1a; 实现代码与解析&#xff1a; DFS 原理思路&#xff1a; 1026. 节点与其祖先之间的最大差值 题目描述&#xff1a; 给定二叉树的根节点 root&#xff0c;找出存在于 不同 节点 A 和 B 之间的最大值 V&#xff…

2024 年最新使用 Wechaty 开源框架搭建部署微信机器人(微信群智能客服案例)

读取联系人信息 获取当前机器人账号全部联系人信息 bot.on(ready, async () > {console.log("机器人准备完毕&#xff01;&#xff01;&#xff01;")let contactList await bot.Contact.findAll()for (let index 0; index < contactList.length; index) {…

Redis各个方面入门详解

目录 一、Redis介绍 二、分布式缓存常见的技术选型方案 三、Redis 和 Memcached 的区别和共同点 四、缓存数据的处理流程 五、Redis作为缓存的好处 六、Redis 常见数据结构以及使用场景 七、Redis单线程模型 八、Redis 给缓存数据设置过期时间 九、Redis判断数据过期的…

云服务器ECS租用价格表报价——阿里云

阿里云服务器租用价格表2024年最新&#xff0c;云服务器ECS经济型e实例2核2G、3M固定带宽99元一年&#xff0c;轻量应用服务器2核2G3M带宽轻量服务器一年61元&#xff0c;ECS u1服务器2核4G5M固定带宽199元一年&#xff0c;2核4G4M带宽轻量服务器一年165元12个月&#xff0c;2核…

Django环境搭建及测试

Django环境搭建及测试 一、安装 Python二、安装 Django三、终端命令创建 Django 项目四、运行 Django 项目五、访问 Django 网站 一、安装 Python 首先确保你的电脑上安装了 Python。 Python官网点击直达 官网下载后双击即可安装 第一个相当于快速安装&#xff0c;第二个则是…

Linux之shell脚本编辑工具awk

华子目录 概念工作流程工作图流程&#xff08;按行处理&#xff09; awk程序执行方式1.通过命令行执行awk程序实例 2.awk命令调用脚本执行实例 3.直接使用awk脚本文件调用实例 awk命令的基本语法格式BEGIN模式与END模式实例awk的输出 记录和域&#xff08;记录表示数据行&#…

了解强化学习算法 PPO

&#x1f349; CSDN 叶庭云&#xff1a;https://yetingyun.blog.csdn.net/ 介绍&#xff1a; PPO 算法&#xff0c;即 Proximal Policy Optimization&#xff08;近端策略优化&#xff09;&#xff0c;是一种强化学习算法。它的主要目的是改进策略梯度方法&#xff0c;使得训练…

真--个人收款系统方案

此文主要说明方案&#xff0c;无代码部分 前言: 有个个人项目需要接入vip系统&#xff0c;我们发现微信、支付宝的官方API主要服务商户&#xff0c;而市面上的“个人收款系统”也往往不符合我们的需求。不过&#xff0c;每次支付时通知栏的信息给了我灵感。走投无路&#xff0…