多线程的同步与互斥

news2025/1/14 18:42:10

在这里插入图片描述

文章目录

    • 线程安全问题
    • 多线程互斥
      • 互斥量mutex
      • 互斥锁的使用
      • 理解锁
        • 加锁如何做到原子性
        • 对mutex做封装
      • 可重入与线程安全
      • 死锁
    • 线程同步
      • 条件变量
      • 条件变量函数接口
      • 理解条件变量
      • 条件变量的使用

线程安全问题

首先来看一段代码,该代码是一个多线程抢票的逻辑

#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>

using namespace std;

//票是共享资源,搞多个线程来抢票

int tickets=1000;

void *gettickets(void * args)
{
    string username=static_cast<const char*>(args);
    //在这里抢票,逻辑是先判断是否有票,有票就直接开抢
    while(true)
    {
        if(tickets>0)
        {
            usleep(1234);//让线程休眠一段时间来模拟抢票消耗的时间
            cout<<username<<"正在抢票,当前票数:"<<tickets<<endl;

            tickets--;

        }
        else
        {
            break;//没有余票,直接结束
        }
    }
}

int main()
{
    //创建多个线程来运行抢票逻辑
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,gettickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,gettickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,gettickets,(void*)"thread 3");
    pthread_create(&t4,nullptr,gettickets,(void*)"thread 4");

    //线程执行完毕还要回收
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

在这里插入图片描述

发现线程的抢票行为使tickets变为负数了,我们明明做了判断,票数大于零才进入抢票逻辑,居然还会出现负数;引发这个问题 的主要原因是数据的修改并非是原子性的,修改一个数据需要三条汇编指令:1.将数据从内存中加载到寄存器 2.在寄存器中让CPU进行算术或逻辑运算 3.将修改过的数据写回到内存中;如果在第三步之前,CPU将这个线程给切换了,那么就可能导致:明明这个数据已经被修改了一次,但还未来的及写回就被切换到下一个线程,此时新来的线程获取到的就是旧的未被修改的数据;

在这里插入图片描述

等到线程1再度被唤醒时,它需要完成之前未完成的动作,它会将未来的及写回的数据再次写回,此时内存中的票数又变成了999

在这里插入图片描述
.

从上述的情况可以得到一个结论:多线程在访问共享资源的时候是不安全的,这主要是因为多线程之间的并发执行的且访问资源的动作是非原子性的(单纯的++或者–都不是原子的)

为了解决这个问题,就提出了互斥锁;互斥锁可以让多个线程串行的访问资源(即有一个线程在访问资源时,其他线程只能等待),它也可以使得访问资源的动作变成原子性的;


在介绍锁之前补充一些概念:

原子性:要么不做,要么做完,它不会被调度机制打断,简单的理解就是:它的汇编指令只有一条

临界资源:被共享的资源都可以叫做临界资源

临界区:访问临界资源的代码段就是临界区

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

多线程互斥

互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题,这在上述的线程安全问题上已经体现了

要解决多线程并发访问临界资源带来的问题,需要做到三点:

  1. 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。

  2. 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。

  3. 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区

其实就是加一把互斥锁,这个锁就是mutex,一个线程在持有锁的期间,其他的线程只能挂起等待;

下面介绍其常用的接口(因为接口属于pthread库,所以makefile中仍然需要包含该库):

#include <pthread.h>//头文件

// 初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 作为全局变量时的初始化方式,此时的锁不需要使用init初始化也不必用destory销毁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//加锁,如果此时没有锁则阻塞等待,直到获取到锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

//如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

//上述所有的接口都是成功返回0,失败返回错误码

互斥锁的使用

将锁设置为全局变量,在临界区的最开始加锁,出临界区之间要记得解锁(否则其他线程就只能一直处于阻塞等待锁的过程)

#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>

using namespace std;

//票是共享资源,搞多个线程来抢票
pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;

int tickets=1000;

void *gettickets(void * args)
{
    string username=static_cast<const char*>(args);
    //在这里抢票,逻辑是先判断是否有票,有票就直接开抢
    while(true)
    {
        pthread_mutex_lock(&lock);
        if(tickets>0)
        {
            usleep(1234);//让线程休眠一段时间来模拟抢票消耗的时间
            cout<<username<<"正在抢票,当前票数:"<<tickets<<endl;

            tickets--;
            //出了临界区需要解锁,否则其他线程无法使用
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;//没有余票,直接结束
        }
    }
}

int main()
{
    //创建多个线程来运行抢票逻辑
    pthread_t t1,t2,t3,t4;
    pthread_create(&t1,nullptr,gettickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,gettickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,gettickets,(void*)"thread 3");
    pthread_create(&t4,nullptr,gettickets,(void*)"thread 4");

    //线程执行完毕还要回收
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

在这里插入图片描述

此时就不会出现抢票抢到负数的情况了

当然也可以使用局部锁,为了让多个线程看到同一把锁,我们可以创建一个结构体,将这个结构体传给线程

#include<iostream>
#include<string>
#include<vector>
#include<unistd.h>
#include<pthread.h>

using namespace std;

//当成结构体来用,里面存放的是线程名称和锁
class BuyTicket
{
public:
    BuyTicket(const string &threadname,pthread_mutex_t *mutex_p)//一般来说传参输入型是:const&,输出型是*,输入输出型是&
    :threadname_(threadname)
    ,mutex_p_(mutex_p)
    {}

public:
    string threadname_;
    pthread_mutex_t*mutex_p_;

};


int tickets=1000;

void *gettickets(void * args)
{
    BuyTicket*td=static_cast<BuyTicket*>(args);
    while(true)
    {
        pthread_mutex_lock(td->mutex_p_);
        if(tickets>0)
        {
            usleep(1234);//让线程休眠一段时间来模拟抢票消耗的时间
            cout<<td->threadname_<<"正在抢票,当前票数:"<<tickets<<endl;

            tickets--;
            //出了临界区需要解锁,否则其他线程无法使用
            pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);
            break;//没有余票,直接结束
        }
    }
}

int main()
{

    //创建局部锁并初始化
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);

    //创建数组,表示线程ID
    vector<pthread_t> tids(4);

    //创建四个线程,并将结构体传给线程,所以要先初始化结构体
    for(int i=0;i<4;i++)
    {
        char buffer[64];
        snprintf(buffer,64,"thread %d",i+1);
        BuyTicket*td=new BuyTicket(buffer,&lock);
        pthread_create(&tids[i],nullptr,gettickets,td);//td是传给gettickets的实参
    }
    
    //回收线程
    for(const auto &tid:tids)
    {
        pthread_join(tid,nullptr);
    }

    //用完锁以后要将锁销毁
    pthread_mutex_destroy(&lock);
    return 0;
}

在这里插入图片描述

这种写法相比上一种要更麻烦一些,在这里我想对pthread_create函数再做一些讲解

pthread_create(pthread_t *thread, const pthread_attr_t *attr,
               void *(*start_routine) (void *), void *arg)1)thread:事先创建好的pthread_t类型的参数。成功时thread指向的内存单元被设置为新创建线程的线程ID。

(2)attr:用于定制各种不同的线程属性,通常直接设为NULL。

(3)start_routine:新创建线程从此函数开始运行。无参数是arg设为NULL即可。

(4)arg:是传给start_routine函数的实参,如果参数大于一个就需要用结构体来传参

首先加锁给我们的直观现象就是程序的运行速度变慢了,这是因为多线程从并发运行变成了串行,且还要加解锁;此外我们发现以上的两种写法,程序在运行时都只有一个线程在抢票,这是因为锁只规定需要互斥访问,谁持有锁谁就占有该资源;解决这个问题的办法也很简单,只需要让该线程陷入休眠即可,在现实中我们抢完票还需要付款,付款的时候线程已经退出临界区了,这里用休眠来代替:

在这里插入图片描述


理解锁

为了保证让多个线程串行的访问临界资源,所以必须多个线程之间只能有一把锁,并且这把锁要对所有线程都可见;也就是说锁也是一种共享资源,那么谁又来保护锁呢?

pthread_mutex_lock,pthread_mutex_unlock加锁和解锁的过程必须是安全的,且加锁的过程是原子性的。谁持有锁,谁就能进入临界区,如果某个线程申请锁,但是此时并没有锁,该线程就会阻塞式等待的加锁,所以说使用pthread_mutex_lock加锁是原子性的

在这里插入图片描述

在接口介绍时有一个trylock接口,该接口就是非阻塞式申请锁

线程申请到锁,就可以继续往下执行;此时其他没有申请到锁的线程就要阻塞等待,直到它们申请到锁;

一个线程在加锁期间,如果时间片到了也是可以被CPU切换的,绝对可以!但持有锁的线程在被切换的时候是抱着锁走的,其他线程仍旧无法申请到锁,所以对于其他线程而言只有两种状态:1.加锁前 2.释放锁后;站在其他线程的角度来看,持有锁的过程是原子的

我们在使用锁的时候,要尽量保证临界区的粒度要小(代码量小);加锁是程序员行为,如果要对公共资源加锁那么每个线程都要加锁

加锁如何做到原子性

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 下面来看一下lock和unlock的伪代码:

在这里插入图片描述

%al代表了一个寄存器,xchgb就是exchange指令,用于数据交换;mov可以理解为赋值

加锁过程:

在这里插入图片描述
.

寄存器中的数据属于线程的上下文,在线程切换时是要呗带走的,所以线程被切换的时候是带着线程走的

解锁的过程就是将mutex中的数据重新置为1,所以一个线程加锁,另一个线程是可以将其解锁的,只是我们的代码不会这样写;

对mutex做封装

为了使用方便,可以对mutex做封装

//Mutex.hpp

#pragma once
#include<iostream>
#include<pthread.h>

//对锁做简单的封装,搞两个类,一个类是Mutex,另一个是加锁的类
class Mutex
{
public:
    Mutex(pthread_mutex_t*mutex_p=nullptr):mutex_p_(mutex_p)
    {}

    //加锁解锁
    void lock()
    {
        if(mutex_p_)pthread_mutex_lock(mutex_p_);
    }

    void unlock()
    {
        if(mutex_p_)pthread_mutex_unlock(mutex_p_);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t*mutex_p_;
};

//构造一个锁类,该类的构造是加锁,析构就是解锁
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock();
    }

    ~LockGuard()
    {
        mutex_.unlock();
    }

private:
    Mutex mutex_;
};

此时抢票的代码可以修改成以下的模样,只需要将锁作为参数传给类用以构造即可,不必再手动调用接口,且解锁过程就不需要我们显示的去调用;

可重入与线程安全

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

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

如果函数可重入,那么线程一定安全;线程安全,函数不一定可重入

常见的线程安全的情况

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  2. 类或者接口对于线程来说都是原子操作

  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可重入的情况

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构

死锁

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

已经持有锁的线程再去申请锁也是一种死锁,死锁产生有四个必要条件:

1.互斥:一个共享资源每次被一个执行流使用

2.请求与保持:一个执行流因请求资源而阻塞,对已有资源保持不放

3.不剥夺:一个执行流获得的资源在未使用完之前,不能强行剥夺

4.环路等待条件:执行流间形成环路问题,循环等待资源

📕为什么会有死锁?

首先肯定是因为我们使用了锁->使用锁是为了保护线程安全->因为多线程在访问共享资源时有数据不一致问题->多线程的大部分资源是共享的->在解决问题的时候又带来了新的问题:死锁

如何解决死锁?

  1. 破坏死锁形成的四个的必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

检测死锁的方法:1.银行家算法 2.死锁检测算法


线程同步

假设学校有一个条件极好的VIP自习室,这个自习室一次只能一个人使用并且规定是来的最早的人使用;我为了体验这个自习室,凌晨三点的时候我就奔向了自习室,当我在里面呆到七点多的时候我想去上个厕所,为了防止在我上厕所期间别人占用该自习室,我将自习室的门反锁并且带走了钥匙;又在自习室里待了几个小时候,我觉得待不住了,我准备离开,我刚将钥匙挂回去,我突然觉得好不容易占到这个自习室不能就这样离去,于是我又拿到钥匙(因为我离钥匙最近),开门以后待了没一分钟我又出来了,刚把钥匙挂好,我又觉得不能就这样算了;于是我一直重复着开门关门拿钥匙放钥匙的动作;虽然没有违反规定,但导致其他同学一直无法使用该自习室,学校的目的无法达到

在上述的例子中,因为我始终离钥匙最近,竞争力最强,所以始终是我获取到资源,且我重复获取资源的过程并没有违反任何规定;但这样并没有使学校达到提升大家学习的目的,也就是说我一直占着资源做着无意义的动作,虽然不违反规定,但是造成了其他线程的饥饿问题;为了解决这个问题就提出了线程同步:

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

饥饿问题:某个线程一直占有资源,导致其他线程无法获得而处于饥饿状态

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解


条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量,当条件满足时,线程会被唤醒。

条件变量通常配合互斥锁一起使用

条件变量函数接口

#include <pthread.h>//与互斥锁有些类似

//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//cond是要初始化的条件

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);

//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond, //在这个条件上等待
       pthread_mutex_t *restrict mutex, //条件要改变必须是多线程,条件改变涉及到共享资源,所以必须要有锁的保护
       const struct timespec *restrict abstime);

//等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);

// 唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t *cond);

// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);

理解条件变量

校招时,公司都会在大学城附近的酒店包一层楼用于面试,来了很多面试的人,但hr是少量的,为了规范来面试的人,于是公司的管理层就规定,没有面试的人需要在某个地方排队等待

排队等待的地方就是条件变量,来面试的人就是线程;当条件不满足的时候,线程必须要到定义好的条件变量上去等,条件变量包含一个等待队列,当线程不满足条件时,就链接在这个等待队列上进行等待,当条件满足了,再去等待队列上唤醒

在这里插入图片描述

条件变量的使用

一次唤醒一个线程:

int tickets=1000;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void *gettickets(void *args)
{
    string username = static_cast<const char *>(args);
    // 在这里抢票,逻辑是先判断是否有票,有票就直接开抢

    // BuyTicket*td=static_cast<BuyTicket*>(args);
    while (true)
    {
        pthread_mutex_lock(&lock);
        pthread_cond_wait(&cond,&lock);
        {
            if (tickets > 0)
            {
                usleep(1234); // 让线程休眠一段时间来模拟抢票消耗的时间
                cout << username << "正在抢票,当前票数:" << tickets << endl;
           		tickets--;  
                pthread_mutex_unlock(&lock);
            }
            else
            {
                pthread_mutex_unlock(&lock);
                break; // 没有余票,直接结束
            }
        }
        usleep(1234); // 用休眠来代替抢到票后的其他行为
    }
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, gettickets, (void *)"thread 1");
    pthread_create(&t2, nullptr, gettickets, (void *)"thread 2");
    pthread_create(&t3, nullptr, gettickets, (void *)"thread 3");
    pthread_create(&t4, nullptr, gettickets, (void *)"thread 4");

    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond); //一次唤醒一个线程
        cout<<"main thread wake up one...."<<endl;

    }
    // 线程执行完毕还要回收
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    return 0;
}

在这里插入图片描述


此外也可一次性唤醒多个线程:

while(true)
    {
        sleep(1);
         pthread_cond_broadcast(&cond);//只要使用这个接口即可,别的代码都不必修改
        cout<<"main thread wake up one...."<<endl;

    }

在这里插入图片描述

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

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

相关文章

go的gin和gorm框架实现切换身份的接口

使用go的gin和gorm框架实现切换身份的接口&#xff0c;接收前端发送的JSON对象&#xff0c;查询数据库并更新&#xff0c;返回前端信息 接收前端发来的JSON对象&#xff0c;包含由openid和登陆状态组成的一个string和要切换的身份码int型 后端接收后判断要切换的身份是否低于该…

排列数字 (dfs)

希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注~ 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不能错过的模板大全…

Dubbo2-概述

Dubbo 阿里公司开源的一个高性能&#xff0c;轻量级的javaRPC&#xff08;远程服务调用方案&#xff09;框架&#xff0c;提供高性能远程调用方案以及SOA服务治理方案 Dubbo架构 节点角色说明&#xff1a; Provider:服务提供方 Container:服务运行容器 Consumer:调用远程服务…

中科亿海微RAM使用

引言 FPGA&#xff08;Field Programmable Gate Array&#xff0c;现场可编程门阵列&#xff09;是一种可编程逻辑设备&#xff0c;能够根据特定应用的需求进行配置和重新编程。在FPGA中&#xff0c;RAM&#xff08;Random Access Memory&#xff0c;随机存取存储器&#xff09…

HTML详解连载(3)

HTML详解连载&#xff08;3&#xff09; 专栏链接 [link](http://t.csdn.cn/xF0H3)下面进行专栏介绍 开始喽表单作用使用场景 input标签基本使用示例type属性值以及说明 input标签占位文本示例注意 单选框 radio代码示例 多选框-checkbox注意代码示例 文本域作用标签&#xff1…

《3D 数学基础》12 几何图元

目录 1 表达图元的方法 1.1 隐式表示法 1.2 参数表示 1.3 直接表示 2. 直线和射线 2.1 射线的不同表示法 2.1.1 两点表示 2.1.2 参数表示 2.1.3 相互转换 2.2 直线的不同表示法 2.2.1 隐式表示法 2.2.2 斜截式 2.2.3 相互转换 3. 球 3.1 隐式表示 1 表达图元的方…

运维监控学习笔记4

系统监控&#xff1a; CPU&#xff1a; 内存&#xff1a; IO INPUT/OUTPUT&#xff08;网络、磁盘&#xff09; CPU三个重要概念&#xff1a; 上下文切换&#xff1a;CPU调度器实施的进程的切换过程&#xff0c;称为上下文切换。CPU寄存器的作用。 上下文切换越频繁&#…

keil下载程序具体过程4:flash下载算法

引言 本篇文章将介绍flash算法文件&#xff0c;阐述从jlink如何下载镜像文件写入到内部的falsh。 一、XIP 在谈flash下载算法文件时&#xff0c;先说明XIP是什么。 芯片的启动方式有很多种&#xff1a;可以从RAM中启动、内部的flash、外部的flash等等&#xff08;还有从sd卡、…

CSDN博客批量查询质量分https://yma16.inscode.cc/请求超时问题(设置postman超时时间)(接口提供者设置了nginx超时时间)

文章目录 查询链接问题请求超时原因解决谷歌浏览器超时问题办法&#xff08;失败了&#xff09;谷歌浏览器不支持设置请求超时时间&#xff08;谷歌浏览器到底有没限制请求超时&#xff1f;貌似没有限制&#xff1f;&#xff09;看能否脱离浏览器请求&#xff0c;我们查看关键代…

基于C++实现了最小反馈弧集问题的三种近似算法(GreedyFAS、SortFAS、PageRankFAS)

该项目是一个基于链式前向星存图、boost&#xff08;boost::hash、asio线程池&#xff09;以及emhash7/8的非官方实现&#xff0c;实现了最小反馈弧集问题的三种近似算法。该问题是在有向图中找到最小的反馈弧集&#xff0c;其中反馈弧集是指一组弧&#xff0c;使得从这些反馈弧…

环境与分支的详细介绍及其关联(开发、测试、预发布、生产)

文章目录 前言一、开发环境&#xff08;dev&#xff09;二、测试环境&#xff08;test&#xff09;三、预发布环境&#xff08;pre&#xff09;四、生产环境&#xff08;pro&#xff09;五、环境与分支的关系总结 前言 在现代软件开发中&#xff0c;前端项目的开发和部署往往需…

【wiki】电竞助手掉落提醒 EsportsHelper「Webhook」「钉钉」「饭碗警告」「企业微信」「Discord」

介绍 本项目链接 Github电竞助手链接 github上项目电竞助手(EsportsHelper)的掉落提醒配置教程,当有掉宝的时候会发送你信息提示. 至于这个脚本是怎么使用的简单说一下,就是通过自动观看英雄联盟直播 从而获取奖励(仅限直营服),有兴趣的可以去github上看readme,非常详细,支持…

广联达 Linkworks办公OA SQL注入+后台文件上传漏洞复现(HW0day)

0x01 产品简介 广联达Linkworks办公OA&#xff08;Office Automation&#xff09;是一款综合办公自动化解决方案&#xff0c;旨在提高组织内部的工作效率和协作能力。它提供了一系列功能和工具&#xff0c;帮助企业管理和处理日常办公任务、流程和文档。 0x02 漏洞概述 由于 广…

hackNos靶机

靶机训练1 - hackNos: Os-hackNos 靶机平台 Vulnhub 是一个提供各种漏洞环境的靶场平台&#xff0c;供安全爱好者学习使用&#xff0c;大部分环境是做好的虚拟机镜像文件&#xff0c;镜像预先设计了多种漏洞&#xff0c;需要使用VMware或者VirtualBox运行。每个镜像会有破解的目…

【CSS学习笔记】

学习内容 1.css是什么 2.CSS怎么用&#xff08;快速入门&#xff09; 3.CSS选择器&#xff08;重点 难点&#xff09; 4.美化页面&#xff08;文字、阴影、超链接、列表、渐变…&#xff09; 5.盒子模型 6.浮动 7.定位 8.网页动画&#xff08;特效&#xff09; 1.什么是CSS C…

Reinforcement Learning with Code 【Chapter 10. Actor Critic】

Reinforcement Learning with Code 【Chapter 10. Actor Critic】 This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of …

Blazor简单教程(2):布局

文章目录 前言布局自定义布局默认布局 前言 我们现在主流的页面都是单页面Layout布局&#xff0c;即一个页面有侧边栏&#xff0c;抬头&#xff0c;下边栏&#xff0c;中间主题。 BootstrapBlazor UI&#xff0c; Blazor Server 模式配置 布局 自定义布局 注入LayoutCompon…

微服务06-分布式事务解决方案Seata

1、Seata 概述 Seata事务管理中有三个重要的角色: TC (Transaction Coordinator) - **事务协调者:**维护全局和分支事务的状态,协调全局事务提交或回滚。 TM (Transaction Manager) - **事务管理器:**定义全局事务的范围、开始全局事务、提交或回滚全局事务。 RM (Resourc…

软考笔记 信息管理师 高级

文章目录 介绍考试内容与时间教材 预习课程一些例子课本结构考试内容 1 信息与信息化1.1 信息与信息化1.1.1 信息1.1.2 信息系统1.1.3 信息化 1.2 现代化基础设施1.2.1 新型基础建设1.2.2 工业互联网1.2.3 车联网&#xff1a; 1.3 现代化创新发展1.3.1 农业农村现代化1.3.2 两化…

常见的路由协议之RIP协议与OSPF协议

目录 RIP OSPF 洪泛和广播的区别 路由协议是用于在网络中确定最佳路径的一组规则。它们主要用于在路由器之间交换路由信息&#xff0c;以便找到从源到目标的最佳路径。 常见的路由协议&#xff1a; RIP (Routing Information Protocol)&#xff1a;RIP 是一种基于距离向量算…