linux入门---线程的互斥

news2024/11/16 1:17:25

目录标题

  • 什么是线程安全
  • 为什么会出现负数
  • 几个概念的介绍
  • 锁的理解
  • 锁有关函数的介绍
  • 锁的问题
  • 如何看待加锁和解锁
  • 锁的实现原理
  • 锁的封装
  • 线程安全和可重入函数
  • 死锁的概念

什么是线程安全

我们通过下面的例子来了解一下线程安全问题,首先我们实现一个模拟抢票的功能创建一个全局变量ticket将其定义为1000,然后创建3个线程每个线程都是一个循环来不停的对变量ticket的值减一以表示票数的减少,每抢到一张票就打印一段话然后休眠一段时间,当票的数量减少为0的话就直接退出循环并结束线程的函数,那么这里的代码就如下:

int ticket_num=1000;
void * func(void* args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        if(ticket_num>0)
        {
            usleep(12345);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_create(&tid1,nullptr,func,(void*)"user1");
    pthread_create(&tid2,nullptr,func,(void*)"user2");
    pthread_create(&tid3,nullptr,func,(void*)"user3");
    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

代码的结果如下:
在这里插入图片描述
可以看到程序运行到最后出现了0 和-1这样的数这是不符合我们的要求的,因为根据逻辑来看的话抢票应该抢到1的时候就截止了,那么这里出现0和-1的现象就称之为线程安全问题。当然有些小伙伴可能下去自己尝试并没有出现负数的情况要想出现这种情况就得尽可能让多个线程交叉执行,这样在不停的交叉运行的过程中就会出现数据交叉的情况也就会出现上面的现象,而多个线程交叉执行的本质就是让调度器尽可能的频繁发生线程调度和切换,线程发生切换的场景就是进程的时间片到了得换一个进程来接着执行,或者来了更搞优先级的线程得将当前的进程换下来让优先级高的进程来执行,或者是线程等待当前进程要等待其他的外设运行好才能接着执行下面的代码那么这个时候cpu不会等他就将其换下来,前两个我们都很难模拟出来所以采用第三种方法让线程进行等待也就是通过usleep函数让其休眠这样就等待了,当程序从内核太返回用户态的时候,线程就要对调度的状态进行检测,如果情况满足就可以发生线程切换,那么接下来我们就讨论一下为什么会出现这样的情况。

为什么会出现负数

我们将当前的情况推向极致,假设当前的ticket的值已经是1了,那么我们创建的三个线程中的循环结束了吗?没有结束,他们依然可以从头到尾的执行循环中的代码,而执行的第一个就是if语句他是用来判断的,而判断的本质是读取内存中的数据放到cpu的寄存器中然后进行判断,当变量的值为1时多个线程可以同时执行判断语句吗?答案是不可以的因为当前机器只有一个cpu所以每个时刻只能执行一个线程。但是当线程一判断完之后就会接着执行usleep函数他会让线程进行等待,所以这个时候就会发生线程切换线程一就会将自己的上下文数据(ticket_num的值是1)被切走了,线程一走了线程二就会接着执行if语句的判断,而判断的过程又是从内存中读取数据然后放到寄存器中进行判断,可是线程一在if语句的时候是没有对内存中的值进行修改的,所以当ticket为1时线程二也能进入if语句进入之后就休眠然后拿着上下文数据被切走了,同样的道理线程三也能进入if语句,也就是说当ticket的值为1时有三个线程在if语句中执行,并且这三个线程都认为自己拿到了ticket的值为1,当线程等待结束时就会接着执行下面的减减代码,而对数据–的操作分为三步:从内存中读取数据,更改数据,写回数据,所以最先被唤醒的线程就会最先对ticket的值进行减减变成了0然后写回内存,然后第二个被唤醒的线程就又会从内存中读取数据也就是0然后对其减减变成-1然后写回内存,第三个线程也是同样的道理,那么这就是为什么会出现负数的原因,那么这个时候就有小伙伴会想这里出现错误的原因是多个线程同时进入了if语句,那我们将if语句去掉能不能解决线程安全问题呢?答案是不行的,即使不加if就让多个线程对全局变量进行更改也不是安全的,对变量进行++,或者- -,在c,c++上看起来只有一条语句,但是汇编之后至少是三条语句:1.从内存中读取数据到寄存器中,2.在寄存器中让cpu执行对应的逻辑运算,3.得到新的结果然后写回内存,该语句的执行需要三个步骤也就是三条汇编语句才能完成,所以这里就会出现干扰问题,比如说当前的存在两个线程同时对一个数据进行减减,变量的值一开始为10000
在这里插入图片描述
线程一会先将num的值读取放到内存上:
在这里插入图片描述
然后对num的值减一:
在这里插入图片描述
然后正当线程一要完成第三步将寄存器中的数据写回内存时可能就会发生线程切换,因为寄存器的值是属于当前线程的,所以会将9999一起打包切走,然后就轮到线程二来执行,线程二也是完成同样的步骤先读取数据然后对数据进行修改,最后将数据写回内存,但是该操作是在一个循环里面执行的,也就会出现多次对数据进行减减的情况,假设线程将原来的10000减到了5000并将其写回到内存:
在这里插入图片描述
线程二将数据修改到一半时他的时间片可能就到了那么这个时候就会发生线程切换,线程一就会接着执行而线程在切换的时候会记录之前的上下文数据,所以线程一就会接着执行之前没有完成的第三步也就是将数据9999写入到内存:
在这里插入图片描述
所以这就会导致刚刚的线程二白做了很多的事情间接的造成了系统资源的浪费,那么这就是线程安全问题我们定义的全局变量在没有保护的时候往往是不安全的,像上面多个线程在交替执行照成的数据安全问题,发生了数据不一致的问题,那么接下来我们就来学习如何解决这个问题。

几个概念的介绍

多个执行流进行安全访问的共享资源称为临界资源,我们把多个执行流中访问临界资源的代码称为临界区,往往是线程代码点的很小的一部分,比如说上述代码中只有if语句的代码才访问到了临界资源,那么为了保证临界资源的安全我们就让多个线程串行的访问共享资源就,我们把这种串行访问的行为就称为互斥也就是说当一个线程访问临界资源时其他的线程不能访问,只有当这个线程访问结束时才能有一个线程接着进行访问,然后我们把对一个资源进行访问的时候要么不做要么就做完的特性称为原子性比如说上面的++,他就有三个步骤他可以做完前两个步骤然后被切换不做第三步所以他就不是原子性他有中间状态,准确来说上面的三个步骤在外来对应着三条汇编语句,而我们说的原子性就是一个对资源的操作只需要一条汇编语句就能完成,如果要多条汇编语句则不是原子。这里大家注意一下我们这里说的原子性的概念只是一个子集也就是完整概念的一部分我们后面会对原子性有根深的理解。那么接下来我们就来看看什么是锁。

锁的理解

上面写的程序出现问题的原因是多个执行流都进入到了if语句里面的然后导致了数据的异常,并且多个执行流对同一个数据++或者- -的话也会出现问题因为++和- -看上去只有一步但是在底层看来他是有多个步骤的,所以当进程切换的时候就可能会出现数据重复的问题导致了计算机资源的浪费,那么我们把上面的if语句和++或者- -看成一个房间,出问题的本质就是多个人同时进入了一个房间,或者一个人在房间里没有把一件事做完就被其他人赶了出来其他人又进入了房间,所以要想解决问题就得给房间的门上一把锁,当没有人进入这个房间的时候门是打卡的,当第一个人进入房间的时候不管第二个人离第一个人有多近这个门都会自动关上并锁上,那么这就确保了只有一个人在房间里面,并且房间的门一旦关上锁上之后外面的人是无法打卡的,只有房间里面的人干完了事主动打开才行,那么这就确保了房间里面的人不会其他人强行的赶出来,那么这个门上的锁不仅在生活中存在在程序里面也是存在的,接下来我们来看看锁的定义。

锁有关函数的介绍

首先来看看函数pthread_mutex_init的声明:
在这里插入图片描述

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const_pthread_mutexattr_t *restrict attr)

该函数的第一个参数就是一个指向pthread_mutex_t类型的指针,pthread_mutex_t就是一个锁当我们定义一个锁类型的变量时就得使用pthread_mutex_init函数对其进行初始化,第二个参数表示锁的属性这个我们不用管一般传递nullptr就行,当我们用完锁之后就要对锁进行销毁,那么这里使用函数就是

pthread_mutex_destroy(pthread_mutex_t *mutex)

该函数的参数就是一个指向锁的指针,想要销毁哪个指针就传递哪个锁的地址,将锁锁起来的函数就是

pthread_mutex_lock(pthread_mutex_t *mutex)

将锁上的锁打开函数就是

pthread_mutex_unlock(pthread_mutex_t *mutex)

在这里插入图片描述
这里有个特例就是如果锁是全局的话就不需要使用pthread_mutex_init函数进行初始化,而是使用pthread_mutex_t *mutex进行初始化比如说下面的代码:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

这就是初始化全局锁的方式,有了上面几个函数我们就可以对上面写的代码就行修改:

void * func(void* args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        if(ticket_num>0)
        {
            usleep(1245);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

首先我们得创建一个锁,然后while循环里面的if语句属于临界区,那么在访问这个区域的代码之前就得使用pthread_mutex_lock函数将锁锁上,当一个线程执行完if语句里面的内容时就得使用pthread_mutex_unlock函数将锁打开,这里大家要注意一下因为线程可能会执行else语句中的内容所以else里也得添加pthread_mutex_unlock函数不然就会导致锁没有被释放其他线程都无法申请的情况,那么修改后的代码就如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void * func(void* args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(ticket_num>0)
        {
            usleep(1245);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
            pthread_mutex_unlock(&mutex);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

我们再运行一下程序让其多执行几次就可以看到,没有出现数据异常的问题:
在这里插入图片描述
那么这就是锁的应用,大家可以通过下面的图片再了解一下锁的使用:
在这里插入图片描述

锁的问题

第一个问题:如果大家亲自去写了执行了上述的代码就会明显的感觉到程序变慢了,而变慢的原因就是枷锁和解锁的过程是多个线程串行执行的。第二个问题:我们写的这个程序是用来模拟抢票的但是通过运行结果来大致比较的话会发现这里好像存在不公平的现象,我们创建了三个线程一共有1000张票,但是线程三一个人就抢走了快800张票并且还是连着抢的,线程二抢走了150张票并且也是连着抢的而线程一只抢到了区区的50张票,那这是为什么呢?原因很简单因为锁只规定了互斥访问也就是让执行流串行的访问并没有规定让谁先优先获取锁,所以锁就是真正的多个执行流竞争的结果,所以很可能会出现当一个线程释放锁之后该线程可能又会申请到锁资源的情况,那么这种现象的产生就必定会导致资源的分配不公平,有些线程过去的繁忙而有些线程又过于的悠闲的现象,并且一个线程执行完任务之后一般会对任务进行汇总并返回给用户,所以当释放掉锁的资源之后一般会干点其他的事情比如说将刚刚锁中计算的结果进行汇总等等(那么这里可以用休眠一下),这么做之后其他的线程才会有更大的几率申请到锁:
在这里插入图片描述
可以看到这会票的分配明显就分散一些并且没有出现连着抢的情况,那么这就是锁的一个问题:锁资源的竞争问题。

如何看待加锁和解锁

在上面的程序中我们创建了一个全局变量的锁,这个锁可以被多个线程使用每个线程都可以让其枷锁或者解锁,那为什么要有锁呢?因为我们要保证共享资源在被多线程访问时的数据安全,可是锁也是一个共享资源啊,锁的安全谁来保护呢?所以锁的枷锁过程必须得是线程安全的也就是必须得是原子的,如果锁申请成功就会继续往后自行,如果申请暂时没有成功执行流会如何?这里可以连续申请两次锁来沿着,然后会发现线程卡住了,所以申请锁没有成功时执行流就会阻塞也就相当于将自己挂起的状态,当然这里也可以使用pthread_mutex_trylock来申请锁,这个申请的方式就是如果可以申请到就正常的申请,如果没有申请到就报错直接返回不会阻塞式等待:
在这里插入图片描述谁持有锁谁就能进入临界区,如果线程1申请锁成功进入了临界资源并正在访问临界资源期间,其他的线程在做什么呢?答案是阻塞等待,那如果线程一申请锁成功进入临界资源并正在访问临界资源期间,我们能不能进行线程切换呢?答案是绝对可以的,因为当持有锁的线程被切走的时候他是和锁一起被切走的,所以即便获线程被切走了,其他的线程也依旧无法成功申请也就无法向后执行,直到该线程最终释放了锁所以对于其他线程而言锁的状态无非就两种1.申请锁之前,2.释放锁之后,所以站在其他线程的角度看待当前线程持有锁的过程就是原子也就是要么我没有持有锁,要么我持有锁就把该执行的操作都执行完然后再释放锁,所以未来我们在使用锁的时候一定要尽量保证临界区的粒度(锁中间保护的代码的多少)非常的小!这里要注意的一点就是枷锁是程序员的行为,必须做到要加锁就对所有访问该资源的全部线程都加锁,不能说有些线程一线程二访问ticket的时候枷锁,线程三就不枷锁。通过上面的讲解大家肯定能够理解枷锁的过程一定是原子的,那解锁的过程一定得是原子的吗?答案是没有必要的因为解锁的前提是已经枷锁了,而枷锁之后也就只有一个执行流在执行临界区的代码,所以这个时候就不太需要担心躲执行流的问题。

锁的实现原理

++i和i++都不是原子的需要多条汇编来实现,所以可能会出现数据的一致性问题,为了实现互斥锁的操作大多数体系结构都提供了swap或者exchange汇编指令,该指令的作用就是把内存单元的数据和寄存器中的数据进行交换,因为只有一条指令所以在执行这个语句的时候是能够保证原子性,锁就是一个数据类型为了理解我们可以将其看成整形类型并且大小为1,cpu中有很多的寄存器但是这些寄存器只有一套被所有的执行流共享,cpu寄存器的内容是每个执行流都私有的,我们将其称之为执行流运行的上下文,枷锁的伪代码就是下面这样:
在这里插入图片描述
cpu中有一个名为%al的寄存器,内存上有一个名为mutex的变量当前环境下有两个线程,一个线程A一个线程B,线程A先放到cpu上先被执行,那么他首先干的事情就是movb $0 ,$al也就是将0放到%al寄存器里面:
在这里插入图片描述

那么在完成第一步分过程中会发生线程切换吗?答案是肯定会的!执行上面的任意一条汇编指令都可能会发生线程切换,但是寄存器中的数据是属于当前进程的,所以即使切换也不会有任何的影响等线程被切换回来时又会进行上下文回复,然后执行的第二步就是xchgb %al, mutex也就是将寄存器中的值与内存上的mutex变量中的值进行交换,mutex是内存上的变量能够被多个线程同时访问而exchange交换只用了一条汇编指令就实现了,而交换的本质就是将共享的数据交换到我的上下文当中所以线程中的寄存器变成了1,内存中的寄存器变量变成了0,而寄存器又是线程A的上下文

在这里插入图片描述
执行完这个汇编指令之后也可能会发生线程交换,切换的时候就会将当前线程的上下文带走也就是将1给带走,当其他线程被切换上来执行枷锁代码的时候也会经历上述的过程,可是这个时候内存mutex变量中的值为0,新来的线程中的值也为0,执行第二个指令交换之后两者依然都为0,然后就会执行后面的if语句进行判断如果当前线程中的值为1的话就能申请锁成果,如果为其他的值就会被挂起等待,所以线程B切换了上来也只能挂起等待了所以申请锁就没有成功,而这个时候将线程A又切换了回来线程A中保存了之前的上下文的内容所以他的值为1,那么这个时候就能执行if语句中的内容申请锁成功直接返回0,所以线程A在申请锁的过程中是不担心被切换走的上述的过程就保证了申请锁的原子性,只要一个线程申请锁成功这个1就一直在这个线程的上下文当中,那么这就是枷锁的过程,而解锁的过程就十分的简单,move指令就是拷贝的意思,那么这里就是将1拷贝到mutex变量里面就可以了,那么这就是枷锁和解锁的原理。

锁的封装

那么有了上面的函数我们可以自行的封装一个锁,我们对这个锁的要求就是创建的时候就枷锁,销毁的时候就自动解锁,那么要想实现这样的功能我们就创建一类,类中有个指向锁类型的指针:

class Mutex
{
public:

private:
    pthread_mutex_t *lock_p;
}

这个类的构造函数有一个参数用来接收锁的地址,然后还提供枷锁函数和解锁函数,这两个函数的内部都是调用库中对应的函数,那么这里的代码如下:

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

有了这个类之后我们就可以再创建一个名为LockGuard的类,这个类里面就有一个Mutex变量:

class LockGuard
{
public:

private:
    Mutex _mutex;
};

那么这个类的构造函数也是需要一个锁类型的指针将他用来初始化_mutex对象,然后在结构体类调用_mutex的lock函数,然后析构函数就直接调用_mutex的unlock函数即可,那么这里的代码就如下:

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* mutex)
    :_mutex(mutex)
    {
        _mutex.lock();
    }
    ~LockGuard()
    {
        _mutex.unlock();
    }

private:
    Mutex _mutex;
};

那么这就是锁的封装,有了自行封装的锁就可以对上述抢票代码进行修改,原代码如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void * func(void* args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        if(ticket_num>0)
        {
            usleep(1245);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
            pthread_mutex_unlock(&mutex);
            usleep(1243);
        }
        else
        {
            pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

因为我们创建的是创建时枷锁,销毁时解锁,所以这里就可以这么修改:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void * func(void* args)
{
    string name=static_cast<const char*>(args);
    while(true)
    {
        //pthread_mutex_lock(&mutex);
        LockGuard lockguard(&mutex);
        if(ticket_num>0)
        {
            usleep(1245);
            cout<<name<<" 正在进行抢票 "<< ticket_num<<endl;
            --ticket_num;
            //pthread_mutex_unlock(&mutex);
            usleep(1243);
        }
        else
        {
            //pthread_mutex_unlock(&mutex);
            break;
        }
    }
    return nullptr;
}

代码的运行结果如下:
在这里插入图片描述
可以看到依然可以正常的执行,那么这就是锁的封装。

线程安全和可重入函数

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

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

线程安全和可重入函数看上去是两个概念,但是线程不安全可能是不可重入函数导致的,如果一个函数不会因为多线程而出现问题,那么我们将其称为线程安全函数,可重入函数只是线程安全函数的一种,函数是否可重入描述的是函数在被多个执行流执行时会不会出现问题,如果出了问题这个函数就是不可重入的,如果一个代码片段被多个执行流执行会不会出现数据安全问题,出现数据安全问题那么这就是数据不安全的。可重入函数和线程安全的关系就是:1.函数是可重入的,那就是线程安全的,2.函数是不可重入的,那就不能由多个线程使用有可能引发线程安全问题 3.如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

死锁的概念

在多把锁的场景下,我们持有自己的锁不释放还要对方的锁,对方也是如此要我们的锁,那么这时就容易造成死锁双方都得不到对方的锁,这就好比两个小朋友各自都有5毛钱但是棒棒糖价格是1块钱,将两个5毛钱合并起来确实能买一个棒棒糖但是两个人都不想把钱给对方而是想对方把钱给自己,所以这导致了两个人谁也得不到另外一个5毛钱两个人都吃不到棒棒糖,并且一把锁也能照成死锁比如说连续对一把锁执行两次枷锁就会被阻塞,那为什么会有死锁,因为多线程的特性:多线程大部分资源都是共享的,所以多线程中的全局资源就可能在多线程的访问中出现数据不一致的问题,所以得保证临界资源的安全,所以得使用锁,使用锁了之后就会导致死锁的问题而造成死锁就得有四个必要条件:
第一个:互斥
访问某些资源的时候是互斥的
第二个:请求和保持
请求你的资源的时候还要保持我的资源,也就是我自己有5毛钱不给你我还想要你的5毛钱。
第三个:不剥夺
剥夺就是你不给我5毛钱我就打你揍你把你的5毛钱抢过来,那么不剥夺就是你不给我5毛钱我也不打你不揍你不抢你的我就等着。
第四个:环路等待
a有自己的锁然后她想要b的锁,b有自己的锁然后他想要c的锁,c有自己的锁然后他想要a的锁这就导致这三个人谁也要不了谁的锁。

所以要想破坏死锁本质上就得破坏上面的四个条件的任意一个,第一个互斥就不用考虑了因为互斥才有了锁,第二个就是:不请求与保持当我申请某个锁失败的时候就将自己的锁释放掉,第三个要剥夺:所以我们可以设置一个竞争策略当优先级更高的线程申请一个锁时优先级较低的线程应该主动释放自己的锁,第四个就是不形成环转也就是让申请锁的顺序保持一致不形成环比如说有abcd四把锁,那么每次申请锁的顺序都是a b c d这样a能申请成功那么b就一定能够申请成功,另外大家要注意的一点就是一个线程申请锁到了锁另外一个线程是可以对其进行解锁的,通过之前的伪代码便可以得知:
在这里插入图片描述
解锁的时候并不是去申请到锁的那个线程的上下文找1而是直接将1放到锁变量里面,因为这个特性所以就有了死锁检测算法和银行家算法这两个算法来避免死锁,那么这就是死锁的概念。

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

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

相关文章

docker搭建halo个人博客

docker run -it -d --name halo -p 9090:8090 -v /usr/local/halo2:/root/.halo2 halohub/halo:2.10halo个人网站搭建效果 提供皮肤市场 编辑界面 自定义菜单

在win10下,使用torchviz对深度学习网络模型进行可视化

目录 1. 安装 graphviz 和 torchviz 2.安装 graphviz.exe 3.实例测试 4.如果你的电脑还是无法画图&#xff0c;并且出现了下面的报错&#xff1a; 5.参考文章&#xff1a; 1. 安装 graphviz 和 torchviz 首先打开 Anaconda prompt 进入自己的 pytorch 环境(图中 pt 是我自…

liunx练习题之在同一主机提供多个的web服务

虚拟web主机类型 一、基于端口 1.vim /etc/httpd/conf.d/vhost2.conf ---- — 改变http服务默认访问路径 <directory /testweb1>allowoverride none 表示不允许覆盖其他配置require all granted 表示允许所有请求 </directory> <virtualhost 0.0.0.0:…

【Leetcode】【简单】13. 罗马数字转整数

力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能&#xff0c;轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/roman-to-integer/description/ …

【网络协议】聊聊套接字socket

网络编程我们知道是通过socket进行编程的&#xff0c;其实socket也是基于TCP和UDP协议进行编程的。但是在socket层面是感知不到下层的&#xff0c;所以在设置参数的时候&#xff0c;其实是端到端协议智商的网络层和传输层。TCP是数据流所以设置为SOCK_STREAM&#xff0c;而UDP是…

【配置环境】VS Code中JavaScript环境搭建

一&#xff0c;环境 Windows 11 家庭中文版&#xff0c;64 位操作系统, 基于 x64 的处理器VS Code 版本: 1.83.1 (user setup)Node.js 版本&#xff1a;20.9.0 二&#xff0c;为什么搭建JavaScript环境 因为在看《重构改善既有代码的设计第2版》的时候&#xff0c;书中的代码展…

嵌入式基础知识-RSA非对称加密基本原理

之前的文章嵌入式基础知识-信息安全与加密&#xff0c;介绍过数据加密的一些基本概念&#xff0c;对称加密的原理比较简单&#xff0c;加密和解密的密钥相同&#xff0c;而非对称加密&#xff0c;两个密钥不同&#xff0c;本篇就来具体介绍RSA这种非对称加密的密钥计算原理。 …

【PyQt学习篇 · ⑧】:QWidget - 窗口特定操作

文章目录 图标标题不透明度窗口状态最大化和最小化窗口标志案例 图标 setWindowIcon(QIcon("resource/header_icon.png"))&#xff1a;该函数用于设置QWidget的窗口图标。可以为窗口设置一个图标&#xff0c;以显示在窗口标题栏、任务栏或窗口管理器中。 windowIcon…

全面解析:oa系统是什么?有哪些好用的oa系统

oa系统是什么&#xff1f;有哪些好用的oa系统 一、什么是OA系统 OA系统全称为Office Automation&#xff0c;即办公自动化系统。它是一种专门为企业和机构的日常办公工作提供服务的综合性软件平台&#xff0c;具有信息管理、流程管理、知识管理&#xff08;档案和业务管理&am…

视频剪辑软件Corel VideoStudio Ultimate 会声会影2024中文旗舰版免费下载安装步骤

我喜欢视频剪辑软件Corel VideoStudio Ultimate 会声会影2024中文旗舰版&#xff0c;因为它使用起来很有趣。它很容易使用&#xff0c;但仍然给你很多功能和力量。VideoStudio让我与世界分享我的想法&#xff01;“这个产品的功能非常多&#xff0c;我几乎没有触及它的表面&…

分布式系统之CAP理论

1. CAP 概念 CAP 概念指的是分布式系统中的三个核心属性&#xff1a;一致性&#xff08;Consistency&#xff09;、可用性&#xff08;Availability&#xff09;、分区容错性&#xff08;Partition Tolerance&#xff09;。CAP 定理由计算机科学家 Eric Brewer 在 2000 年提出&…

keealived安装配置启动

1.keepalived作用和原理图 keepalived作用:解决单点故障简单原理图1: 2.keepalived安装配置启动 地址: https://www.keepalived.org/download.html# 1)解压 tar -zxvf keepalived-2.0.18.tar.gz # 2)进入keepalived目录 cd keepalived-2.0.18/ # 3)安装libnl/libnl-3依赖…

系统架构设计之云原生架构

云原生架构 一. 云原生技术介绍二. 传统架构模式 VS 云原生架构模式三. 云原生架构反模式四. 云原生架构设计原则 其它相关推荐&#xff1a; 软考系统架构之案例篇(架构设计相关概念) 系统架构之微服务架构 系统架构设计之微内核架构 鸿蒙操作系统架构 所属专栏&#xff1a;系统…

Tomcat简介 安装 站点部署 多实例配置 反向代理

一、Tomcat简介二 、Tomcat帮手 --- JDK安装jdk检查jdk是否安装成功 三.安装TomcatTomcat配置管理页面 四、web站点部署五、部署开源站点&#xff08;jspgou商城&#xff09;安装数据库 六、 jspgou商城上线配置数据库连接 七、Tomcat多实例配置复制程序文件浏览器访问 八、Tom…

3D LUT 滤镜 shader 源码分析

最近在做滤镜相关的渲染学习&#xff0c;目前大部分 LUT 滤镜代码实现都是参考由 GPUImage 提供的 LookupFilter 的逻辑&#xff0c;整个代码实现不多。参考网上的博文也有各种解释&#xff0c;参考了大量博文之后终于理解了&#xff0c;所以自己重新整理了一份&#xff0c;方便…

RT-Smart 开发笔记:int 类型数值溢出造成的奇怪问题的分析与排查记录

前言 最近在调试 RT-Smart 上的用户态 mq&#xff08;消息队列&#xff09;时&#xff0c;遇到一个奇怪的问题&#xff0c;这个例程打印了一下获取的时间&#xff0c;就可以正常的工作&#xff08;超时退出&#xff09;&#xff0c;否则&#xff0c;就一直卡住&#xff08;无法…

10.27~10.29数电第三次实验分析与问题

实验要求 分析 寄存器 D触发器有两个输出口&#xff0c;一个输入口&#xff0c;一个时钟信号&#xff0c;一个复位信号 同步异步就是说复位信号在不在always里 给它加一个load就成了一位寄存器&#xff0c; 寄存器堆 8个8位的寄存器堆&#xff0c;每个寄存器都有两读一写…

Zynq-Linux移植学习笔记之64- 国产ZYNQ在linux下配置国产5396芯片

1、背景介绍 复旦微ZYNQ通过SPI配置国产JEM5396&#xff0c;框图如下&#xff1a; 现在需要在linux下的应用程序内配置JEM5396的寄存器。其中FMQL和进口的XILINX ZYNQ类似&#xff0c;JEM5396和进口的BCM5396兼容。因此可以参考进口ZYNQ在linux下配置BCM5396过程。Zynq-Linux移…

【Java 进阶篇】Java HTTP响应消息详解

在Web开发中&#xff0c;HTTP&#xff08;Hypertext Transfer Protocol&#xff09;是一种用于传输数据的协议&#xff0c;它用于浏览器和Web服务器之间的通信。当你在浏览器中访问一个网页时&#xff0c;浏览器向Web服务器发送HTTP请求&#xff0c;然后Web服务器返回HTTP响应。…