当前位置: 首页 > news >正文

Linux线程互斥

目录

一、线程不安全

1.线程不安全现象

2.线程不安全程序的特质

3.线程不安全程序的原因

二、线程互斥

1.基本概念

2.锁

(1)认识锁

(2)互斥锁的使用

(3)代码的改造

3.锁的本质

(1)加锁对线程的影响

(2)锁的原理

4.封装锁

三、重入和线程安全的理解

1.正确认识重入

(1)认识重入

(2)认识可重入

2.正确认识线程安全

3.可重入与线程安全的联系

四、死锁

1.四个必要条件

2.避免死锁


一、线程不安全

1.线程不安全现象

我们都有在12306上抢票的经历吧,毕竟一打开满眼的候补着实是血压高了。

那我们也编写一个简单的抢票程序,设置全局变量tickets=10表示一共有十张票。创建五个线程每一个线程代表一个抢票者。五个抢票者不断抢票,抢到票后tickets减一并显示当前余票。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5int tickets = 10;class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += p->buffer;s += "Remaining tickets:";while(1){if(tickets > 0){sleep(1);--tickets;printf("%s%d\n", s.c_str(), tickets);}elsebreak;}pthread_exit(nullptr);
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "thread:%d buy ticket:",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){pthread_join(e->tid, nullptr);delete e;}return 0;
}

运行结果:

按道理说票减到0线程就都应该退出了,不应该出现余票为负的情况。

2.线程不安全程序的特质

线程不安全的程序一般都有这个特质:多个线程交叉执行,换句话说就是调度器频繁发生线程调度与切换。

虽然我们看上去所有线程都在同时执行,其实线程也是同时只能执行一个(单核),只是因为CPU运行速度太快,人是观察不到的。

对于线程切换有以下细节:

  • 线程达到时间片,更高优先级线程需要执行,线程等待时会发生线程切换。
  • 线程切换的检测是以内核态身份进行的,访问的是地址空间的内核部分,本质是操作系统在检测。
  • 线程在从内核态转为用户态时,会检测是否达到线程切换条件。

CPU负责调度这些执行流,在一个线程达到被切换的条件时,CPU就会与该线程分离并执行另一个线程,当再次轮到这个被切走的线程后才会继续执行。

如果CPU不停切换线程,一个线程执行一半就接着执行下一个去了,这样的交叉执行就会导致线程不安全的问题。

3.线程不安全程序的原因

首先,语句的执行都需要先把变量从内存读取到寄存器,然后在寄存器内进行处理,最后再覆盖到内存中。根据这样的思想我们就可以试着解释上面的票数为什么会出现负数。

我们假设ticket=1依旧有五个线程抢票。

第一阶段

首先,内存中储存的ticket为1。第一个线程thread1开始执行,判断ticket>0。CPU从内存中将tickets的数值1读取到ebx寄存器内,tickets确实大于0,将1写回内存,执行内部语句。

当线程要执行sleep时,线程1会被切走。由于CPU和寄存器从只有一套,所以它的上下文数据会被保存起来。

接着,第二个线程thread2同样从内存中读取到tickets为1,判断为真,再次被切走。

当然还有thread3、thread4、thread5都会经历这样的过程。

第二阶段

我们首先要知道,--tickets需要三步才能完成,包括读取数据到寄存器,寄存器数据减一,将寄存器数据写回内存。

此时每一个线程都进入了if语句框,线程thread1再次被CPU执行。此时内存中的tickets为1,寄存器ebx读取数据变为1。此时执行--tickets,寄存器内数据变为0,最后将0写回到内存的tickets中。

thread2线程也被再次唤醒,再次读取tickets为0,减一得到-1,再将-1写回内存中。

后面的thread3、thread4、thread5也是这样的流程,最后内存中的tickets经过五次减一变成了-4,这就出现了负数。

二、线程互斥

1.基本概念

  • 临界资源:多个执行流进行安全访问的共享资源。

上面的tickets就不是临界资源,因为多线程对它的访问出现了问题。

  • 临界区:多个执行流中,访问临界资源的代码。

上面只有部分代码属于一部分临界区,对tickets进行if判断,打印,减一的那部分代码属于临界区。

  • 互斥:让多个线程串行访问共享资源,任何时候只有一个执行流在访问共享资源。

上面的代码如果将多进程交叉并行变为串行,就不会出现进程不安全的情况。让共享资源变成临界资源其实就是实现互斥。

  • 原子性:对一个资源进行访问的时候,要么不做,要么就做完。

在前面也说过像加加(++)和减减(--)这样的操作,看似只有一条代码,但是它对应的汇编指令有3条,也就是说这个操作不能一次完成。

现在的我们可以认为:对资源进行操作,如果只用一条汇编就能完成,那么就说该操作具有原子性。(这只是原子性表述的其中一种)

2.锁

(1)认识锁

要想解决多线程的数据不一致问题需要做到以下几点:

  • 代码必须有互斥行为,当一个线程进入临界区执行代码时,不允许其他线程进入该临界区。
  • 如果有多个线程同时请求执行临界区代码,并且临界区没有线程在执行代码,那么只允许一个线程进入该临界区。
  • 如果线程不在临界区中执行代码,那么该线程也不能阻止其他线程进入临界区。

其实做到上面三点只需要一把互斥锁,你可以将锁看作一个通行证,持有锁的线程才能进入临界区中执行代码,其他线程不持有锁,无法进入该临界区。

加锁本质就是让共享资源临界资源化,多个线程串行访问共享资源,从而保护共享资源的安全。

互斥锁本质上就是一个类(class pthread_mutex_t),可以构造对象pthread_mutex_t mutx,mutx就是互斥锁对象。

(2)互斥锁的使用

以下是锁的一些成员函数和使用代码:

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

头文件:pthread.h

功能:初始化互斥锁。

参数:pthread_mutex_t *restrict mutex表示需要被初始化的锁的地址,const pthread_mutexattr_t *restrict attr表示锁的属性,一般都为nullptr。

返回值:取消成功返回0,取消失败返回错误码。

int pthread_mutex_destroy(pthread_mutex_t *mutex);

头文件:pthread.h

功能:销毁互斥锁。

参数:pthread_mutex_t *mutex表示需要被销毁的锁的地址。

返回值:销毁成功返回0,失败返回错误码。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果是全局或static修饰的锁,使用上面语句初始化锁。

int pthread_mutex_lock(pthread_mutex_t *mutex);

头文件:pthread.h

功能:对lock到unlock的部分代码加锁(仅允许线程串行)。

参数:pthread_mutex_t *mutex表示需要加锁的锁指针。

返回值:加锁成功返回0,失败返回错误码。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

头文件:pthread.h

功能:标识走出lock到unlock的部分代码解锁(恢复并发)。

参数:pthread_mutex_t *mutex表示需要解锁的锁指针。

返回值:加锁成功返回0,失败返回错误码。

其实加锁和解锁可以圈定临界区的范围,临界区内的代码只允许同一时间有一个线程执行内部代码,只有该线程退出后才允许另一个线程执行该部分代码,外部的代码依旧允许并行。

我打个比方的话就像只容许一个人公共厕所,厕所只能进一个人,必须等里面的人使用完毕后另一个人才能进去,而外面的公共空间不受管制。

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(lock);
//临界区
//...
pthread_mutex_unlock(lock);

(3)代码的改造

对线程加锁需要做两件事:让所有线程都看到同一把锁,所有线程都使用同一把锁。

我们如果将锁定义在main函数内,那么只有主线程能看到该锁。所以我们在Thread类中增加一个锁指针,这时所有的线程就能使用同一把锁了。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5int tickets = 10;class pthread_data
{
public:pthread_t tid;char buffer[64];pthread_mutex_t* pmtx;//锁指针
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += p->buffer;s += "Remaining tickets:";while(1){pthread_mutex_lock(p->pmtx);//加锁if(tickets > 0){sleep(1);--tickets;pthread_mutex_unlock(p->pmtx);//解锁printf("%s%d\n", s.c_str(), tickets);//不修改临界资源,可以不包含在内}else{pthread_mutex_unlock(p->pmtx);//解锁break;}}pthread_exit(nullptr);
}int main()
{vector<pthread_data*> vpd;pthread_mutex_t mutx;//创建锁pthread_mutex_init(&mutx, nullptr);//初始化for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;pd->pmtx = &mutx;snprintf(pd->buffer, sizeof(pd->buffer), "thread%d buy ticket:",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){pthread_join(e->tid, nullptr);delete e;}return 0;
}

注意加锁和解锁的区域只需要覆盖住对临界资源进行操作的代码,不要覆盖太大。

还有一定要注意解锁一定要覆盖到代码的执行路径,比如抢票代码else中如果没有解锁,那下面的所有执行就都加锁了,会对代码运行造成巨大影响。

再次运行,程序正常运行并退出,只是由于串行,程序的运行时间加长了。

因为正常的抢票往往是很短时间内就会有许多人访问,我们将sleep(1)换成usleep(1000)缩短睡眠时间。

我们会发现,大部分的票都被一个线程抢走了。

实际上,锁只保证互斥访问,不管执行线程的顺序。

thread5抢的多,说明该线程的竞争能力强,别的线程打不过它。

现在的抢票逻辑是抢到票解锁以后,该线程又直接去申请锁,这就导致了之前持有锁的线程更加容易再次申请到锁。

咱们再12306抢票成功后也不可能立刻再去抢,程序还需要做打印订单等等工作,所以我们在线程执行最后也睡眠一会儿。

这次就是正常的你来我往了。

3.锁的本质

(1)加锁对线程的影响

锁必须让所有线程都看到,所以锁本身就是共享资源。那谁来保护锁的安全呢?

锁是通过加锁和解锁操作的原子性来保证自身的安全的。

一个线程,如果成功申请锁,那么它就会继续向下执行,如果申请不成功呢?

我们发现进程线程都还在,但线程卡住了。

一个锁只能被申请一次,只有锁被释放才能再次申请。当一个线程申请锁失败,它就会阻塞不动。

所以我们此时就能理解CPU排队处理线程和串行的关系了:

  • 当一个线程申请锁成功,进入临界区访问临界资源,其他线程要想进入临界区只能阻塞等待,等待该进程将锁释放。
  • 当一个线程申请锁成功,进入临界区访问临界资源,在满足条件时也是可以被换下CPU的。而且锁还在该线程的受力,其他线程仍然无法申请锁成功。
  • 操作系统内不存在锁的概念,所以调度器在调度轻量级进程的时候并不会考虑是否有锁。如果调度到了没有锁的进程,不进行处理就可以了。

所以站在其他线程的角度,锁只有两种状态:申请锁前和申请锁后。

站在其他线程的角度,当前持有锁的过程就是原子的。

(2)锁的原理

为了保证加锁的原子性,在大多数体系结构都提供了swap或者xchange汇编指令,保证加锁只需要一条汇编指令。

下面是加锁和解锁的伪代码(xchange是原子的):

lock:movb %al, $0//将0写入al寄存器中xchange %al, mutex//将al寄存器的内容与锁的成员变量1交换if(al寄存器的内容 > 0){return 0;}else{挂起等待;    }goto lock;unlock:movb mutex, $1唤醒等待mutex的线程;return 0;

在CPU中有一个al寄存器,它也是锁的能正常运行的保证之一。

假设有两个线程,每个线程中都有加锁的代码。

首先CPU开始处理线程thread1,thread2等待被处理。由于thread1是第一次被处理,此时需要向al寄存器内写入0。

当线程thread1执行到加锁代码时,由于内存中的锁变量储存了一个变量1,所以al寄存器会与内存中的这个变量进行数据交换。

在执行临界区代码时,很可能thread1还没有解锁,该线程就被换下去了。但是CPU和寄存器只有一套,那么上下文数据就必须保存后由线程带走,同样al寄存器里的1就也被带走了。

当thread2也是初次执行,需要在al寄存器写入0。然后同样将al寄存器会与内存中的这个变量进行数据交换,但此时锁变量也是0,0和0交换完还是0。

由于交换后al寄存器内容不大于0,所以该线程申请不到锁,只能挂起等待。

由于thread2被挂起,所以thread1再次被执行,此时它的上下文数据被加载回寄存器,al寄存器数据为1,线程继续运行。

当thread1完成了临界区代码执行就需要将al寄存器的1还回给锁变量,thread1的al寄存器重新变回0。然后,唤醒等待锁的线程thread2,thread1又被挂起。

经过上面过程的描述,我们不难发现发现:

  • 锁只能被一个线程持有,而且由于加锁是一条xchange汇编代码,操作是原子性的,也不需要担心线程切换的事情。
  • 一旦一个线程申请到锁,因为即使该线程被切走,锁还是在它的上下文数据中。所以,其他线程无法拿到锁,只能挂起等待,只有等锁被释放时才能申请。
  • 锁的工作本质上就是锁类变量中的一个标志位1在不同进程间传递的过程,只有申请到该标志位,或者说持有锁的线程才能执行。形象地说,利用锁达到线程串行类似于很多人抢一张入场券。
  • 释放锁的过程对原子性的要求不高,因为只有持有锁的线程才能释放锁,未申请到锁的线程都在挂起。

4.封装锁

锁的成员函数名普遍偏长,也不方便使用,不如我们自己将锁封装,自己使用也方便。

mutex.h

#include<pthread.h>
class mutex
{
public://构造函数mutex(pthread_mutex_t* p = nullptr):_pmutx(p){}//加锁void lock(){pthread_mutex_lock(_pmutx);}//解锁void unlock(){pthread_mutex_unlock(_pmutx);}    
private:pthread_mutex_t* _pmutx;
};class LockGuard//这个类型变量的构造和销毁就可以执行加解锁
{
public:LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p){_mutex.lock();//构造函数内加锁}~LockGuard(){_mutex.unlock();//析构函数内解锁}
private:mutex _mutex;
};

我们使用这个封装的锁修改代码。

#include<iostream>
#include"mutex.h"
#include<unistd.h>
#include<stdio.h>
#include<vector>
using namespace std;#define NUM 5pthread_mutex_t mutx = PTHREAD_MUTEX_INITIALIZER;//构建一个全局锁
int tickets = 10;class pthread_data
{
public:pthread_t tid;char buffer[64];
};void* start_routine(void* args)
{pthread_data* p = (pthread_data*)args;string s;s += p->buffer;s += "Remaining tickets:";while(1){{LockGuard lock(&mutx);//左侧的语句块标识这个lock变量的生命周期//构造函数加锁,走出代码块时,该变量的声明周期结束,执行析构函数解锁//这样的加锁模式也叫做RAII加锁if(tickets > 0){usleep(1000);--tickets;printf("%s%d\n", s.c_str(), tickets);//不修改临界资源,可以不包含在内}else{break;}}usleep(1000);}pthread_exit(nullptr);
}int main()
{vector<pthread_data*> vpd;for(int i = 0; i<NUM; ++i){pthread_data* pd = new pthread_data;snprintf(pd->buffer, sizeof(pd->buffer), "thread%d buy ticket:",i+1);pthread_create(&(pd->tid), nullptr, start_routine, (void*)pd);vpd.push_back(pd);}for(auto& e : vpd){pthread_join(e->tid, nullptr);delete e;}return 0;
}

正确运行

三、重入和线程安全的理解

1.正确认识重入

(1)认识重入

同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。

在信号部分就有重入,比如说进程执行一个函数,还没执行完就收到了一个信号,另一个执行流执行的还是这个函数。而在多线程这里就更好理解了,我们上面写的多线程代码都是重入的。

(2)认识可重入

一个函数在重入的情况下,对程序的运行过程和结果没有影响,则该函数被称为可重入函数,反之是不可重入函数。

常见的可重入情况:

  • 不使用全局变量或静态变量。
  • 不使用malloc或者new开辟出的空间。
  • 不返回静态或全局数据,所有数据都有函数的调用者提供。

常见的不可重入情况:

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

总之,函数如果使用了全局数据、静态数据和堆区的数据,就是不可重入的,反之就是可重入的。

2.正确认识线程安全

线程安全是指多个线程并发同一段代码时,会出现相同的结果。不加锁对全局变量或者静态变量操作时一般会出现线程安全问题。

常见线程安全情况:

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

常见线程不安全情况:

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

3.可重入与线程安全的联系

可重入与线程安全有以下关系:

  • 函数可重入,就是线程安全的。这样的代码没有全局或静态变量,不会产生数据不一致的问题。
  • 函数不可重入,如果多个线程并发,就有可能引发线程安全问题。对不可重入函数的全局变量需要加锁保护。
  • 如果一个函数中有不加锁保护的全局变量或静态变量,那这个函数既不可重入,多线程并发也不能保证线程安全。

可重入与线程安全的区别:

  • 可重入说的是函数的中性属性,而线程安全说的是线程并发是否会出问题。
  • 可重入函数是线程安全函数的一种,因为不存在全局或者静态变量。
  • 线程安全不一定保证函数可重入的,而可重入函数又一定是线程安全的。因为线程安全的情况可能是对全局变量等进行了加锁。
  • 由于线程安全可以通过加锁实现,所以线程安全的情况比可重入要多。

四、死锁

1.四个必要条件

死锁形成的四个必要条件:互斥、请求与保持、不剥夺、环路等待。

  • 互斥:只要用到锁就必定有互斥。
  • 请求与保持:请求指一个执行流申请其他锁,保持指不释放自己已经持有的锁。
  • 不剥夺:已经持有锁的执行流,在不主动释放锁前,不能强行剥夺它的锁。
  • 环路等待:线程A,B,C都持有一把锁,并且不释放。

下图中,线程A持有线程B的锁,线程B持有线程C的锁,线程C持有线程A的锁。这就是一个典型的环路等待,ABC都互相等待,哪个线程都不运行,构成死锁。

2.避免死锁

四个必要条件中只有第一个不能破坏,改变后三个任何一个都能避免死锁。

  • 破坏请求与等待——避免锁位释放

当一个执行流在申请另一个锁的时候,要先释放已经有的锁再申请新锁。

  • 破坏不剥夺——加锁顺序一致

注意加锁顺序不要构成环路。

  • 避免死锁的建议——资源一次性分配

临界资源尽量一次性分配好,不要分散在太多的地方加锁。

避免死锁的算法,有兴趣可以了解:

  • 死锁检测算法
  • 银行家算法

采用算法避免死锁一半都会有一个执行流专门监测其他执行流的状态,一旦发现某个执行流长时间不执行,就代替它释放锁(本质是将那个线程间传递的1再送回到共享区的锁变量中)。

总之,互斥锁虽然帮助我们实现了线程安全,但不合理使用会造成巨大的问题,所以我们再以后的代码中尽量少用互斥锁。

相关文章:

Linux线程互斥

目录 一、线程不安全 1.线程不安全现象 2.线程不安全程序的特质 3.线程不安全程序的原因 二、线程互斥 1.基本概念 2.锁 &#xff08;1&#xff09;认识锁 &#xff08;2&#xff09;互斥锁的使用 &#xff08;3&#xff09;代码的改造 3.锁的本质 &#xff08;1&a…...

【仿写spring之ioc篇】三、检查是否实现了Aware接口并且执行对应的方法

Aware接口 Aware接口中只是设置了对应的set方法&#xff0c;目前只定义了三个Aware 以BeanNameAware为例 package com.ez4sterben.spring.ioc.factory.aware;/*** bean名字清楚** author ez4sterben* date 2023/08/31*/ public interface BeanNameAware {/*** 设置beanName* …...

C++ 异常处理

C 异常​(Exception)是指在程序运行时产生的特殊情况&#xff0c;例如&#xff0c;尝试除以零的操作。异常提供了一种转移程序控制权的方式&#xff0c;异常处理涉及到三个关键字&#xff1a;try、catch、throw。 throw: 当问题出现时&#xff0c;程序会抛出一个异常。这是通过…...

OJ练习第157题——单词拆分

单词拆分 力扣链接&#xff1a;139. 单词拆分 题目描述 给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。 注意&#xff1a;不要求字典中出现的单词全部都使用&#xff0c;并且字典中的单词可以重复使用。 示例 …...

若依tab-content面板失效、使用load的解决方法(附详细步骤)

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 思路&…...

2023年03月 C/C++(五级)真题解析#中国电子学会#全国青少年软件编程等级考试

第1题&#xff1a;拼点游戏 C和S两位同学一起玩拼点游戏。有一堆白色卡牌和一堆蓝色卡牌&#xff0c;每张卡牌上写了一个整数点数。C随机抽取n张白色卡牌&#xff0c;S随机抽取n张蓝色卡牌&#xff0c;他们进行n回合拼点&#xff0c;每次两人各出一张卡牌&#xff0c;点数大者获…...

Android安卓实战项目(12)—关于身体分析,BMI计算,喝水提醒,食物卡路里计算APP【支持中英文切换】生活助手类APP(源码在文末)

Android安卓实战项目&#xff08;12&#xff09;—关于身体分析&#xff0c;BMI计算&#xff0c;喝水提醒&#xff0c;食物卡路里计算APP【支持中英文切换】生活助手类APP&#xff08;源码在文末&#x1f415;&#x1f415;&#x1f415;&#xff09; 一.项目运行介绍 B站演示…...

Hadoop 3.2.4 集群搭建详细图文教程

目录 一、集群简介 二、Hadoop 集群部署方式 三、集群安装 3.1 集群角色规划 3.2 服务器基础环境准备 3.2.1 环境初始化 3.2.2 ssh 免密登录&#xff08;在 hadoop01 上执行&#xff09; 3.2.3 各个节点上安装 JDK 1.8 环境 3.3 安装 Hadoop 3.4 Hadoop 安装包目…...

STL的学习之一

1&#xff09;STL扫盲 1&#xff09;C标准库和标准模板库是不一样的 2&#xff09;标准模板库是用泛型编程方式编写的函数或者类库; 3) SGI STL linux一般用&#xff0c;P.J.Plauger STL,visual2017 windows用 STL六大组件 : 容器&#xff0c;迭代器 STL 算法(说白了就是函数…...

如何使用Python进行数据科学实验?

使用Python进行数据科学实验通常需要以下步骤&#xff1a; 以上仅为使用Python进行数据科学实验的基本步骤&#xff0c;具体实验过程会根据具体问题和数据集的特点而有所不同。可以进一步学习和探索相关的数据科学和机器学习技术&#xff0c;以提高实验的效果和表现。 安装Pyt…...

华为数通方向HCIP-DataCom H12-821题库(拖拽题,知识点总结)

以下是我在现有题库中整理的需要重点关注的考点内容,如有遗漏小伙伴可以留言补充。...

第三课:C++实现PDF去水印

PDF去水印是一项非常复杂的任务,需要一定的计算机图形学知识和技术,也需要使用到一些专业的工具库。以下是一种可能的实现方法: 首先,需要将PDF文件解析成一系列图形元素,包括文字、矢量图形等。可以使用开源库Poppler或MuPDF来解析PDF文件。 接下来,需要判断PDF文件是否…...

实现Android分布式协同办公:将待办事件App与本地Web服务结合

AndServer AndServer 是 Android 平台的 Web Server 和 Web Framework,它基于编译时注解提供了类似 SpringMVC 的注解和功能。 Github :https://github.com/yanzhenjie/AndServer使用文档:https://yanzhenjie.com/AndServer/业务需求 实现待办事件APP本地启动Web服务,将本…...

VMware12.1.1安装Centos7

VMware12.1.1安装Centos7 1、下载相关软件 1.1 Centos7下载 官方下载链接&#xff1a; http://isoredirect.centos.org/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-1511.iso 1.2 VMware Workstation下载 VMware Workstation 12.1.1官方原版下载&#xff1a; https://dow…...

bazel构建原理

调度模型 传统构建系统有很多是基于任务的&#xff0c;例如 Ant&#xff0c;Maven&#xff0c;Gradle。用户可以自定义"任务"(Task&#xff09;&#xff0c;例如执行一段 shell 脚本。用户配置它们的依赖关系&#xff0c;构建系统则按照顺序调度。 基于 Task 的调度…...

matlab 点云的二进制形状描述子

目录 一、功能概述1、算法概述2、主要函数3、参考文献二、代码示例三、结果展示四、参数解析输入参数名称-值对应参数输出参数五、参考链接本文由CSDN点云侠原创,...

MongoDB实验——在Java应用程序中操作 MongoDB 数据

在Java应用程序中操作 MongoDB 数据 1. 启动MongoDB Shell 2. 切换到admin数据库&#xff0c;使用root账户 3.开启Eclipse&#xff0c;创建Java Project项目&#xff0c;命名为MongoJava File --> New --> Java Project 4.在MongoJava项目下新建包&#xff0c;包名为mo…...

java+springboot+mysql校园跑腿管理系统

项目介绍&#xff1a; 使用javaspringbootmysql开发的校园跑腿管理系统&#xff0c;系统包含超级管理员&#xff0c;系统管理员、用户角色&#xff0c;功能如下&#xff1a; 超级管理员&#xff1a;管理员管理&#xff1b;用户管理&#xff08;充值&#xff09;&#xff1b;任…...

ubuntu20.04 server 安装后磁盘空间只有一半的处理

这里扩展&#xff1a;/dev/mapper/ubuntu–vg-ubuntu–lv rootbook:/data# df -h Filesystem Size Used Avail Use% Mounted on udev 3.9G 0 3.9G 0% /dev tmpfs 795M 1.2M 79…...

〔017〕Stable Diffusion 之 常用模型推荐 篇

✨ 目录 🎈 模型网站🎈 仿真系列🎈 国风系列🎈 卡通动漫系列🎈 3D系列🎈 一些好用的lora模型🎈 模型网站 由于现在大模型超级多,导致每种画风的模型太多,那么如何选择最好最适合的模型,成了很多人头疼的问题由于用的大部分都是1.5的模型,所以优先下载 safete…...

多目标应用:基于多目标人工蜂鸟算法(MOAHA)的微电网多目标优化调度MATLAB

一、微网系统运行优化模型 参考文献&#xff1a; [1]李兴莘,张靖,何宇,等.基于改进粒子群算法的微电网多目标优化调度[J].电力科学与工程, 2021, 37(3):7 二、多目标人工蜂鸟算法MOAHA 多目标人工蜂鸟算法&#xff08;multi-objective artificial hummingbird algorithm&…...

【HTML5】HTML5 特性

HTML5 特性 1. 语义化标签 <header>&#xff1a;表示网页或某个区域的页眉部分&#xff0c;通常包含网站的标志、导航菜单等内容。<nav>&#xff1a;表示导航区域&#xff0c;用于包含网站的主要导航链接。<main>&#xff1a;表示网页的主要内容区域&#…...

【FreeRTOS】互斥量的使用与逐步实现

在FreeRTOS中&#xff0c;互斥量是一种用于保护共享资源的同步机制。它通过二进制信号量的方式&#xff0c;确保在任意时刻只有一个任务可以获取互斥量并访问共享资源&#xff0c;其他任务将被阻塞。使用互斥量的基本步骤包括创建互斥量、获取互斥量、访问共享资源和释放互斥量…...

Spring-Cloud-Openfeign如何传递用户信息?

用户信息传递 微服务系统中&#xff0c;前端会携带登录生成的token访问后端接口&#xff0c;请求会首先到达网关&#xff0c;网关一般会做token解析&#xff0c;然后把解析出来的用户ID放到http的请求头中继续传递给后端的微服务&#xff0c;微服务中会有拦截器来做用户信息的…...

OpenCV(十一):图像仿射变换

目录 1.图像仿射变换介绍 仿射变换&#xff1a; 仿射变换矩阵&#xff1a; 仿射变换公式&#xff1a; 2.仿射变换函数 仿射变换函数&#xff1a;warpAffine() 图像旋转&#xff1a;getRotationMatrix2D() 计算仿射变换矩阵&#xff1a;getAffineTransform() 3.demo 1.…...

多路波形发生器的控制

本次波形发生器&#xff0c;主要使用运算放大器、NE555以及一些其他的电阻电容器件来实现。整体电路图如下所示&#xff1a; 产生的三角波如下&#xff1a; 正弦波如下 方波如下&#xff1a; 运算放大器&#xff08;Operational Amplifier&#xff0c;简称OP-AMP&#xff09;是…...

[C/C++]天天酷跑超详细教程-中篇

个人主页&#xff1a;北海 &#x1f390;CSDN新晋作者 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏✨收录专栏&#xff1a;C/C&#x1f91d;希望作者的文章能对你有所帮助&#xff0c;有不足的地方请在评论区留言指正&#xff0c;大家一起学习交流&#xff01;&#x1f9…...

面试被打脸,数据结构底层都不知道么--回去等通知吧

数据结构之常见的8种数据结构&#xff1a; -数组Array -链表 Linked List -堆 heap -栈 stack -队列 Queue -树 Tree -散列表 Hash -图 Graph 数据结构-链表篇 Linklist定义&#xff1a; -是一种线性表&#xff0c;并不会按线性的顺序存储数据&#xff0c;即逻辑上相邻…...

微服务面试问题小结( 微服务、分布式、MQ、网关、zookeeper、nginx)

什么是微服务&#xff0c;单体架构的优点和缺点&#xff0c;微服务架构的优点和缺点&#xff1f; 单体架构 优点&#xff1a;架构简单&#xff0c;维护成本低缺点&#xff1a;各个模块耦合度太高&#xff0c;当对一个模块进行更新修改时&#xff0c;会影响到其他模块&#xff…...

Vue3全局变量使用

全局变量&#xff08;函数等&#xff09;可以在任意组件内访问&#xff0c;可以当组件间的传值使用。 main.js import ./assets/main.cssimport { createApp } from vue import App from ./App.vueconst app createApp(App); app.config.globalProperties.$global_id10; app.…...