【Linux学习】线程互斥与同步

目录

二十.线程互斥

        20.1 什么是线程互斥?

        20.2 为什么需要线程互斥?

        20.3 互斥锁mutex

        20.4 互斥量的接口

        20.4.1 互斥量初始

        20.4.2 互斥量销毁

        20.4.3 互斥量加锁

        20.4.4 互斥量解锁

        20.4.5 互斥量的基本原理

        20.4.6 带上互斥锁后的抢票程序

        20.5 死锁问题

        死锁的四个必要条件

        如何避免死锁

        20.6 互斥量的实现机制

二十一.线程同步

        21.1 同步概念与竞态条件

        21.2 条件变量

        21.2.1 条件变量初始

        21.2.2 条件变量销毁

        21.2.3 等待满足

        21.2.3 唤醒等待

        21.3 利用条件变量实现线程同步 

        21.4 为什么pthread_cond_wait需要互斥量?

        21.5 条件变量使用规范


二十.线程互斥

        20.1 什么是线程互斥?

线程互斥是一种同步机制,用于控制对共享资源的访问,以确保在任意给定的时刻只有一个线程可以访问该资源。在多线程编程中,当多个线程同时竞争访问某个共享资源时,如果没有适当的同步机制,可能会导致竞争条件和数据不一致性的问题。线程互斥通过引入互斥锁等机制,使得在任意时刻只能有一个线程持有资源的访问权限,从而避免了竞争条件和数据不一致性的发生。

进程线程间的互斥相关背景概念

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

        20.2 为什么需要线程互斥?

这里我们直接举个栗子来回答这个问题,我们用代码来模拟一个抢票机制,这里的所定义的票数tickets就是所谓的临界资源,这里我们一共创建5个线程来模拟抢票程序,并不断打印时时监测抢票过程

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;int tickets = 1000;//定义一个全局变量,这就是临界资源,1000张票  void* ThreadRotinue(void* args)  
{  int id = *(int*)args;  delete (int*)args;while(true){  if(tickets > 0){  usleep(10000); //usleep函数能把线程挂起一段时间, 单位是微秒(千分之一毫秒)。 printf("线程[%d] 抢票:%d\n", id, tickets);  tickets--;  //抢票,票数递减}  else{  break;  }  }                                                                                                                                                                 
}                                                                                                                                                 int main()                                                                                                                                        
{                                                                                                                                                 pthread_t tid[5];for(int i = 0; i < 5; i++)//主线程创建出5个线程去抢票{                                                                                                                   int* id = new int(i);                                                                                            pthread_create(tid + i, nullptr, ThreadRotinue, id);                                                                                        }for(int i = 0; i < 5; i++){                                                                                                                   pthread_join(tid[i], nullptr); //等待线程                                                                                                            }return 0;                                                                                                                                     
} 

运行结果如下:

 这里我们惊讶的发现,结果竟然出现了票数剩余为负数的情况!

该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及--tickets这些代码就是临界区,因为这些代码对临界资源进行了访问。

分析剩余票数出现负数的原因:

  • if语句判断条件为真以后,代码可以并发的切换到其他线程。
  • usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  • --ticket操作本身就不是一个原子操作。

为什么--ticket不是原子操作?

我们对一个变量进行--,我们实际需要进行以下三个步骤:

  1. Load(加载):将共享变量 tickets 从内存加载到寄存器中。这一步确保了线程正在使用的是最新的变量值。

  2. Update(更新):在寄存器中执行减 1 操作。这意味着对寄存器中的值进行修改,而不是直接在内存中进行修改,以确保线程独占了这个操作。

  3. Store(存储):将新的值从寄存器写回到共享变量 tickets 的内存地址。这样可以确保其他线程在需要访问该变量时,能够获取到更新后的值。

--操作对应的汇编代码如下 :

这个过程我们可以用下面几个图片形象表示 :

1.现在有两个线程thread1和thread2,thread1处于运行中、thread2等待中

当thread1把tickets的值读进CPU由于时间片耗尽被切走了,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的1000叫做thread1的上下文信息,因此需要被保存起来,之后thread1就被挂起了,放到了等待队列。(也就是说thread1只进行了Load(加载)操作)

2.此时thread2被调度了,thread1处于等待中

此时thread2被调度了,由于thread1只进行了 Load(加载),此时thread2此时看到tickets的值还是1000,有可能系统给thread2的时间片较多,导致thread2一次性执行了100次完整的 --操作才被切走,最终tickets由1000减到了900。

3.thread2时间片耗尽被切走了,切到thread1带着上下文信息恢复

此时thread2时间片到了被挂起了,又切换到了thread1,它就带着上下文过来恢复,而他的上下文记录到它还处于刚刚完成对ticket的Load(加载)操作,此时寄存器中load的ticket值仍然是1000,这时它接着完成了Update(更新)操作,也就是对1000减到999,最后再然后再Store(存储)操作将更新的ticket写回到内存中,此时内存中的值又由900变成了999


为了解决这个问题,这里我们引入互斥锁mutex的概念

        20.3 互斥锁mutex

  • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。
  • 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。
  • 多个线程并发的操作共享变量,就会带来一些问题。

要解决上述抢票系统的问题,需要做到三点:

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

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

        20.4 互斥量的接口

20.4.1 互斥量初始

在使用互斥量之前,需要对其进行初始化。一般使用 pthread_mutex_init 函数进行初始化,其原型如下:

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

参数说明:

  • mutex:需要初始化的互斥量。
  • attr:初始化互斥量的属性,一般设置为NULL即可。

返回值说明:

  • 互斥量初始化成功返回0,失败返回错误码。

调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

20.4.2 互斥量销毁

在不再需要使用互斥量时,需要将其销毁以释放资源。一般使用 pthread_mutex_destroy 函数进行销毁,其原型如下:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要销毁的互斥量。

返回值说明:

  • 互斥量销毁成功返回0,失败返回错误码。

销毁互斥量需要注意:

  • 使用PTHREAD_MUTEX_INITIALIZER初始化(静态分配)的互斥量不需要销毁。
  • 不要销毁一个已经加锁的互斥量。
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

20.4.3 互斥量加锁

当线程需要访问临界资源时,需要先对互斥量加锁,以确保只有一个线程能够进入临界区。一般使用 pthread_mutex_lock 函数进行加锁,其原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数说明:

  • mutex:要加锁的互斥量

返回值说明:

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

 调用pthread_mutex_lock时,可能会遇到以下情况:

  1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
  2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

20.4.4 互斥量解锁

当线程访问完临界资源后,需要对互斥量解锁,以允许其他线程进入临界区。一般使用 pthread_mutex_unlock 函数进行解锁,其原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数说明:

  • mutex:需要解锁的互斥量。

返回值说明:

  • 互斥量解锁成功返回0,失败返回错误码。

20.4.5 互斥量的基本原理

  • 互斥量的初始化与销毁: 在使用互斥量之前,需要对其进行初始化,一般通过 pthread_mutex_init 函数实现。销毁互斥量时使用 pthread_mutex_destroy 函数。这些操作确保互斥量的正确性和可用性。

  • 互斥量的加锁与解锁: 当线程需要访问临界资源时,首先需要对互斥量进行加锁,以确保只有一个线程能够进入临界区。加锁使用 pthread_mutex_lock 函数,解锁则使用 pthread_mutex_unlock 函数。这些操作保证了临界资源的独占性。

引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了

此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

临界区内的线程可能进行线程切换吗?

临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

20.4.6 带上互斥锁后的抢票程序

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;int tickets = 1000; // 定义一个全局变量,这就是临界资源,1000张票
pthread_mutex_t mutex; // 定义互斥锁void* ThreadRotinue(void* args)
{int id = *(int*)args;delete (int*)args;while(true){pthread_mutex_lock(&mutex); // 加锁if(tickets > 0){usleep(10000); // usleep函数能把线程挂起一段时间,单位是微秒(千分之一毫秒)。printf("线程[%d] 抢票:%d\n", id, tickets);tickets--; // 抢票,票数递减}else{pthread_mutex_unlock(&mutex); // 解锁break;}pthread_mutex_unlock(&mutex); // 解锁}
}int main()
{pthread_t tid[5];pthread_mutex_init(&mutex, NULL); // 初始化互斥锁for(int i = 0; i < 5; i++) // 主线程创建出5个线程去抢票{int* id = new int(i);pthread_create(tid + i, nullptr, ThreadRotinue, id);}for(int i = 0; i < 5; i++){pthread_join(tid[i], nullptr); // 等待线程}pthread_mutex_destroy(&mutex); // 销毁互斥锁return 0;
}

演示效果: 

        20.5 死锁问题

死锁是多线程编程中常见的问题,指的是两个或多个线程相互等待对方持有的资源,导致所有线程都无法继续执行的状态。 在使用互斥锁时,如果不注意锁的加锁顺序,就容易导致死锁问题。

这里我们举一个经典的死锁例子:

#include <iostream>
#include <thread>
#include <mutex>using namespace std;mutex mutex1;
mutex mutex2;void threadFunction1() {lock_guard<mutex> lock1(mutex1);this_thread::sleep_for(chrono::milliseconds(100));lock_guard<mutex> lock2(mutex2);cout << "Thread 1 acquired mutex1 and mutex2" << endl;
}void threadFunction2() {lock_guard<mutex> lock2(mutex2);this_thread::sleep_for(chrono::milliseconds(100));lock_guard<mutex> lock1(mutex1);cout << "Thread 2 acquired mutex2 and mutex1" << endl;
}int main() {thread t1(threadFunction1);thread t2(threadFunction2);t1.join();t2.join();return 0;
}

运行代码后:

可以观察到,此时程序就处于一个被阻塞的状态 

用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。

  • 在这个示例中,有两个线程,每个线程都试图先锁定 mutex1,然后再锁定 mutex2。当一个线程已经锁定了 mutex1,而另一个线程已经锁定了 mutex2,那么它们都会等待对方释放对方所持有的互斥量。这种情况下就可能发生死锁。
  • 例如,线程1获得了 mutex1 的锁,然后暂停,线程2获得了 mutex2 的锁,然后暂停。接下来,线程1试图获取 mutex2 的锁,但由于线程2已经持有了 mutex2 的锁,因此线程1会被阻塞。同样的,线程2也试图获取 mutex1 的锁,但由于线程1已经持有了 mutex1 的锁,因此线程2也会被阻塞。这样,两个线程就会相互等待,导致死锁。
  • 为了避免这种死锁,我们应该保持一致的锁定顺序。例如,可以约定所有线程都先锁定 mutex1,然后再锁定 mutex2。

死锁的四个必要条件

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

注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

如何避免死锁

  1. 加锁顺序:对多个互斥量加锁时,保持一致的加锁顺序,避免不同线程以不同的顺序加锁而导致死锁。
  2. 加锁时间:尽量减小临界区的范围,在持有锁的时间内,尽快完成对资源的操作。
  3. 超时机制:在获取锁的时候设置超时,如果超过一定时间仍未获得锁,则放弃获取资源。
  4. 避免嵌套锁:尽量避免在一个互斥区域内再次申请其他锁,以免造成死锁。

除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。

        20.6 互斥量的实现机制

锁是否需要被保护?

我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

如何实现申请锁的过程是原子的?

  • 上面我们已经说明了--++操作不是原子操作,可能会导致数据不一致问题。
  • 要保证申请锁的过程是原子的,通常使用底层的硬件指令来实现。大多数体系结构提供了一种原子交换指令,如 xchg 或 exchange 指令。这些指令可以在一个操作中完成寄存器和内存单元之间的数据交换,保证了这个操作的原子性。因此,申请锁的过程可以通过这些原子交换指令来实现,确保在任何时候只有一个线程能够成功地获取锁。

下面我们来看看lock和unlock的伪代码:

%al是一个cpu上的寄存器,xchgb是交换指令

我们创建锁,本质是在内存上创建一个变量,初始化锁是将锁的初始化为一个非0的值。

例如,此时内存中mutex的值为1,thread1申请锁时先将al寄存器中的值设为0,然后将al寄存器中的值与内存中mutex的值进行交换。

交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。

而此后的thread2若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁

二十一.线程同步

        21.1 同步概念与竞态条件

  • 同步概念:同步是指在多线程环境下,协调不同线程之间的执行顺序和操作,以确保它们能够按照预期的顺序执行和相互协作。在多线程编程中,同步用于解决竞争条件和数据一致性的问题,确保线程之间的协作正确可靠。
  • 竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。

举个生活例子:

 同步就是操作过程中必须要有先后,比如妈妈做完饭后,儿子才能开始吃饭。一家人到齐后才能吃饭。


  • 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。
  • 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。
  • 现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
  • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

        21.2 条件变量

条件变量是一种线程同步机制,用于在多个线程之间进行协调和通信。条件变量通常与互斥锁结合使用,用于等待某个条件的发生。当条件不满足时,线程可以调用条件变量的等待操作来等待条件的发生,同时释放互斥锁,让其他线程能够进入临界区。当条件满足时,线程可以调用条件变量的通知操作来通知等待的线程条件已经满足,从而唤醒等待的线程继续执行。

21.2.1 条件变量初始

初始化分为两种:

//动态分配
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
//静态分配
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

参数说明:

  • cond:需要初始化的条件变量。
  • attr:初始化条件变量的属性,一般设置为NULL。

返回值说明:

  • 成功返回0,失败返回错误码。

21.2.2 条件变量销毁

条件变量的销毁可以使用 pthread_cond_destroy 函数,用于释放条件变量占用的资源。

int pthread_cond_destroy(pthread_cond_t *cond);

参数说明:

  • cond:需要销毁的条件变量。

返回值说明:

  • 成功返回0,失败返回错误码。

使用静态分配初始化的条件变量不需要销毁;

21.2.3 等待满足

线程可以使用条件变量的等待操作来等待条件的发生。等待操作通常与互斥锁一起使用,以确保等待操作的原子性。

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

参数说明:

  • cond:需要等待的条件变量。
  • mutex:当前线程所处临界区对应的互斥锁。

返回值说明:

  • 成功返回0,失败返回错误码。

21.2.3 唤醒等待

条件变量的通知操作用于唤醒等待条件的线程。有两种通知方式:

唤醒单个等待线程:使用 pthread_cond_signal 函数。

int pthread_cond_signal(pthread_cond_t *cond);

唤醒全部等待线程:使用 pthread_cond_broadcast 函数。

int pthread_cond_broadcast(pthread_cond_t *cond);

参数说明:

  • cond:唤醒在cond条件变量下等待的线程。

返回值说明:

  • 函数调用成功返回0,失败返回错误码。

        21.3 利用条件变量实现线程同步 

#include <iostream>    
#include <pthread.h>    
#include <string>    
#include <unistd.h>    using namespace std;    #define NUM 5    pthread_mutex_t mtx;    
pthread_cond_t cond;    
int tickets = 100;void* producer(void* args)    
{    string name = (char*)args;    while (tickets > 0)    {       pthread_mutex_lock(&mtx); // 加锁// 生产者线程在唤醒消费者线程之前修改共享资源pthread_cond_signal(&cond);//1.唤醒在条件变量下一个线程  pthread_mutex_unlock(&mtx); // 解锁sleep(1);    }    
}    void* buyer(void* args)    
{    int id = *(int*)args;    delete (int*)args;    pthread_mutex_lock(&mtx); // 加锁while (tickets > 0) {// 消费者线程在循环中等待条件变量的信号pthread_cond_wait(&cond, &mtx); // 等待唤醒if (tickets > 0) {cout << "线程[" << id << "] 抢到票:" << tickets << endl;tickets--; // 抢票,票数递减}}pthread_mutex_unlock(&mtx); // 解锁return NULL;
}int main()
{pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t master; // 创建生产者线程pthread_t worker[NUM]; // 创建消费者线程数组pthread_create(&master, nullptr, producer, (void*)"boss");for(int i = 0; i < NUM; i++){int* num = new int(i);pthread_create(worker + i, nullptr, buyer, (void*)num);}for(int i = 0; i < NUM; i++){pthread_join(worker[i], nullptr);}pthread_join(master, nullptr);pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}                                                                                                                                                                  

此时我们会发现这五个线程时具有明显的顺序性,这是因为这5个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的第一个线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行等待,所以我们能够看到一个轮换的现象。 这样就实现了线程的同步

        21.4 为什么pthread_cond_wait需要互斥量?

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
  • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
  • 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
  • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
  • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

//错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false){pthread_mutex_unlock(&mutex);//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过pthread_cond_wait(&cond);pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

调用解锁之后,在调用pthread_cond_wait函数之前,如果已经有其他线程获取到互斥量,发现此时条件满足,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号,最终可能会导致线程永远不会被唤醒

        21.5 条件变量使用规范

等待条件变量的代码

pthread_mutex_lock(&mutex);
while (条件为假)pthread_cond_wait(&cond, &mutex);
修改条件
pthread_mutex_unlock(&mutex);

唤醒等待线程的代码

pthread_mutex_lock(&mutex);
//设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

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

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

相关文章

React18原理: 核心包结构与两大工作循环

React核心包结构 1 ) react react基础包&#xff0c;只提供定义 react组件(ReactElement)的必要函数一般来说需要和渲染器(react-dom,react-native)一同使用在编写react应用的代码时, 大部分都是调用此包的api比如, 我们定义组件的时候&#xff0c;就是它提供的class Demo ext…

[VulnHub靶机渗透] Nyx

&#x1f36c; 博主介绍&#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 hacker-routing &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【应急响应】 【python】 【VulnHub靶场复现】【面试分析】 &#x1f389;点赞➕评论➕收藏…

2月3日作业

1.编程实现单向循环链表的头插&#xff0c;头删、尾插、尾删 尾插/头插&#xff0c;头删&#xff0c;尾删&#xff1a; 头文件&#xff1a; #ifndef __HEAD_H_ #define __HEAD_H_#include<stdio.h> #include<string.h> #include<stdlib.h>enum {FALSE-1,SU…

Spring Cloud Gateway 网关路由

一、路由断言 路由断言就是判断路由转发的规则 二、路由过滤器 1. 路由过滤器可以实现对网关请求的处理&#xff0c;可以使用 Gateway 提供的&#xff0c;也可以自定义过滤器 2. 路由过滤器 GatewayFilter&#xff08;默认不生效&#xff0c;只有配置到路由后才会生效&#x…

浅谈进制的转换

本文创作灵感来自CSDN咸鱼WCY 的 咸鱼小白学嵌入式之C语言&#xff08;2.进制&#xff09; 博主更完就没更了&#xff0c;决定书接上回&#xff08;喜 进制是个啥 要理解进制&#xff0c;首先哈&#xff0c;咱得知道不同进制的含义 说到底&#xff0c;各个进制其实有点像在…

学生公寓|基于Springboot的学生公寓管理系统设计与实现(源码+数据库+文档)

学生公寓管理系统目录 目录 基于Springboot的学生公寓管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、宿舍列表 2、宿舍公告信息管理 3、宿舍公告类型管理 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八…

学生成绩管理系统|基于Springboot的学生成绩管理系统设计与实现(源码+数据库+文档)

学生成绩管理系统目录 目录 基于Springboot的学生成绩管理系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员功能模块 2、学生功能模块 3、教师功能模块 四、数据库设计 1、实体ER图 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源…

AI引领低代码革命:未来应用开发的新主流

距离ChatGPT发布已经过去快一年时间。 在这一年里&#xff0c;以ChatGPT为代表的自然语言处理领域的重大进步&#xff0c;为我们的对话系统和语言交流提供了更加智能和自然的体验。随着ChatGPT的应用不断扩大&#xff0c;人们开始认识到人工智能&#xff08;AI&#xff09;技术…

elasticsearch增删改查

一、数据类型 1、字符串类型 &#xff08;1&#xff09;text &#xff08;2&#xff09;keyword 2、数值类型 &#xff08;1&#xff09;long、integer、short、byte、float、double 3、日期类型 &#xff08;1&#xff09;date 4、布尔类型 &#xff08;1&#xff0…

【AI绘图】初见·小白入门stable diffusion的初体验

首先&#xff0c;感谢赛博菩萨秋葉aaaki的整合包 上手 stable diffusion还是挺好上手的&#xff08;如果使用整合包的话&#xff09;&#xff0c;看看界面功能介绍简单写几个prompt就能生成图片了。 尝试 我在网上找了一张赛博朋克边缘行者Lucy的cos图&#xff0c;可能会侵…

开发自定义标记应用程序

开发自定义标记应用程序 问题陈述 Larry Williams 是ABC Inc.公司的CEO,他希望公司能够拥有一个交互式网站以向访问网站的用户表示问候并显示当前时间。他还希望最终用户能够指定主页的背景颜色。您是公司的网站管理员。Larry要您修改网站的主页,以便向最终用户显示自定义问…

鸿蒙小案例-你画我猜

鸿蒙小案例-你画我猜 1.准备组件(组件布局) 2.实现跟随鼠标画笔画出图案功能 3.实现复制上面的画笔的图案功能 4.其他小功能1.组件的准备 画布的组件官方给的API是Canvas&#xff0c;需要传递一个参数CanvasRenderingContext2D 直接搜索API 使用官方案例 private settings: …

【蓝桥杯Python】试题 算法训练 藏匿的刺客

资源限制 内存限制&#xff1a;256.0MB C/C时间限制&#xff1a;1.0s Java时间限制&#xff1a;3.0s Python时间限制&#xff1a;5.0s 问题描述 强大的kAc建立了强大的帝国&#xff0c;但人民深受其学霸及23文化的压迫&#xff0c;于是勇敢的鹏决心反抗。   kAc帝国防守…

书生谱语-基于 InternLM 和 LangChain 搭建知识库

大语言模型与外挂知识库&#xff08;RAG&#xff09;的优缺点 RAG方案构建与优化 作业 在创建web_demo时&#xff0c;需要根据教程将服务器端口映射到本地端口&#xff0c;另外需要将链接的demo从服务器中复制出来&#xff0c;不要直接从服务器打开demo页面&#xff0c;不然会…

分布式事务详解

概述 随着互联网的发展&#xff0c;软件系统由原来的单体应用转变为分布式应用。分布式系统把一个单体应用拆分为可独立部署的多个服务&#xff0c;因此需要服务与服务之间远程协作才能完成事务操作。这种分布式系统下不同服务之间通过远程协作完成的事务称之为分布式事务&…

JavaScript中有哪些不同的数据类型

在 JavaScript 中&#xff0c;数据类型是一种用来表示数据的分类&#xff0c;它决定了我们可以对这个数据类型执行哪些操作。在 JavaScript 中有以下几种不同的数据类型&#xff1a; 基本数据类型 字符串 (String)&#xff1a;表示一组字符&#xff0c;可以使用引号&#xff08…

ElasticSearch级查询Query DSL上

目录 ES高级查询Query DSL match_all 返回源数据_source 返回指定条数size 分页查询from&size 指定字段排序sort 术语级别查询 Term query术语查询 Terms Query多术语查询 exists query ids query range query范围查询 prefix query前缀查询 wildcard query通…

CVE-2022-25487 漏洞复现

漏洞描述&#xff1a;Atom CMS 2.0版本存在远程代码执行漏洞&#xff0c;该漏洞源于/admin/uploads.php 未能正确过滤构造代码段的特殊元素。攻击者可利用该漏洞导致任意代码执行。 其实这就是一个文件上传漏洞罢了。。。。 打开之后&#xff0c;/home路由是个空白 信息搜集&…

controller-manager学习三部曲之三:deployment的controller启动分析

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 《controller-manager学习三部曲》完整链接 通过脚本文件寻找程序入口源码学习deployment的controller启动分析 本篇概览 本文是《controller-manager学习三…

深入了解JavaScript混淆工具:jsjiami.v6

JavaScript混淆工具在前端开发中发挥着重要的作用&#xff0c;帮助开发者保护源代码&#xff0c;减少代码被轻易破解的风险。其中&#xff0c;jsjiami.v6 是一款备受开发者关注的混淆工具之一。本文将深入介绍jsjiami.v6的基本原理和使用方法&#xff0c;并通过案例代码演示其效…