Linux——多线程互斥

news2025/1/20 19:06:02

多线程互斥

  • 抢票问题
  • 互斥锁
    • 锁的接口
  • 理解锁
    • 锁的背景概念
    • 如何看待锁:
    • 加锁和解锁的原理
    • 锁的封装
  • 可重入与线程安全
  • 死锁
    • 死锁的概念与条件

抢票问题

这里用上一篇:
https://blog.csdn.net/qq_63580639/article/details/131054847?spm=1001.2014.3001.5501
的封装函数。
这里还需要用一个函数:
在这里插入图片描述
这里是以微妙做单位进行休眠的。
假设有1000张火车票,一共四个接口在抢,最后我们要看到什么现象呢?
因为多个线程进行交叉执行。
多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换。
线程一般在什么时候发生切换?当时间片到了,来了更高优先级的线程,线程等待的时候。
那么线程是什么时候检测上面的问题?是从内核态切换到用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换。

#include "Thread.hpp"

int tickets = 1000;//票数

void* thread_run(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << name << "用户正在抢票:"<< tickets-- <<endl;
        }
        else
            break;
    }
}
int main()
{
    unique_ptr<Thread> thread1(new Thread(thread_run, (void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run, (void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run, (void*)"user3",3));
    unique_ptr<Thread> thread4(new Thread(thread_run, (void*)"user4",4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();

    return 0;
}

在这里插入图片描述
但是结果竟然出现了0,-1,-2,为什么会发生这种现象呢?
首先:判断的逻辑有两步
1.读取内存数据到CPU的寄存器中。
2.进行判断

因为CPU只有一个,每次线程只能有一个进行判断。

线程1运行的时候,CPU将tickets的数据放进内存其中与线程1的数据进行对比,但是对比结束之后,突然时间片到了,线程切换,线程2也进行了如上步骤,刚对比完又切换了。
如果极端场景,四个进程都在这个时候对比,tickets的数据一直都是1,那么这个时候线程1被唤醒,线程1带着他的上下文回到CPU,CPU处理这段代码,tickets的数据进行- - ,处理完又去处理线程2,线程3,线程4。
这也就导致了出现0,-1,-2的结果。

还有另一种情况。
对一个全局变量进行多线程更改,这个操作也不是安全的。
对于++,- -这两种操作,在C,C++上看起来只有一条语句,其实汇编用了三条语句。
1.从内存中读取数据到CPU寄存器中。
2.在寄存器中让CPU进行对应的逻辑运算。
3.写回新的结果到内存中变量的位置。
在这里插入图片描述
假设线程1先将票数减少了333张。
然后CPU本来要将666传给内存中的票数时突然进行了线程切换,到了线程2一看票数还是999。
于是线程2开始继续抢票:
在这里插入图片描述
线程2将票数减少到了222,这个时候,又换回了线程1.
在这里插入图片描述
这个时候首先恢复的是上下文,然后更新内存中的数据,一下子变成了666,之前变成222等于白做事情了。

总结:我们定义的全局数据在没有保护的时候往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生数据不一致。
那么如何解决呢?

互斥锁

锁的接口

之前说过原子性是要么做,要么不做,这里再结合上面抢票问题说一下。
像上面进行++操作,需要三条汇编语言,CPU在执行的时候是一定会将一条汇编语言执行完毕(失败与否不关心),也就是说这会导致有中间状态,是可以被打断的,这就叫做非原子性。
那么原子性其实就是一个对资源进行的操作,如果只用一条汇编能完成,这个就是原子性,反之就不是原子性。

这个时候,对于以上提出问题的解决方案叫做加锁。
锁也有对应的函数:
在这里插入图片描述
这把锁的类型是:
在这里插入图片描述
第一个函数是释放锁,第二个函数是初始化锁
在这里插入图片描述
这里是对全局定义的锁初始化方式。
在这里插入图片描述
这里第一个函数是对对应的锁进行加锁。
第三个函数是解锁。

#include "Thread.hpp"

int tickets = 1000;//票数

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;//全局锁

void* thread_run(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&lock);//这里进行加锁
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << name << "用户正在抢票:"<< tickets-- <<endl;
            pthread_mutex_unlock(&lock);//解锁
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
    }
}
int main()
{

    unique_ptr<Thread> thread1(new Thread(thread_run, (void*)"user1",1));
    unique_ptr<Thread> thread2(new Thread(thread_run, (void*)"user2",2));
    unique_ptr<Thread> thread3(new Thread(thread_run, (void*)"user3",3));
    unique_ptr<Thread> thread4(new Thread(thread_run, (void*)"user4",4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();

    return 0;
}

在这里插入图片描述
在运行的过程中,这个速度也变慢了,因为现在的线程是串行执行,所以也没有发生之前奇怪的打印结果。
那么为什么一直都是一个线程抢到票了呢?这是因为锁虽然规定了串行执行,但是并没有去管理线程的竞争,这里第四个线程竞争力最强,所以每次都是线程4抢到票。

这里我们在用一下局部锁,并且解决一下刚才的问题。

#include "Thread.hpp"

int tickets = 1000;//票数

class ThreadData
{
public:
    ThreadData(const string& threadname, pthread_mutex_t *mutex_p):_threadname(threadname),_mutex_p(mutex_p)
    {}
    ~ThreadData()
    {}
public:
    string _threadname;
    pthread_mutex_t *_mutex_p;
};
void* thread_run(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    while(true)
    {
        pthread_mutex_lock(p->_mutex_p);//加锁
        if(tickets > 0)
        {
            usleep(1234);//1秒=1000毫秒=1000000微秒
            cout << p->_threadname << "用户正在抢票:"<< tickets-- <<endl;
            pthread_mutex_unlock(p->_mutex_p);
        }
        else
        {
            pthread_mutex_unlock(p->_mutex_p);
            break;
        }
        usleep(1234);//模拟抢完票形成一个订单,这里也就等于阻止了竞争力强的线程,让竞争力强的到后面排队去
    }
}

int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);//初始化锁,第二个参数设为nullptr就可以
    vector<pthread_t> arr(4);
    for(int i = 0;i < 4; i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread %d",i+1);
        ThreadData* p = new ThreadData(buffer, &lock);
        pthread_create(&arr[i], nullptr, thread_run, p);
    }
    for(const auto& e:arr)
    {
        pthread_join(e,nullptr);
    }
    pthread_mutex_destroy(&lock);//解锁
    return 0;
}

在这里插入图片描述

理解锁

锁的背景概念

临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

如何看待锁:

1.我们在使用锁的时候,锁能被每个线程都看到,所以锁本身就是共享资源。锁是保护资源的,那么锁的安全谁来保护呢?
2.pthread_mutex_lock枷锁的过程中必须是安全的。(其实就是原子的)
3.如果申请成功,就继续向后执行,如果失败执行流就阻塞。
在这里插入图片描述
注意,我这里申请了两次加锁。
在这里插入图片描述
这里就阻塞了。
4.谁持有锁,谁就进入临界区。
假如线程1持有锁,进入临界资源,其他线程在阻塞,那么在这个过程中线程1是可以被切换的。
这也说明,线程1是和锁一起被切走了。
所以对于其他线程而言,线程1有意义的状态只有两个。

申请锁前
释放锁后

站在其他线程角度,看待当前线程持有锁的过程就是原子的。
未来我们在使用锁的时候,一定要尽量保证临界区的粒度(锁中间保护的代码)非常小。
并且,加锁是程序员的行为,针对某一处公共资源,对于一个线程加锁,其他线程也要想办法加锁。

加锁和解锁的原理

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
现在我们把lock和unlock的伪代码改一下:
在这里插入图片描述
假设现在是线程A运行,线程A进行了申请加锁,内存中的int当中的1就是锁。
首先让0放进CPU中的寄存器%al当中,然后将内存中的1%al中的0交换。
在这里插入图片描述
这里交换是一条汇编完成的。
这个时候,如果突然时间片到了,线程B换了上来,线程A就要带着自己的上下文走。
在这里插入图片描述
然后线程B从头开始,先将0放入%al,然后交换:
在这里插入图片描述
这里继续向下执行语句,发现寄存器%al中的内容并不大于0,走第二条语句,线程B就被挂起等待了。
然后线程A又切换回来继续向下执行:
在这里插入图片描述
这就是为什么当前线程申请锁之后其他线程无法申请锁!
解锁的过程就是将%al的1移动到内存中:
在这里插入图片描述

锁的封装

因为C语言很多接口是不兼容C++的,所以我们要想办法设计让锁的接口兼容C++。

#pragma once
#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <memory>
using namespace std;

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr):_lock_p(lock_p)
    {}
    void lock()
    {
        if(_lock_p) pthread_mutex_lock(_lock_p);
    }
    void unlock()
    {
        if(_lock_p) pthread_mutex_unlock(_lock_p);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *_lock_p;
};

class LockGuard//这里像智能指针一样,自动解锁
{
public:
    LockGuard(pthread_mutex_t *mutex):_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }
private:
    Mutex _mutex;
};
#pragma once
#include <iostream>
#include <pthread.h>
#include <cassert>
#include <string>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <cstdlib>
#include <memory>
using namespace std;

class Thread;//声明
class Context//上下文,相当于一个大号的结构体
{
public:
    Thread *this_;
    void* args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};
class Thread
{
    typedef function<void* (void*)> func_t;
public:
    //这里需要加一个静态,因为不加静态就是类成员函数,还有一个隐藏的this指针,也就说明这等于前面有一个缺省参数
    //所以在类内创建线程,想让对应的线程执行方法需要在方法前面加一个static
    static void* start_routine(void* args)
    {
        //但是静态方法不能调用成员方法或者成员变量,这里可以设置一个上下文
        Context* ctx = static_cast<Context*>(args);
        void* ret = ctx->this_->run(ctx->args_);//这里让自身去调用这个方法
        delete ctx;
        return ret;
    }
    void* run(void* args)
    {
        return _func(args);//调用该函数
    }
    Thread(func_t func,void* args,int num):_func(func),_args(args)
    {
        char buffer[1024];
        snprintf(buffer, sizeof(buffer), "thread_%d", num);
        _name = buffer;

        Context* ctx = new Context();
        ctx->this_ = this;
        ctx->args_ = _args;//这里是将自身的部分数据传给ctx
        int n = pthread_create(&_tid, nullptr, start_routine, ctx);//这里要通过调用函数来转化,直接传func是不行的,因为类型是C++的类,不是C语言的类
        assert(n==0);
        (void)n;
    }
    void join()
    {
        int n = pthread_join(_tid,nullptr);
        assert(n==0);
        (void)n;
    }
    ~Thread()
    {}
private:
    string _name;//线程名字
    pthread_t _tid;//线程id
    func_t _func;//线程调用的函数
    void* _args;//传给函数的参数
};

#include "Thread.hpp"
#include "MUtex.hpp"
int tickets = 1000;//票数

class ThreadData
{
public:
    ThreadData(const string& threadname, pthread_mutex_t *mutex_p):_threadname(threadname),_mutex_p(mutex_p)
    {}
    ~ThreadData()
    {}
public:
    string _threadname;
    pthread_mutex_t *_mutex_p;
};
void* thread_run(void* args)
{
    ThreadData* p = static_cast<ThreadData*>(args);
    LockGuard lockGuard(p->_mutex_p);//这里会自动加锁解锁
    while(true)
    {
    	{//这里的域是为了避免对下面的usleep进行加锁
	        if(tickets > 0)
	        {
	            usleep(1234);//1秒=1000毫秒=1000000微秒
	            cout << p->_threadname << "用户正在抢票:"<< tickets-- <<endl;
	        }
	        else
	        {
	            break;
	        }
        }
        usleep(1234);//模拟抢完票形成一个订单,这里也就等于阻止了竞争力强的线程,让竞争力强的到后面排队去
    }
}

int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);//初始化锁,第二个参数设为nullptr就可以
    vector<pthread_t> arr(4);
    for(int i = 0;i < 4; i++)
    {
        char buffer[64];
        snprintf(buffer,sizeof(buffer),"thread %d",i+1);
        ThreadData* p = new ThreadData(buffer, &lock);
        pthread_create(&arr[i], nullptr, thread_run, p);
    }
    for(const auto& e:arr)
    {
        pthread_join(e,nullptr);
    }
    pthread_mutex_destroy(&lock);//解锁
    return 0;
}

在这里插入图片描述
这种风格叫做RAII加锁。

可重入与线程安全

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全的情况

不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数

常见的线程安全的情况

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

常见不可重入的情况

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

可重入与线程安全联系

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

可重入与线程安全区别

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

死锁

死锁的概念与条件

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

死锁四个必要条件

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

上面代码有个例子,申请了两次锁,就等于自己本来有锁,又申请一次之后等于将自己挂起,这样谁也申请不到锁了。

避免死锁

破坏死锁的四个必要条件。
加锁顺序一致。
避免锁未释放的场景。(也就是用完锁一定要释放)
资源一次性分配。(不要到处给锁分配资源,不然看起来很乱,就容易造成死锁)

这里要注意一下,当前线程的锁可以被别的线程释放,上面的汇编语言释放锁的逻辑就说明了这一点。

避免死锁算法

死锁检测算法
银行家算法

注意:平时尽量不要用锁。

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

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

相关文章

LVGL lv_color_t 像素定义详解

更多源码分析请访问&#xff1a;LVGL 源码分析大全 目录 1、概述2、颜色格式详解2.1、LV_IMG_CF_RAW_X2.2、LV_IMG_CF_TRUE_COLOR_X2.3、LV_IMG_CF_INDEXED_XBIT2.4、LV_IMG_CF_ALPHA_XBIT2.5、LV_IMG_CF_RGBX 3、送显函数&#xff08;flush_cb&#xff09;中的 lv_color_t附录…

【P54】JMeter 生成概要结果(Generate Summary Results)

文章目录 一、生成概要结果&#xff08;Generate Summary Results&#xff09;参数说明二、准备工作三、测试计划设计 一、生成概要结果&#xff08;Generate Summary Results&#xff09;参数说明 可以将测试结果在客户端模式下输出&#xff0c;同时能美化压测输出的结果 使…

申请Let‘s Encrypt免费SSL证书、自动化续签证书

一、环境 安装证书的环境为Centos Nginx&#xff0c;如果没有安装Nginx则需要先安装。 二、申请流程 1、开放80和443端口 firewall-cmd --permanent --add-port80/tcp firewall-cmd --permanent --add-port443/tcp firewall-cmd --reload2、安装 certbot 使用certbot工具能…

Doris动态表使用快速入门实战

1. 动态表构功能概述 半结构化数据&#xff0c;是介于结构化和非结构化之间的数据。和普通纯文本相比&#xff0c;半结构化数据具有一定的结构性。和结构化数据相比&#xff0c;其结构变化复杂&#xff0c;我们又不能方便的使用结构化的方式去描述它。 半结构的数据中通常即包…

Java基础知识总结归纳

0. 入门常识 0.1 Java 特点 0.2 Java 和 C 0.3 JRE 和 JDK 0.4 Java 程序编译过程 1. 数据类型 1.1 基本数据类型 1.2 引用类型 1.3 封装类 1.4 缓存池 2. 字符串 String 2.1 定义 2.2 不可变性的优点 2.3 String vs StringBuffer vs StringBuffer 2.4 字符串常量池…

【网络协议详解】——BGP协议(学习笔记)

目录 &#x1f552; 1. 概述&#x1f552; 2. BGP 发言人&#x1f552; 3. 工作原理&#x1f552; 4. 报文格式&#x1f558; 4.1 报文首部&#x1f558; 4.2 打开报文&#x1f558; 4.3 更新报文&#x1f558; 4.4 保活报文&#x1f558; 4.5 通知报文 &#x1f552; 5. BGP 的…

接口实战一“篇”入魂!你真正地了解接口测试么?

目录 前言&#xff1a; 需求描述&#xff1a; 计划和目标&#xff1a; 需求分析&#xff1a; 1.功能点划分 2.接口测试用例设计 3.测试用例评审 4.测试准备 5.测试执行 前言&#xff1a; 接口测试是软件测试的一种类型&#xff0c;它主要关注软件系统中的接口或 API …

STM32CubeIDE 入门教程

1.安装教程 1.1 去ST 官网下载软件安装包&#xff1a;https://www.st.com/content/st_com/en.html 1.2软件安装&#xff1a; 2.创建工程 2.1 双击打开软件&#xff0c;选择工作区路径&#xff0c;不要出现中文名称 2.2 选择直接创建STM32 工程&#xff0c;我们这里以STM32F103…

详细讲解!Selenium真正绕过webdriver检测

目录 前言&#xff1a; 一、什么是真正绕过浏览器检测&#xff1f; 2、 普通的启动webdriver 3、Js注入真正绕过webdriver的检测属性 js注入的文件[stealth.min.js] 前言&#xff1a; Selenium是一个流行的开源测试工具&#xff0c;用于网络应用程序测试。它使测试人员能够…

运用短信案例举例!Appium自动化测试该如何进行PO模式

目录 前言&#xff1a; 案例一&#xff1a;自动发送短信 前言&#xff1a; Appium是一种广泛用于移动应用程序自动化测试的工具&#xff0c;可以支持iOS和Android系统。在测试App时&#xff0c;使用Page Object&#xff08;PO&#xff09;模式可以提高测试用例的可重用性和可…

如何短时间内上手公众号的运营?选择工具很重要

现在网上都在讲如何运营微信公众号&#xff0c;讲有什么微信公众号技巧&#xff0c;这些都是需要用户话花较多的时间去尝试和理解&#xff0c;今天小编分享一个更简单高效运营公众号的方法&#xff01;下面跟着小编的教程一起学习如何使用乔拓云公众号助手去运营自己的公众号吧…

Vue3 ElementPlus Dialog封装

引言 多个页面中需要录入用户数据&#xff08;弹窗内容相同&#xff09;&#xff0c;重复写弹窗代码比较繁琐。因此封装一下组件&#xff0c;使用效果如下&#xff1a; 本例中模型较简单&#xff0c;记录下使用方法和原理 实现原理 参考VUE官方两个例子&#xff0c;基本父子件…

C++ 教程(02)---- 环境设置

本地环境设置 如果您想要设置 C 语言环境&#xff0c;您需要确保电脑上有以下两款可用的软件&#xff0c;文本编辑器和 C 编译器。 文本编辑器 这将用于输入您的程序。文本编辑器包括 Windows Notepad、OS Edit command、Brief、Epsilon、EMACS 和 vim/vi。 文本编辑器的名…

技术架构演进

架构演进 单机架构应用数据分离架构应用服务集群架构读写 主从分离架构冷热分离架构垂直分库架构微服务架构容器编排架构实际互联网架构 单机架构 单机架构&#xff1a; 用户访问量很少&#xff0c;没有对我们的性能、安全等提出很高的要求&#xff0c;而且系统架构简单&#x…

Pilota:为什么一个代码生成工具如此复杂丨GOTC Rust系列分享

对于一个 Rust RPC 框架来说&#xff0c;根据 IDL 做代码生成是为了让用户更方便地使用框架。而生成代码的质量以及周边能力都会对用户的开发体验有着非常非常直接的影响。 所以&#xff0c;字节跳动 CloudWeGo 开发了 Pilota 这样的一个框架&#xff0c;来为用户生成良好的代码…

Redis延时双删

1.为什么缓存和MySQL数据没有保持一致性&#xff1f; 数据一致性是什么意思&#xff0c;“一致性”包含如下情况&#xff1a; 若缓存中有数据&#xff0c;则缓存的数据值需要和DB值相同若缓存无数据&#xff0c;则DB值必须是最新值 不符合这两种情况的&#xff0c;都属于缓存…

【OJ比赛日历】快周末了,不来一场比赛吗? #06.10-06.16 #12场

CompHub[1] 实时聚合多平台的数据类(Kaggle、天池…)和OJ类(Leetcode、牛客…&#xff09;比赛。本账号会推送最新的比赛消息&#xff0c;欢迎关注&#xff01; 以下信息仅供参考&#xff0c;以比赛官网为准 目录 2023-06-10&#xff08;周六&#xff09; #4场比赛2023-06-11…

实验五 标准ACL的配置【网络安全】

实验五 标准ACL的配置【网络安全】 前言推荐实验五 标准ACL的配置问题方案步骤 最后 前言 2023-6-8 18:54:22 以下内容源自《【网络安全】》 仅供学习交流使用 推荐 配置标准ACL 实验五 标准ACL的配置 问题 络调通后&#xff0c;保证网络是通畅的。同时也很可能出现未经…

软件测试与打螺丝

单元测试中的FIRST代表下面五组英文单词对应的原则&#xff1a; FastIsolated / IndependentRepeatableSelf-validatingTimely / Thorough 软件开发中&#xff0c;往往会因为我们没有注意到的逻辑或难以理解的代码&#xff0c;而引进Bug来。 怎么尽早地发现Bug&#xff0c;…

【springCloud-2】Ribbon负载均衡

负载均衡&#xff0c;一般分为服务端负载均衡和客户端负载均衡。 服务端&#xff1a;如Nginx&#xff0c;F5等&#xff0c;请求到达服务器后进行负载均衡。 客户端&#xff1a;客户端获取到服务端的列表&#xff0c;自己经过一定的计算后选择某一台访问。 ribbon实现的就是客…