Linux——线程的同步与互斥

目录

模拟抢火车票的过程

代码示例

thread.cc

Thread.hpp

运行结果

分析原因

tickets减到-2的本质 

解决抢票出错的方案

临界资源的概念

原子性的概念

加锁

定义

初始化

销毁

代码形式如下

代码示例1:

代码示例2:

总结

如何看待锁

申请失败将会阻塞

 pthread_mutex_tyrlock

互斥锁实现原理

 锁是如何实现互斥的?

封装加锁组件(RAII风格)

Mutex.hpp

mythread.cc

RAII风格

可重入与线程的关系(浅谈)

可重入与线程安全联系

可重入与线程安全区别

死锁

概念

提出问题

死锁的四个必要条件

如何破坏死锁

概念:

线程同步

本质

先谈生产者消费者模型

"321"原则

生产者消费者模型的特点

条件变量

概念

定义

具体操作 

所用函数

定义的返回值

代码示例:

 唤醒方式


模拟抢火车票的过程

代码示例

thread.cc


#include <iostream>
#include <pthread.h>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"

// pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。

// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述

// 解决方案:加锁

void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

    while (true)
    {
        // pthread_mutex_lock(&lock);
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << username << "正在抢票" << tickets-- << std::endl;
            // 用这段时间来模拟真实的抢票要花费的时间
            // pthread_mutex_unlock(&lock);
        }
        else
        {
            // pthread_mutex_unlock(&lock);

            break;
        }
    }
    return nullptr;
}

int main()
{

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

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

    return 0;
}

Thread.hpp


#pragma once

#include <iostream>
#include <pthread.h>
#include <functional>
#include <cassert>
#include <cstring>
#include <string>

class Thread;

// 上下文
class Context
{
public:
    Thread *this_;
    void *args_;
public:
    Context():this_(nullptr),args_(nullptr)
    {}
    ~Context()
    {}
};





class Thread
{
public:
    typedef std::function<void *(void *)> func_t;
    const int num = 1024;

public:
    Thread(func_t func, void *args, int number)
        : func_(func), args_(args)
    {

        // name_="thread-";
        // name_+=std::to_string(number);

        char buffer[num];
        snprintf(buffer, sizeof buffer, "thread-%d", number);
        name_ = buffer;
        
        // 异常 == if : 意料之外用异常或者if判断
        // assert:意料之中用assert

        Context *ctx=new Context();

        ctx->this_=this;
        ctx->args_=args_;



        int n = pthread_create(&tid_, nullptr, start_routine,ctx); // TODO
        assert(n == 0);
        (void)n;
        // 编译debug的方式发布的时候是存在的,release方式发布,
        // assert就不存在l,n就是一个定义,
        // 但是没有使用的变量,有些编译器下会有warning
    }

    // 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
    // 类内成员,有缺省参数!在第一参数中包含了一个this指针
    static void *start_routine(void *args) 
    {
        Context *ctx=static_cast<Context*>(args);
        
        void *ret=ctx->this_->run(ctx->args_);

        delete ctx;

        return ret;

        // 静态方法不能调用成员方法或者成员变量
        // return func_(args_);

    }

   

    void join()
    {
        int n = pthread_join(tid_, nullptr);
        assert(n == 0);

        (void)n;
    }


    void *run(void *args)
    {
        return func_(args);
    }


    ~Thread()
    {
        // do northing
    }

private:
    std::string name_;
    func_t func_;
    void *args_;

    pthread_t tid_;
};

运行结果

user2正在抢票16
user4正在抢票15
user1正在抢票14
user3正在抢票13
user2正在抢票12
user4正在抢票11
user1正在抢票10
user3正在抢票9
user2正在抢票8
user4正在抢票7
user1正在抢票6
user3正在抢票5
user2正在抢票4
user4正在抢票3
user1正在抢票2
user3正在抢票1
user2正在抢票0
user4正在抢票-1
user1正在抢票-2

分析原因

很明显,我们只有10000张票,可是运行结果中抢到了-2。比如这个那么这多出的3张票对应的人将没地方坐。那么为什么会出现这种问题呢,下面将来解答。

抢票出错的本质:

多个线程交叉执行!

多个线程交叉执行本质:

调度器频繁发生线程调度与切换。

线程一般在什么时候发生切换呢?

1、时间片到了

2、来了更高优先级的线程,线程等待的时候。

线程是什么时候检测上面的问题呢?

从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就发生切换。

判断票是否大于0的本质逻辑是:

if(tickets>0)

1、读取内存数据CPU内的寄存器中
2、进行判断

tickets--的本质逻辑是:

1、读取数据
2、更改数据
3、写回数据

对变量进行++,或者--,在C、C++上,看起来只有一条语句,但是汇编之后至少是三条语句:

1、从内存中读取数据到CPU内的寄存器中
2、在寄存器中让CPU进行对应的算逻运算
3、写回新的结果到内存中变量的位置

tickets减到-2的本质 

 

 

全局变量
int g_val=1000;

寄存器只有一套,因此不同的线程在寄存器的存放的上下文为各个线程私有

线程A在做完 1 和 2 后 带着自己的上下文切走了
1000->999


线程B在做完1 和 2 后也带着自己的上下文切走了        

结果现在线程A切换回来了(切回来之后需要让自己的上下文给重新写入寄存器) 又把数据改为999了。

我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题!

解决抢票出错的方案

我们可以使用加锁的方式来避免这种情况发生

临界资源的概念

1、多个执行流进行安全访问的共享资源 - 临界资源
2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码的很小的一部分
3、想让多个线程串行访问资源 -- 互斥
4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性 

注意:并不是上述所有代码都属于临界区,而是访问临界资源的代码是临界区 。临界区的代码往往粒度(代码长度)很小。

原子性的概念

原子性:一个资源进行的操作,如果只用一条汇编就能完成 -- 就是原子的      反之不是。

加锁

定义

可以把这个锁定义为全局的/静态的 -- 不用初始化和销毁
直接定义为 :
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

初始化

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

销毁

int pthread_mutex_destroy(pthread_mutex_t *mutex);

代码形式如下

加锁
临界区
解锁

加锁和解锁之间的区域是临界区(也就是需要加锁的地方)。

代码示例1:

只展示了getTicket函数部分(包括加锁和解锁)


void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

    while (true)
    {
	  // 加锁
        pthread_mutex_lock(&lock);

        // 判断的本质:
        // 1、读取内存数据CPU内的寄存器中
        // 2、进行判断
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << username << "正在抢票" << tickets << std::endl;

            tickets--;
            // --的本质
            // 1、读取数据
            // 2、更改数据
            // 3、写回数据


            // 用这段时间来模拟真实的抢票要花费的时间
		// 解锁
            pthread_mutex_unlock(&lock);
        }
        else
        {
		// 解锁
            pthread_mutex_unlock(&lock);

            break;
        }
    }
    return nullptr;
}

代码示例2:

使用结构体封装了线程名和锁,并且使用了原生的线程创建方式


#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include "Thread.hpp"

// 共享资源, 火车票
int tickets = 10000;
// 就需要尽可能的让多个线程交叉执行
// 多个线程交叉执行本质:就是让调度器尽可能的频繁发生线程调度与切换
// 线程一般在什么时候发生切换呢?时间片到了,来了更高优先级的线程,线程等待的时候。
// 线程是什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测
// 如果可以,就发生切换。

// 1、多个执行流进行安全访问的共享资源 - 临界资源?
// 2、我们把多个执行流中,访问临界资源的代码 -- 临界区 -- 往往是线程代码很小的一部分
// 3、想让多个线程串行访问共享资源 -- 互斥
// 4、对一个资源进行访问的时候,要么不做,要么做完 -- 原子性
// 一个对资源进行操作,如果只用一条汇编语句就能完成 -- 原子性
// 反之:不是原子的 -- 当前理解,方便表述

// 解决方案:加锁


// 使用结构体 对锁和名字进行了封装 使用的时候直接使用结构体对象调用即可
class ThreadData
{
public:
    ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
        : threadname_(threadname), mutex_p_(mutex_p)
    {}
    ~ThreadData() {}

public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};

void *getTicket(void *args)
{
    // 获取线程的名字
    // std::string username = static_cast<const char *>(args);

    ThreadData *td = static_cast<ThreadData *>(args);

    while (true)
    {
        //  加锁
        pthread_mutex_lock(td->mutex_p_);

       

        // 判断的本质:
        // 1、读取内存数据CPU内的寄存器中
        // 2、进行判断
        if (tickets > 0)
        {
            usleep(1000);

            // 只有票数大于0才值得抢
            std::cout << td->threadname_ << "正在抢票" << tickets << std::endl;

            tickets--;
            // --的本质
            // 1、读取数据
            // 2、更改数据
            // 3、写回数据

            // 用这段时间来模拟真实的抢票要花费的时间

            // 解锁            				      pthread_mutex_unlock(td->mutex_p_);
        }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);

            // 解锁
            break;
        }
 	  // 抢完票就完了吗? 当然不是
        usleep(1000);// 模拟形成一个订单给用户
    }
    return nullptr;
}

int main()
{
#define NUM 4

    pthread_mutex_t lock;

    pthread_mutex_init(&lock, nullptr);
    std::vector<pthread_t> tids(NUM);


    for (int i = 0; i < NUM; i++)
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "thread %d", i + 1);
        ThreadData *td = new ThreadData(buffer, &lock);

        pthread_create(&tids[i], nullptr, getTicket, td);
    }

    for (const auto &tid : tids)
    {
        pthread_join(tid, nullptr);
    }


    pthread_mutex_destroy(&lock);

    
}

总结

加锁和解锁的过程:

多个线程由并行执行变为了串行执行的,程序变慢了!

锁只规定互斥访问,没有规定必须谁优先执行!

锁就是真正的让多个执行流进行竞争的结果。

执行结果一直是一个线程在抢票原因是:

该线程竞争能力强,其他线程则比较弱,因此每次都是该线程抢到了。

如何看待锁

  1.  锁,本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源,锁的安全谁来保护呢?
  2. pthread_mutex_lock、pthread_mutex_unlock:加锁的过程必须是安全的!加锁的过程其实是原子的!要不申请成功,要不申请失败。
  3. 如果申请成功,就继续向后执行,如果申请暂时没有成功,执行流会阻塞 

申请失败将会阻塞

 pthread_mutex_tyrlock

当然我们也可以使用 pthread_mutex_tyrlock(pthread_mutex_t *mutex)
函数目的是:试着去加锁
如果我加锁成功了,我会持有锁
如果加锁失败了,会出错返回

如何理解加锁和解锁的本质?

加锁的过程是原子的。

若当前线程1 线程2 线程3 同时访问临界资源的时候,如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,其他线程在做什么??

阻塞,等待。


如果线程1,申请锁成功,进入临界资源,正在访问临界资源期间,我可不可以被切换呢??

可以

当持有锁的线程被切走的的时候其他线程可以申请锁吗? 

当持有锁的线程被切走的时候,是抱着锁被切走的,即便自己被切走了,其他线程依旧无法申请锁成功,也便无法向后执行!直到我最终释放这个锁!

 

所以,对于其他线程而言,有意义的锁的状态,无非两种

1.申请锁前
2.申请锁后
站在其他线程角度,看待当前线程持有锁的过程就是原子的!!

使用锁注意事项:

1、未来我们在使用锁的时候,一定要尽量保证临界区的粒度(代码长度)非常小。

2、加锁是程序员行为,必须要做到要加都要加!

互斥锁实现原理

i++或者++i都不是原子的。
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。

 锁是如何实现互斥的?

%al:寄存器

交换的本质:共享的数据,交换到我的上下文中!!!

lock:
    movb $0, %al  //将0放到该线程的寄存器中
    xchgb %al, mutex // 寄存器和内存单元的数据相交换
        if(al寄存器的内容 > 0)
        {
            return 0;
        }
        else
            挂起等待;
        goto lock;
unlock:
    movb $1, mutex // 将1移动到内存里面
    唤醒等待Mutex的线程;
    return 0;    


1、CPU内寄存器只有一套被所有执行流共享
2、CPU内寄存器的内容,是每个执行流私有的,运行时上下文

封装加锁组件(RAII风格)

Mutex.hpp


#pragma once

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

// 写了一个锁的类,里面有加锁和解锁的成员函数
// 并且有一个锁的成员变量
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_; 
};

// 这个类中定义了一个上述类中的对象
// 并且将上述类中的成员函数放进该类中的构造函数和析构函数中
// guard:警卫
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mutex):mutex_(mutex)
    {
        mutex_.lock(); // 在构造函数中进行加锁
    }
    ~LockGuard()
    {
        mutex_.unlock();// 在析构函数中进行解锁
    }
private:
    Mutex mutex_;

};

mythread.cc



#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <cstdio>
#include <unistd.h>
#include <memory>
#include <cstring>
#include"Mutex.hpp"
// #include "Thread.hpp"

// // 定义全局变量的
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

// 共享资源, 火车票
int tickets = 10000;


class ThreadData
{
public:
    ThreadData(const std::string &threadname, pthread_mutex_t *mutex_p)
        : threadname_(threadname), mutex_p_(mutex_p)
    {}
    ~ThreadData() {}

public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};

void *getTicket(void *args)
{
    // 获取线程的名字
    std::string username = static_cast<const char *>(args);

   

    while (true)
    {
     LockGuard lockguard(&lock);
        // 将lock传过去之后调用析构函数自动加锁
        // 然后出作用域调用析构函数自动解锁


        if (tickets > 0)
        {
            usleep(1000);


            std::cout << username << "正在抢票" << tickets << std::endl;

            tickets--;

        }
        else
        {
            // pthread_mutex_unlock(&lock);

            // 解锁
            // pthread_mutex_unlock(&lock);

            break;
        }

        // 抢完票就完了吗? 当然不是
        usleep(1000);// 模拟形成一个订单给用户
    }
    return nullptr;
}

int main()
{



    pthread_mutex_init(&lock, nullptr);
    pthread_t t1, t2, t3, t4;


    pthread_create(&t1,nullptr,getTicket,(void*)"thread 1");
    pthread_create(&t2,nullptr,getTicket,(void*)"thread 2");
    pthread_create(&t3,nullptr,getTicket,(void*)"thread 3");
    pthread_create(&t4,nullptr,getTicket,(void*)"thread 4");

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    pthread_mutex_destroy(&lock);

    
}

RAII风格

重点:

 LockGuard lockguard(&lock);
 // 将lock传过去之后调用析构函数自动加锁
 // 然后出作用域调用析构函数自动解锁

可重入与线程的关系(浅谈)

可重入与线程安全联系

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

可重入与线程安全区别

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

死锁

概念

一组执行流(无论是进程还是线程),它在持有自己的锁资源的同时,还想方设法的去申请对方的锁资源,因为大家互相持有自己的,还互相申请对方的,因为锁是不可抢占式的锁(我拿了锁,除非我自己主动归还,否则别人是不能直接要我的锁的)。

所以,就是大家互相持有自己的资源,还在等待对方的锁资源而导致代码无法推进的情况,这种情况就叫死锁。

提出问题

1、一把锁,有可能死锁吗?

可能的,比如自己连续申请两次锁
pthread_mutex_lock(&lock);
pthread_mutex_lock(&lock);


2、为什么会有死锁? 逻辑链条

首先一定是用了锁才会发生死锁

3、为什么要用锁呢?

保证临界资源的安全
多线程访问我们可能出现数据不一致的问题
多线程&&全局资源
多线程大部分资源(全局的)是共享的
多线程的特征

引出的大概念

任何技术都有自己的边界,是解决问题的,但有可能在解决问题的同时,一定会可能引入新的问题!

死锁的四个必要条件

1.互斥   --   我们必须得保证访问一份资源是互斥的(这是我们锁基本特性,没有互斥那就是没有加锁)
2.请求与保持   --   我要你的资源这是请求,我还保持我自己的不释放这就是保持,我又要你的资源又不释放我自己的资源,就是请求与保持。
3.不剥夺   --   不能去抢占对方的锁,只能等待对方自动的给你这叫做不剥夺。
4.环路等待条件   --   比如线程A、线程B、线程C。线程A拥有自己的锁它去要线程B的锁,线程B有自己的锁它去要线程C的锁,线程C有自己的锁它去要线程A的锁。形成了一个环路情况。

注意:

只有这四个必要条件同时都满足的情况下才会造成死锁。

如何破坏死锁

概念:

破坏死锁的本质就是破坏这四个条件的至少一个!

做法:

加锁顺序一致
避免锁未释放的场景
资源一次性分配

线程同步

本质

线程运行同步的本质:
当我们在进行临界资源访问安全的前提条件下,让多个线程按照一定的顺序进行资源访问。

先谈生产者消费者模型

生产的过程和消费的过程 - 解耦

生产者跟消费者互不影响。

临时保存产品的场所  --  缓冲区

不符合生产者消费者模型的例子

函数调用:

调用方:生产了数据

形成变量:变量暂时保存数据

目标函数:消费了数据

main函数调用fun函数的时候
正在调用的过程中main正在等待fun函数返回
main函数与fun函数是强耦合关系

"321"原则

3种关系:  生产者和生产者(互斥),消费者和消费者(互斥),生产者和消费者(互斥|[保证共享资源的安全性]/同步)。
-- 产品(数据)

2种角色:  生产者线程,消费者线程

1个交易场所:  一段特定结构的缓冲区。交易的是数据。

注意:
只要我们想写生产消费模型,我们本质工作其实就是维护321原则!

生产者消费者模型的特点

1、生产线程和消费线程进行解耦
2、支持生产和消费的一段时间的忙闲不均的问题 -- 缓冲区的优势
3、提高效率

条件变量

概念

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

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

定义

是一种数据类型,我们可以使用这个类型来定义变量或者对象。

具体操作 

本质是对线程的PCB做处理。

所用函数

操作系统调用pthread_cond_wait()的时候就是把它放进等待队列里面

操作系统调用pthread_cond_signal()就是它唤醒。

定义的返回值

定义条件变量之前要先初始化。成功返回0,失败返回对应的错误码。

谁排队谁唤醒?

让指定线程在条件变量里面进行排队
主线程进行唤醒。

代码示例:


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

int tickets = 1000;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void *args)
{
    std::string name = static_cast<const char *>(args);

    while (true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond, &mutex); // 为什么要有mutex 后面会说
        // 判断暂时省略
        std::cout << name << " -> " << tickets << std::endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
    return nullptr;
}

int main()
{
    // 通过条件变量控制线程的执行

    pthread_t t[5];
    for (int i = 0; i < 5; i++)
    {
        char *name = new char[64];
        snprintf(name, 64, "thread %d", i + 1);
        pthread_create(&t[i], nullptr, start_routine, name);
    }

    while (true)
    {
        sleep(1);
        pthread_cond_signal(&cond);
        std::cout << "main thread wakeup one thread" << std::endl;
    }

    for (int i = 0; i < 5; i++)
    {
        pthread_join(t[i], nullptr);
    }

    return 0;
}

 唤醒方式

唤醒单个线程:

int pthread_cond_broadcast(pthread_cond_t *cond);


唤醒多个线程:

int pthread_cond_signal(pthread_cond_t *cond);

运行结果图如下:

 

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

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

相关文章

【C++】STL中stack的用法及模拟实现

目录 一、stack的简介二、stack的使用三、stack的模拟实现 一、stack的简介 stack是一种容器适配器&#xff0c;专门用在后进先出操作的上下文中环境中&#xff0c;其中的元素只允许从容器固定的一端进行插入和删除操作。stack是作为容器适配器来实现的&#xff0c;容器适配器…

信息安全实践1.3(HTTPS)

前言 做这个实验对Tomcat的版本有要求&#xff0c;最好是使用Tomcat8。因为我之前使用Tomcat10&#xff0c;然后一直做不出来。 要求 部署Web服务器端HTTPS功能&#xff0c;通过网络嗅探分析HTTPS通过SSL实施安全保护的效果 关键步骤 首先要给tomcat配置https&#xff0c;也…

Unity3D安装:离线安装 Unity

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 3D工具集&#xff1a; NSDT简石数字孪生 在没有 Hub 的情况下离线安装 Unity Unity 下载助手 (Download Assistant) 支持离线部署。在这种部署方式中&#xff0c;可下载用于安装 Unity 的所有文件&#xff0c;然后生成脚本…

采购申请审批测试

采购申请审批的配置并不难&#xff0c;但是总会有原因导致业务无审批策略&#xff0c;而且这个配置也比较脆弱&#xff0c;有时同步也会出现问题&#xff0c;小编利用这篇操作记录下测试结果。 1、项目类别的审批策略分类 下图是审批策略分类-项目类别不给值&#xff0c;测试…

(浙大陈越版)数据结构 第三章 树(上) 3.4 小白专场:树的同构(PTA编程题讲解)

题意理解和二叉树表示 给定两棵树T1和T2。如果T1可以通过若干次左右孩子互换变成T2&#xff0c;则称两棵树是“同构”的。 eg1&#xff1a;现请你判断如下两棵树&#xff08;左侧为T1&#xff0c;右侧为T2&#xff09;是否为同构树&#xff1f; 显然T1可以通过有限次左右孩子…

如何利用IDEA将Git分支代码回退到指定历史版本

一、背景 作为一名后端开发&#xff0c;相信大家一定遇到过这样的情景&#xff0c;代码开发人员过多&#xff0c;并且开发分支过多&#xff0c;导致代码版本管理困难&#xff0c;这样就难免遇到一些代码合并出错&#xff0c;比如&#xff0c;当我提交了本次修改到本地和远程分…

jsp页面调试

现象: 访问jsp页面, 页面为空, 网络请求显示失败, 控制台打印错误net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 分析: 错误描述&#xff1a;编码模块不完整&#xff0c;返回浏览器的流不完整 可能得原因: 1、网络是否稳定 2、服务器端是否有对响应数据做限制&#xff0c;比如…

photoshop矫正扫描图片的倾斜问题以及修改图片内容

由于工程原因&#xff0c;资料需要重新梳理 1.扫描工程表格到电脑中 2.在ps中导入表格内容&#xff08;表格有时候是倾斜的&#xff09; 需要修正为正常状态&#xff0c;即垂直状态 设置步骤&#xff1a; 1.调整ps的背景颜色与所在图片的背景颜色一致 用吸管工具&#xff…

【thingsboard+NodeRed+chirpstack】实现Lora节点设备的数据上下行通讯

本文主要实现基于 thingsboard+NodeRed+chirpstack 实现 lora设备的数据上下行通讯。 NodeRed作为mqtt桥接器,在开源的社区版 thingsboard上实现 这里写目录标题 LoRa 设备上下行通讯方案数据上行数据下行Device 层面创建设备时,要添加 relation规则链层面灯控模块规则链规则…

Sentinel降级规则

1.降级规则简介 官方文档 熔断降级概述 除了流量控制以外&#xff0c;对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块&#xff0c;可能是另外的一个远程服务、数据库&#xff0c;或者第三方 API 等。例如&#xff0c;支付的…

华为OD机试之处理器问题(Java源码)

处理器问题 题目描述 某公司研发了一款高性能AI处理器。每台物理设备具备8颗AI处理器&#xff0c;编号分别为0、1、2、3、4、5、6、7。 编号0-3的处理器处于同一个链路中&#xff0c;编号4-7的处理器处于另外一个链路中&#xff0c;不通链路中的处理器不能通信。 如下图所示。…

基于html+css的图展示97

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

安装Arch Linux后要做的十件事

Arch Linux 是一款轻量级、灵活且高度可定制的Linux发行版&#xff0c;被广泛用于个人电脑和服务器。一旦您成功安装了Arch Linux&#xff0c;接下来有一些重要的任务需要完成&#xff0c;以确保系统的稳定性和安全性&#xff0c;并为您的需求做好准备。 本文将详细介绍安装Ar…

【Android】配置不同的开发和生产环境

目录 前言 配置build.gradle&#xff08;Module级别&#xff09; 创建对应环境的目录 切换不同环境 ​编辑选择打包的环境 前言 在web开发中不同的环境对应的配置不一样&#xff0c;比如开发环境的url是这样&#xff0c;测试环境的url是那样的&#xff0c;在app中也会涉…

jdk15至17——sealed密封关键字

sealed关键字是从jdk15开始预览&#xff0c;直到jdk17成为正式版&#xff0c;可以对继承父类和实现接口进行更加细粒度的限制&#xff0c;之前的限制也只有final用于禁止继承&#xff0c;默认包权限限制在同一个包内&#xff0c;sealed密封类/接口可以明确指定哪些类可以进行继…

通过Python的PIL库给图片添加马赛克

文章目录 前言一、Pillow是什么&#xff1f;二、安装PIL库三、查看PIL库版本四、使用方法1.引入库2.定义图片路径3.打开需要打马赛克的图片4.获取图片尺寸5.创建一个新的图片对象6.定义块的宽高7.循环遍历图片中的每个块进行处理8.保存马赛克图片9.效果 总结 前言 大家好&#…

客服配置-shopro

客服配置 注意事项 shopro客服系统 采用 workerman 的 gateway-worker 作为服务基础&#xff0c;请先安装 gateway-worker 扩展包shopro商城 已不再支持 workerman 在线客服插件 安装部署 安装扩展包 composer require workerman/gateway-worker:~3.0 删除禁用函数(如有未列…

C Primer Plus第十二章编程练习答案

学完C语言之后&#xff0c;我就去阅读《C Primer Plus》这本经典的C语言书籍&#xff0c;对每一章的编程练习题都做了相关的解答&#xff0c;仅仅代表着我个人的解答思路&#xff0c;如有错误&#xff0c;请各位大佬帮忙点出&#xff01; 1.不使用全局变量&#xff0c;重写程序…

idea使用Alibaba Cloud Toolkit插件远程操作Docker

idea使用Alibaba Cloud Toolkit插件远程操作Docker 文章目录 前言一、tcp://IP:2375或者Unix socket 连接Docker(不安全)问题1&#xff1a;为什么本地虚拟机能连上&#xff0c;xxx云ECS服务器连不上&#xff1f;问题2&#xff1a;什么是Unix域套接字&#xff1f;有什么作用&…

Linux之创建进程、查看进程、进程的状态以及进程的优先级

文章目录 前言一、初识fork1.演示2.介绍3.将子进程与父进程执行的任务分离4.多进程并行 二、进程的状态1.进程的状态都有哪些&#xff1f;2.查看进程的状态2.运行&#xff08;R&#xff09;3.阻塞4.僵尸进程&#xff08;Z&#xff09;1.僵尸状态概念2.为什么要有僵尸状态&#…