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

【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);

相关文章:

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

目录 二十.线程互斥 20.1 什么是线程互斥&#xff1f; 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 死锁问题 死锁…...

前端开发:(三)CSS入门

1. 介绍CSS 1.1 什么是CSS CSS&#xff08;Cascading Style Sheets&#xff09;是一种用于描述文档样式和布局的样式表语言&#xff0c;用于美化和排版HTML和XML等标记语言的内容。 1.2 CSS的作用和优势 CSS的主要作用是控制网页的样式和布局&#xff0c;包括字体、颜色、间…...

一周学会Django5 Python Web开发-Django5创建项目(用PyCharm工具)

锋哥原创的Python Web开发 Django5视频教程&#xff1a; 2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili2024版 Django5 Python web开发 视频教程(无废话版) 玩命更新中~共计11条视频&#xff0c;包括&#xff1a;2024版 Django5 Python we…...

寒假学习记录13:JS对象

目录 对象转数组 对象双值转数组 Object.entries() &#xff08;转为二维数组&#xff09;&#xff08;属性的值和键&#xff09; 对象右值转数组 Object.values() &#xff08;属性的值&#xff09; 对象左值转数组 Object.keys() (属性的键) 对象左值转…...

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

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

C#向数组指定索引位置插入新的元素值:自定义插入方法 vs List<T>.Add(T) 方法

目录 一、使用的方法 1.自定义插入方法 2.使用List.Add(T) 方法 二、实例 1.示例1&#xff1a;List.Add(T) 方法 2.示例&#xff1a;自定义插入方法 一、使用的方法 1.自定义插入方法 首先需要定义一个一维数组&#xff0c;然后修改数组的长度(这里使用Length属性获取…...

【大数据Hive】hive 表设计常用优化策略

目录 一、前言 二、hive 普通表查询原理 2.1 操作演示说明 2.1.1 创建一张表&#xff0c;并加载数据 2.1.2 统计3月24号的登录人数 2.1.3 查询原理过程总结 2.2 普通表结构带来的问题 三、hive分区表设计 3.1 区表结构 - 分区设计思想 3.2 操作演示 3.2.1 创建分区表…...

jvm垃圾收集器之七种武器

目录 1.回收算法 1.1 标记-清除算法(Mark-Sweep) 1.2 复制算法(Copying) 1.3 标记-整理算法(Mark-Compact) 2.HotSpot虚拟机的垃圾收集器 2.1 新生代的收集器 Serial 收集器&#xff08;复制算法&#xff09; ParNew 收集器 (复制算法) Parallel Scavenge 收集器 (复制…...

STM32面试相关问题

STM32面试相关问题&#xff1a; STM32的内核型号&#xff0c;主频&#xff0c;传感器和单片机总线类型&#xff0c;IIC,SPI,RS485UART数据帧项目中一些参数的设置 STM32 系统移植 ARM编译 常用的驱动编写方式 自己写过哪些方面驱动 其实如果问32的问题&#xff0c…...

风行智能电视N39S、N40 强制刷机升级方法,附刷机升级数据MstarUpgrade.bin

升级步骤&#xff1a; 1、下载刷机数据&#xff0c;如是压缩包&#xff0c;需要先解压&#xff0c;然后将刷机bin格式的文件重命名为MstarUpgrade.bin 2、将此文件放到U盘根目录 &#xff08;U盘格式FAT32&#xff0c;单分区&#xff0c;建议4G的优盘刷机成功率高&#xff09;…...

【C语言】简易英语词典

文章目录 一、定义英语单词信息的结构体二、主函数功能逻辑三、查单词函数四、背单词函数五、补充 一、定义英语单词信息的结构体 添加必要的头文件、宏定义和声明&#xff0c;之后定义英语单词信息结构体。 /* 头文件和宏定义 */ #include <stdio.h> #include <std…...

【算法题】104. 二叉树的最大深度

题目 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3 示例 2&#xff1a; 输入&#xff1a;root [1,nul…...

Docker配置Portainer容器管理界面

目录 一、Portainer 简介 优点&#xff1a; 缺点&#xff1a; 二、环境配置 1. 拉取镜像 2. 创建启动容器 三、操作测试 1. 进入容器 2. 拉取镜像并部署 3. 访问测试 一、Portainer 简介 Portainer 是一个开源的轻量级容器管理界面&#xff0c;用于管理 Docker 容器…...

Linux network namespace 访问外网以及多命名空间通信(经典容器组网 veth pair + bridge 模式认知)

写在前面 整理K8s网络相关笔记博文内容涉及 Linux network namespace 访问外网方案 Demo实际上也就是 经典容器组网 veth pair bridge 模式理解不足小伙伴帮忙指正 不必太纠结于当下&#xff0c;也不必太忧虑未来&#xff0c;当你经历过一些事情的时候&#xff0c;眼前的风景已…...

网络渗透测试:Wireshark抓取qq图片

Wireshark Wireshark Downloadhttps://www.wireshark.org/download.html 简介 WireShark是非常流行的网络封包分析工具&#xff0c;可以截取各种网络数据包&#xff0c;并显示数据包详细信息。常用于开发测试过程中各种问题定位。本文主要内容包括&#xff1a; 1、Wireshar…...

网络协议与攻击模拟_16HTTP协议

1、HTTP协议结构 2、在Windows server去搭建web扫描器 3、分析HTTP协议流量 一、HTTP协议 1、概念 HTTP&#xff08;超文本传输协议&#xff09;用于在万维网服务器上传输超文本&#xff08;HTML&#xff09;到本地浏览器的传输协议 基于TCP/IP(HTML文件、图片、查询结构等&…...

叙事弧基础

原文&#xff1a;MasterClass. 2020. Learn About Narrative Arcs: Definition, Examples, and How to Create a Narrative Arc in Your Writing - 2021. https://www.masterclass.com/articles/what-are-the-elements-of-a-narrative-arc-and-how-do-you-create-one-in-writin…...

python从入门到精通(二十):python的exe程序打包制作

python的exe程序打包制作 python打包的概念python打包的模块导入模块安装验证基本语法命令参数文件夹模式单文件模式资源嵌入exe更改图标启动画面&#xff08;闪屏&#xff09;禁用异常提示 python打包的概念 将普通的*.py程序文件打包成exe文件。exe文件即可执行文件&#xf…...

three.js 细一万倍教程 从入门到精通(一)

目录 一、three.js开发环境搭建 1.1、使用parcel搭建开发环境 1.2、使用three.js渲染第一个场景和物体 1.3、轨道控制器查看物体 二、three.js辅助设置 2.1、添加坐标轴辅助器 2.2、设置物体移动 2.3、物体的缩放与旋转 缩放 旋转 2.4、应用requestAnimationFrame …...

电路设计(16)——纪念馆游客进出自动计数显示器proteus仿真

1.设计要求 设计、制作一个纪念馆游客进出自动计数显示器。 某县&#xff0c;有一个免费参观的“陶渊明故里纪念馆”&#xff0c;游客进出分道而行&#xff0c;如同地铁有确保单向通行的措施。在入口与出口处分别设有红外检测、声响、累加计数器装置&#xff0c;当游人进&#…...

Python数学建模之回归分析

1.基本概念及应用场景 回归分析是一种预测性的建模技术&#xff0c;数学建模中常用回归分析技术寻找存在相关关系的变量间的数学表达式&#xff0c;并进行统计推断。例如&#xff0c;司机的鲁莽驾驶与交通事故的数量之间的关系就可以用回归分析研究。回归分析根据变量的…...

单片机学习笔记---DS18B20温度传感器

目录 DS18B20介绍 模拟温度传感器的基本结构 数字温度传感器的应用 引脚及应用电路 DS18B20的原理图 DS18B20内部结构框图 暂存器内部 单总线介绍 单总线电路规范 单总线时序结构 初始化 发送一位 发送一个字节 接收一位 接收一个字节 DS18B20操作流程 指令介…...

【网络】WireShark过滤 | WireShark实现TCP三次握手和四次挥手

目录 一、开启WireShark的大门 1.1 WireShark简介 1.2 常用的Wireshark过滤方式 二、如何抓包搜索关键字 2.1 协议过滤 2.2 IP过滤 ​编辑 2.3 过滤端口 2.4 过滤MAC地址 2.5 过滤包长度 2.6 HTTP模式过滤 三、ARP协议分析 四、WireShark之ICMP协议 五、TCP三次握…...

开源免费的Linux服务器管理面板分享

开源免费的Linux服务器管理面板分享 一、1Panel1.1 1Panel 简介1.2 1Panel特点1.3 1Panel面板首页1.4 1Panel使用体验 二、webmin2.1 webmin简介2.2 webmin特点2.3 webmin首页2.4 webmin使用体验 三、Cockpit3.1 Cockpit简介3.2 Cockpit特点3.3 Cockpit首页3.4 Cockpit使用体验…...

leetcode算法-位运算

位运算&#xff0c;直接在二进制上进行的按位操作&#xff0c;位运算的种类如下&#xff1a; 1.按位异或^:异或的含义是操作的两位不同&#xff0c;则结果为1&#xff0c;相同则结果为0&#xff0c;所以两个相同的数异或&#xff0c;结果应该是0&#xff0c;3^3的结果是0,3^4的…...

「MySQL」约束

概述 分类 约束描述关键字非空约束限制该字段的数据不能为 nullNOT NULL唯一约束保证该字段的所有数据都是唯一、不重复的UNIQUE主键约束主键是一行数据的唯一标识&#xff0c;要求非空且唯一PRIMARY KEY默认约束保存数据时&#xff0c;如果未指定该字段的值&#xff0c;则采…...

C语言:详解操作符(下)

上一篇链接&#xff1a;C语言&#xff1a;详解操作符&#xff08;上&#xff09;摘要&#xff1a; 在上篇文章中&#xff0c;我们已经讲过位操作符等涉及二进制的操作符&#xff0c;这些有助于帮助我们后期理解数据如何在计算机中运算并存储&#xff0c;接下来本篇将更多的讲述…...

Vue3.0(六):VueX 4.x详解

Vuex4状态管理 什么是状态管理 在开发中&#xff0c;我们的应用程序需要处理各种各样的数据&#xff0c;这些数据需要保存在应用程序的某一个位置&#xff0c;对于这些数据的管理&#xff0c;就是 状态管理目前前端项目越来越复杂&#xff0c;多组件共享同一数据的状态很常见…...

突破编程_C++_面试(基础知识(13))

面试题45&#xff1a;C中的字符串如何存储 在C中&#xff0c;字符串可以通过多种方式存储&#xff0c;但最常见和推荐使用的方式是通过 std::string 类&#xff0c;该类位于 <string> 头文件中。std::string 是一个类模板的实例&#xff0c;通常用于存储字符数组&#x…...

掌握C语言文件操作:从入门到精通的完整指南!

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ &#x1f388;&#x1f388;养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; 所属专栏&#xff1a;C语言学习 贝蒂的主页&#xff1a;Betty‘s blog 1. 什么是文件 文件其实是指一组相关数据的有序集合。这个数据集有一个名称&a…...

JavaEE作业-实验二

目录 1 实验内容 2 实验要求 3 思路 4 核心代码 5 实验结果 1 实验内容 实现两个整数求和的WEB程序 2 实验要求 ①采用SpringMVC框架实现 ②数据传送到WEB界面采用JSON方式 3 思路 ①创建一个SpringMVC项目&#xff0c;配置好相关的依赖和配置文件。 ②创建一个Con…...

2月8号作业

Sqlite3系统命令 .quit 退出数据库 .exit 退出数据库 .help 显示帮助信息&#xff0c;获取所有系统命令 .table 查看当前数据库下的所有表格 .schema 查看表的结构 Sqlite3语句 创建表格&#xff1a; create table 表名 (字段名 数据类型, 字段名 数据类型); create table if…...

08:K8S资源对象管理|服务与负载均衡|Ingress

K8S资源对象管理&#xff5c;服务与负载均衡&#xff5c;Ingress DaemonSet控制器污点策略容忍容忍污点 其他资源对象Job资源对象 有限生命周期CronJob资源对象 集群服务服务自动发现headless服务 实现服务定位与查找 服务类型 Ingress插件 发布服务的方式 DaemonSet控制器 Da…...

HarmonyOS 横屏调试与真机横屏运行

我们有些程序 需要横屏才能执行出效果 我们在预览器上 点击如下图指向出 就进入一个横屏调试了 但 我们真机运行 依旧是竖着的 我们如下图 找到 module.json5 在 abilities 下面 第一个对象 最下面 加上 "orientation": "landscape"然后 我们再真机运…...

Javaweb基础-tomcat,servlet

一.配置文件基础&#xff1a; properties配置文件&#xff1a; 由键值对组成 键和值之间的符号是等号 每一行都必须顶格写&#xff0c;前面不能有空格之类的其他符号 xml配置文件&#xff1a;&#xff08;xml语法HTML语法HTML约束&#xff09;xml约束-DTD / Schema DOM4…...

HCIA-HarmonyOS设备开发认证V2.0-3.2.轻量系统内核基础-中断管理

目录 一、中断基础概念二、中断管理使用说明三、中断管理模块接口四、代码分析&#xff08;待续...&#xff09;坚持就有收获 一、中断基础概念 在程序运行过程中&#xff0c;出现需要由 CPU 立即处理的事务时&#xff0c;CPU 暂时中止当前程序的执行转而处理这个事务&#xf…...

【开源】JAVA+Vue+SpringBoot实现就医保险管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 科室档案模块2.2 医生档案模块2.3 预约挂号模块2.4 我的挂号模块 三、系统展示四、核心代码4.1 用户查询全部医生4.2 新增医生4.3 查询科室4.4 新增号源4.5 预约号源 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVue…...

Stable Diffusion 模型下载:DreamShaper XL(梦想塑造者 XL)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 DreamShaper 是一个分格多样的大模型&#xff0c;可以生成写实、原画、2.5D 等…...

【机器学习】数据清洗之处理异常点

&#x1f388;个人主页&#xff1a;甜美的江 &#x1f389;欢迎 &#x1f44d;点赞✍评论⭐收藏 &#x1f917;收录专栏&#xff1a;机器学习 &#x1f91d;希望本文对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff0c;让我们共同学习、交流进步…...

JavaScript学习之旅10------掌握jQuery:实用应用案例深度解析

目录 写在开头1. jQuery基础知识回顾1.1. 选择器1.2. 事件1.3. 效果1.4. DOM操作1.5. AJAX 2. 实用应用案例分析2.1. 动态内容加载2.2. 表单验证2.3. 图像滑动门效果2.4. 创建动态导航菜单 3. 高级技巧与最佳实践3.1. 优化jQuery代码的性能3.2. jQuery插件的使用和自定义3.3. j…...

017_逆向工程搭建和使用

文章目录 启动代码生成器然后访问第一步处理:前端代码删除逆向生成的代码中有好多东西要引入创建gulimall-common插曲:修改模块名dao层entity层service层controllerRQuery文件当中的报错☆ 调整renren-generator的逆向工程逆向生成代码当中有什么总结...

位运算+leetcode(1)

基础 1.基础知识 以下都是针对数字的二进制进行操作 >> 右移操作符<< 左移操作符~ 取反操作符 & 有0就是0&#xff0c;全一才一 | 有一才一 &#xff0c;全0才0^ 相同为0&#xff0c;相异为1 异或( ^ )运算的规律 a ^ 0 a a ^ a 0a ^ b ^ c a ^ (b …...

如何在 JavaScript 中比较两个日期 – 技术、方法和最佳实践

在 JavaScript 中&#xff0c;您可以使用 date 对象有效地处理应用程序中的日期、时间和时区。 Date 对象可帮助您有效地操作数据、处理各种与日期相关的任务&#xff0c;并在创建实际应用程序时执行一些计算。 &#xff08;本文内容参考&#xff1a;java567.com&#xff09;…...

【More Effective C++】条款17:考虑使用lazy evaluation

含义&#xff1a;将计算拖延到必须计算的时候&#xff0c;以下为4个场景 优点&#xff1a;避免不必要的计算&#xff0c;节省成本 缺点&#xff1a; 管理复杂性&#xff1a;可能会增加代码复杂性&#xff0c;特别是在多线程环境中需要正确处理同步和并发问题。性能开销&…...

深入探索Pandas读写XML文件的完整指南与实战read_xml、to_xml【第79篇—读写XML文件】

深入探索Pandas读写XML文件的完整指南与实战read_xml、to_xml XML&#xff08;eXtensible Markup Language&#xff09;是一种常见的数据交换格式&#xff0c;广泛应用于各种应用程序和领域。在数据处理中&#xff0c;Pandas是一个强大的工具&#xff0c;它提供了read_xml和to…...

如何在我们的模型中使用Beam search

在上一篇文章中我们具体探讨了Beam search的思想以及Beam search的大致工作流程。根据对Beam search的大致流程我们已经清楚了&#xff0c;在这我们来具体实现一下Beam search并应用在我们的seq2seq任务中。 1. python中的堆&#xff08;heapq&#xff09; 堆是一种特殊的树形…...

PKI - 借助Nginx 实现Https 服务端单向认证、服务端客户端双向认证

文章目录 Openssl操系统默认的CA证书的公钥位置Nginx Https 自签证书1. 生成自签名证书和私钥2. 配置 Nginx 使用 HTTPS3. 重启 Nginx 服务4. 直接访问5. 不验证证书直接访问6. 使用server.crt作为ca证书验证服务端解决方法1&#xff1a;使用 --resolve 参数进行请求域名解析解…...

WebSocket原理详解

目录 1.引言 1.1.使用HTTP不断轮询 1.2.长轮询 2.websocket 2.1.概述 2.2.websocket建立过程 2.3.抓包分析 2.4.websocket的消息格式 3.使用场景 4.总结 1.引言 平时我们打开网页&#xff0c;比如购物网站某宝。都是点一下列表商品&#xff0c;跳转一下网页就到了商品…...

在面试中如何回复擅长vue还是react

当面试官问及这个问题的时候&#xff0c;我们需要思考面试官是否是在乎你是掌握vue还是react吗&#xff1f;&#xff1f;&#xff1f; 在大前端的一个环境下&#xff0c;当前又有AI人工智能的加持辅助&#xff0c;我们是不是要去思考企业在进行前端岗位人员需求的时候&#xf…...

使用Vue.js输出一个hello world

导入vue.js <script src"https://cdn.jsdelivr.net/npm/vue2/dist/vue.js"></script> 创建一个标签 <div id"app">{{message}}</div> 接管标签内容&#xff0c;创建vue实例 <script type"text/javascript">va…...