【Linux】线程安全-互斥同步

文章目录

  • 线程安全问题的引入
  • 线程互斥
    • 互斥概念
    • 互斥锁
    • 互斥锁的计数器当中如何保证原子性
    • 互斥锁基础API
      • 初始化互斥锁变量函数
        • 动态初始化
        • 静态初始化
      • 加锁函数
        • 阻塞加锁
        • 非阻塞加锁
        • 带有超时时间的加锁
      • 解锁函数
      • 销毁互斥锁函数
  • 线程同步
  • 线程同步的必要性
  • 条件变量
    • 条件变量的使用原理
    • 条件变量的原理
    • 条件变量基础API
      • 初始化条件变量函数
        • 动态初始化
        • 静态初始化
      • 销毁条件变量函数
      • 等待条件变量函数
      • 唤醒条件变量函数
        • 单个唤醒
        • 广播唤醒
    • 条件变量常见问题

线程安全问题的引入

使用一个 抢票程序 演示线程安全的概念及重要性:

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 int g_tickets = 100;
W>  5 void* mythread_start(void* arg){6   while(1){7     if(g_tickets <= 0)8       break;
W>  9     printf("I am %p, I have ticket %d\n", pthread_self(),g_tickets--);10     //usleep(100);11     //--g_tickets;12   }
W> 13 }                                                                   14 int main(){     
W> 15   int i = 0;16   pthread_t tid[2];17   for(int i=0; i<2; ++i){18     int ret = pthread_create(&tid[i], NULL, mythread_start, NULL);                               19     if(ret < 0){   20       perror("pthread_create");21       return 0;                                                   22     }                                                             23     for(int i=0; i<2; ++i){    24       pthread_join(tid[i],NULL);//设置为阻塞25     }26   }27   return 0;28 }

执行结果:

在这里插入图片描述

结果分析:

我们代码的预期目标是创造两个黄牛(线程),让这两个线程去抢票,我们观察执行可以不难看出,抢票活动只有一个黄牛在参与,两外一个线程并没有参与,这是怎么回事呢?这是正常现象吗?

首先这是正常的现象和结果:我们称两个线程分贝为A和B,A首先被创建出来,A被创建出来后是不会等B创建出来再一起去抢票的,我们之前强调过,线程是被操作系统独立调度的,所以,先被创建出来的A线程就先执行了抢票程序,拿到了全部的票。

那什么样的结果算是有问题的呢?A和B抢到了同一张票,或者抢到了不合法的票(负数)

为了让两个黄牛都能抢到票,我们每次抢票结束后都让黄牛休息一下

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 int g_tickets = 100;
W>  5 void* mythread_start(void* arg){6   while(1){7     if(g_tickets <= 0)8       break;
W>  9     printf("I am %p, I have ticket %d\n", pthread_self(),g_tickets);10     usleep(100);11     --g_tickets;12   }
W> 13 }                                                                   14 int main(){     
W> 15   int i = 0;16   pthread_t tid[2];17   for(int i=0; i<2; ++i){18     int ret = pthread_create(&tid[i], NULL, mythread_start, NULL);                               19     if(ret < 0){   20       perror("pthread_create");21       return 0;                                                   22     }                                                             23     for(int i=0; i<2; ++i){    24       pthread_join(tid[i],NULL);//设置为阻塞25     }26   }27   return 0;28 }

执行结果:

在这里插入图片描述

出现了段错误,这是什么原因呢?因为我们出现了二义性,在同一时刻两个线程对同一个数据进行修改。

错误分析:

1、假设同一个程序中有两个线程:A和B,A和B同时对一个int类型的全局变量n=10在其各自的线程入口函数中对这样一个全局变量进行加加操作
2、当A拥有CPU之后,对n进行++操作是非原子性的操作,也就是说,这个操作随时可能会被打断,假设A被调度,刚把全局变量的数值10读取到CPU的寄存器中时就被切换出去了(程序计数器中保存着线程A下一步要执行的指令,上下文信息中保存寄存器的值,这两个东西是为了当线程A再次拥有CPU使用权的时候,还原当时线程A切换出去的线程现场使用的)
3、当线程A被切换出去后,这会儿,可能线程B获取了CPU时间片,被调度,B对全局变量进行了++操作,此时全局变量从10变成了11,并且回写到内存中去
4、当线程A再次拥有CPU时间片之后,恢复当时切换出去的现场,继续往下执行,由于上下文信息中保存的全局变量仍然是10,执行完++操作变成11,然后再回写到内存中去,全局变量的值仍任是11
5、虽然两个线程都对这个全局变量进行了++操作,但是从值上面来看,全局变量仅仅进行了一次++操作,这就是线程的不安全

线程互斥

互斥概念

互斥要做的事情:控制线程的访问时序,保证各个线程对共享资源的独占式访问。当多个线程能够同时访问到临界资源的时候,有可能会导致线程执行的结果产生二义性。而互斥就是要保证多个线程在访问同一个临界资源,执行临界区代码的时候 (非原子性性操作(线程可以被打断) ),控制访问时序。让一个线程独占临界资源执行完,再让另外一个独占执行。

临界资源:能被多个线程同时访问到的资源
临界区代码:访问临界资源的代码

互斥锁

互斥锁的本质就是0/1计数器,计数器的取值只能为0/1

  • 0:表示当前线程不可以获取到互斥锁,也就不能访问临界区资源
  • 1:表示当前线程可以获取到互斥锁,可以访问临界资源

需要注意的是:并不是说线程不获取互斥锁就不能访问临界资源,而是程序员需要在代码中用同一个互斥锁去约束多个线程,意思就是说,在加锁时,必须加的是同一把锁,当线程A拿到这把锁将这把锁的信号量改成0其他线程就无法访问了,但是当线程A被切换出去之后,其他线程加锁加的也是这把被A置为0的锁,只有当A访问结束将这把锁置为1其他线程才可以访问临界区资源,否则A加锁访问,B访问临界资源之前不加锁,这样也不能约束B,这就叫防君子不防小人,也就是说在每一个线程代码中要访问一个临界区资源之前要先获取锁,也就是加锁。

为了避免出现两个线程可能同时加锁成功,我们需要互斥锁本身就是原子性的

互斥锁的计数器当中如何保证原子性

为什么计数器(锁)当中的值从0变成1,或者从1变成0是原子性的呢?

为了保证互斥锁操作,大多数体系结构都提供了swap和exchange指令,该指令作用是把寄存器和内存单元的数据相交换,由于只有一条指令,只会出现执行成功和未执行两种情况,所以是原子性的

加锁的时候(将寄存器当中的值设置为0)

  • 第一种情况:计数器的值为1,说明锁空闲,没有被线程加锁
  • 交换情况:加锁成功
  • 第二种情况:计数器的值为0,说明锁忙碌,被其他线程加锁拿走(此时当前线程进入等待状态:等待其他线程访问完毕将锁打开)
  • 交换情况:加锁失败

解锁的时候(将寄存器当中的值设置为1)

  • 计数器的值为0,需要解锁, 进行一步交换
  • 交换成功:解锁成功
  • 交换失败:解锁失败

互斥锁基础API

互斥锁的相关函数主要有以下5个:

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 这些函数的第一个参数mutex都是一个pthread_mutex_t结构体指针变量,指向要操作的目标互斥锁结构体
  • 参数attr是一个pthread_mutexattr_t结构指针变量,指向互斥锁属性结构体,如果填入NULL,表示使用默认属性

初始化互斥锁变量函数

动态初始化

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

功能:初始化一个互斥锁结构体的属性

参数:

  • pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量
  • pthread_mutexattr_t* : 一个指向pthread_mutexattr_t结构的指针变量

返回值:成功返回0,失败返回errno

静态初始化

使用宏PTHREAD_MUTEX_INITIALIZER来初始化一个互斥锁结构体,实际上该宏是将互斥锁的各个字段初始化为0

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//初始化一个mutex锁

宏定义源码:

#define PTHREAD_MUTEX_INITIALIZER \
{ { 0, 0, 0, 0, 0, __PTHREAD_SPINS, { 0, 0 } } }

加锁函数

阻塞加锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

功能:给临界区代码阻塞等待加锁

参数:pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示成功;加锁失败返回errno

阻塞加锁解释:

  • 如果mutex中互斥量的值为1,则pthread_mutex_lock函数就返回0表示加锁成功
  • 如果mutex中互斥量的值为0,则线程阻塞在pthread_mutex_lock函数中,直到加锁成功

非阻塞加锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);

功能:给临界区代码非阻塞加锁

参数:pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示成功;加锁失败返回errno

非阻塞加锁解释:

  • 如果mutex中互斥量的值为1,则pthread_mutex_trylock函数就返回0表示加锁成功
  • 如果mutex中互斥量的值为0,则pthread_mutex_trylock函数就返回错误码EBUSY

非阻塞加锁,拿锁的时候如果拿不到锁就直接返回了,也不等待,需要搭配循环使用,一直调用,当其返回值为zero时循环结束,否则使用这个接口的后果就和我们上面说的,当一个进程对一块被占用的资源访问时,如果不拿到这个锁,也是可以访问的,这样就达不到互斥访问的目的了

带有超时时间的加锁

int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abs_timeout);

功能:如果不能立即获得目标互斥锁,则等待abs_timeout时间,如果在等待时间内加锁成功则直接返回,如果超过等待时间则直接返回表示加锁失败

解锁函数

int pthread_mutex_unlock(pthread_mutex_t *mutex);

功能:将互斥锁中的互斥量从0变成1,表示其他线程可以获取该互斥锁了。以原子操作的方式给一个互斥锁解锁,如果此时有其他线程正在等待这个互斥锁,则其他线程会获得该互斥锁

参数:pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:返回0表示解锁成功;解锁失败返回errno

销毁互斥锁函数

int pthread_mutex_destroy(pthread_mutex_t *mutex);

功能:将目标互斥锁销毁

参数:pthread_mutex_t* : 一个指向pthread_mutex_t结构体的指针变量

返回值:销毁成功返回0;销毁失败返回errno

如果是动态初始化互斥锁,就需要销毁,如果是静态初始化就不用销毁。

在所有线程可能退出的地方进行解锁,防止产生死锁

代码演示:

我们对上面的黄牛抢票的程序进行修改,代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 int g_tickets = 100;5 pthread_mutex_t lock;
W>  6 void* mythread_start(void* arg){7   pthread_mutex_lock(&lock);//加锁8   while(g_tickets--){
W>  9     printf("I am %p, I have ticket %d\n", pthread_self(),g_tickets);10   }
W> 11 }12 int main(){13   pthread_mutex_init(&lock, NULL);//动态初始化14   pthread_t tid[2];15   for(int i=0; i<2; ++i){                                                                   16     int ret = pthread_create(&tid[i], NULL, mythread_start, NULL);17     if(ret < 0){18       perror("pthread_create");19       return 0;20     }21   }22   while(1){23     sleep(1);24   }25   pthread_mutex_destroy(&lock);//退出时销毁互斥锁26   return 0;27 }

执行结果:

在这里插入图片描述

结果分析:

在这里插入图片描述

我们查看进程的调用栈,发现除了主线程外,只有一个线程,这个工作线程一直在等待锁,但是加锁的线程已经将锁锁上,而且执行完代码后退出了,现在这把锁就被永远的锁上,这种情况叫死锁
上述导致死锁的原因是线程退出时没有进行解锁,那防止死锁的产生也就需要在线程所有可能退出的地方进行解锁。

我们对代码进行改进,前五十张票让第一个线程拿,也就是全局变量小于50时,第一个线程进行解锁,让另一个线程可以接手,修改代码如下:

在这里插入图片描述

执行结果:

在这里插入图片描述

这下子并没有再出现死锁的情况了

结论:在线程所有有可能退出的地方都进行解锁,防止产生死锁,不要让线程退出的时候把锁带走

线程同步

线程同步的必要性

多个线程保证了互斥, 也就是保证了线程能够合理的访问临界资源了。但并不是说, 各个线程在访问临界资源的时候都是合理的。同步是为了保证多个线程对临界资源的访问的合理性这个合理性建立在多个线程保证互斥的情况下。就比如说一个吃面的场景,当一个只有一个碗,吃面和做面的人都可以对这个碗进行访问操作,而对这个碗同时只能有一个人访问,不可以同时做面的人在往碗里做面吃面的人也在碗中吃面,要防止这样的情况发生就要采用互斥的原理,但是当有了互斥之后保证线程可以独自访问资源了,就如吃面的人可以独自吃面了而不会有做面的人来干扰,而做面的人也可以独自做面了,也不会有吃面的人同时和它抢生意,但是还有一个问题,就是你吃面的人不可以在碗里没有面的情况下去吃面,甚至把碗吃掉,你做面的人不可以在碗里有面的情况下,再去做面,那碗里都盛不下面了,所以此时要有同步的概念来保证访问临界资源的合理性。

同步:保证各个线程对于共享资源的访问具有合理性

模拟一下上面的场景,代码如下:

    1 #include<stdio.h>2 #include<pthread.h>3 #include<unistd.h>4 pthread_mutex_t g_bowl;//互斥锁,碗5 int bowl = 1;
W>  6 void* eat_thread_start(void* arg){7   while(1){8     pthread_mutex_lock(&g_bowl);9     printf("I am eatman eat%d\n",bowl--);10     pthread_mutex_unlock(&g_bowl);11   }12 }
W> 13 void* make_thread_start(void* arg){14   while(1){15     pthread_mutex_lock(&g_bowl);16     printf("I am makeman make%d\n",++bowl);17     pthread_mutex_unlock(&g_bowl);18   }19 }20 int main(){                                                                                 21   pthread_mutex_init(&g_bowl, NULL);22   pthread_t eat_tid;23   pthread_t make_tid;24   int ret = pthread_create(&eat_tid, NULL, eat_thread_start, NULL);25   if(ret < 0){26     perror("pthread_create");27   }28   ret = pthread_create(&make_tid, NULL, make_thread_start, NULL);29   if(ret < 0){30     perror("pthread_create");31   }32   pthread_join(eat_tid, NULL);33   pthread_join(make_tid, NULL);34   pthread_mutex_destroy(&g_bowl);35   return 0;36 }

执行结果:

在这里插入图片描述

做面做出了负数,出现了不合理的访问。

为了控制线程对共享资源的独占式访问,并且访问次序具有合理性,有以下方式实现线程同步

  • POSIX信号量
  • 条件变量

什么是对共享资源的独占式访问呢?

同学们在上厕所的时候,坑位就是一个共享资源,同学们在上厕所时,将厕所门关上,就是对共享资源的独占式访问,在你上厕所的时候,别人进不来(无法访问),引申到代码中,就是相当:将对于一个变量的操作变成原子性操作,要么操作成功,要么没操作,不存在操作一半被打断的情况

条件变量

条件变量的使用原理

  • 线程在加锁后,判断下临界资源是否可用
  • 如果可用,则直接访问临界资源
  • 如果不可用,则调用等待接口,让该线程进行等待

改进代码:

在这里插入图片描述

执行结果:可以看到这里就正常了。做面人不会因为碗里有面再做面,吃面人不会因为碗里没有面而把碗吃掉

在这里插入图片描述

但是上述代码有很严重的效率缺陷,十分耗费CPU资源,还不拿CPU资源干正事儿,假设此时一个线程拿到的时间片是200ms,该线程在该时间片内判断,吃面人要吃面但是如果碗里没面他就continue退出这一次循环,但是由于时间片没有结束,就会再次进行循环,加锁判断是否有面,但是还是没有面,就再次退出循环,如此反复,假设一次他加锁后,还没进行解锁,时间片就用完了,下一个线程要执行,要拿到互斥锁,但是此时互斥锁没有被释放,这个线程又要在所有的时间片内重复判断,但是这些举动是毫无意义的,程序效率就很低下

条件变量的原理

条件变量提供了一种线程间的通知机制:当某个共享数据达到某个值的时候,唤醒等待这个共享数据的线程
条件变量本质是一个PCB等待队列(存放在等待的线程的PCB)

条件变量基础API

条件变量的相关函数主要以下几个:

#include <pthread.h>
int pthread_cond_init(pthread_cond_t * cond,pthread_condattr_t * attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;int pthread_cond_destroy(pthread_cond_t *cond);int pthread_cond_timedwait(pthread_cond_t * cond, pthread_mutex_t * mutex, struct timespec * abstime);
int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_t是条件变量结构体
  • pthread_condattr_t是条件变量属性结构体
  • PTHREAD_COND_INITIALIZER是一个宏,用来初始化条件变量结构体的,本质是将条件变量各个字段设置为0

初始化条件变量函数

动态初始化

int pthread_cond_init(pthread_cond_t* cond, pthread_condattr_t* attr);

功能:初始化条件变量结构体

参数:

  • pthread_cond_t : 条件变量结构体指针
  • pthread_condattr_t : 条件变量属性结构体指针,常传入NULL,使用默认的属性
pthread_cond_t cond;
pthread_cond_init(&cond,NULL);

静态初始化

使用宏PTHREAD_COND_INITIALIZER初始化条件变量

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

销毁条件变量函数

int pthread_cond_destroy(pthread_cond_t *cond);

功能:销毁一个条件变量

参数:pthread_cond_t : 条件变量结构体指针

pthread_cond_destroy(&cond)

等待条件变量函数

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

功能:将调用该函数的线程放入PCB等待队列中

参数:

  • pthread_cond_t : 条件变量结构体指针

  • mutex : 该线程等待的互斥锁

唤醒条件变量函数

单个唤醒

int pthread_cond_signal(pthread_cond_t *cond);

功能:唤醒一个等待目标条件变量的线程,至于唤醒的是PCB等待队列当中的哪个线程,取决于线程的优先级和调度策略,(有可能唤醒两个或者三个或者全部都唤醒)

参数:pthread_cond_t : 条件变量结构体指针

广播唤醒

int pthread_cond_broadcast(pthread_cond_t *cond);

功能:以广播的方式唤醒所有等待目标条件变量的线程

参数:pthread_cond_t : 条件变量结构体指针

代码演示:

在这里插入图片描述

执行结果:吃面人吃面,做面人做面,很和谐

在这里插入图片描述

将上述程序的进程数量增加,比如说有两个做面人和两个吃面人,此时又会出现新的问题:当wait结束后,执行的是printf,也就是吃面/做面,再之后,我们的if判断就不起作用了,就会出现疯狂的做面和吃面,所以我们在wait结束后,printf之前,也就是吃面/做面前,再次判断是否能吃面/做面

代码如下:

#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#define countman 2
pthread_mutex_t g_bowl;//互斥锁,碗
pthread_cond_t g_cond;//条件变量
int bowl = 1;
void* eat_thread_start(void* arg){while(1){pthread_mutex_lock(&g_bowl);if(bowl==0){//如果碗里没有面了,就不能吃面了pthread_mutex_unlock(&g_bowl);pthread_cond_wait(&g_cond, &g_bowl);//没面了等做面人做面}printf("I am eatman eat%d\n",bowl--);pthread_mutex_unlock(&g_bowl);pthread_cond_signal(&g_cond);//面吃完了通知做面人来做面}
}
void* make_thread_start(void* arg){while(1){pthread_mutex_lock(&g_bowl);if(bowl==1){//如果碗里有面就不再做面了pthread_mutex_unlock(&g_bowl);pthread_cond_wait(&g_cond, &g_bowl);//等待吃面人吃面,没碗装面了}printf("I am makeman make%d\n",++bowl);pthread_mutex_unlock(&g_bowl);pthread_cond_signal(&g_cond);//面做好了通知吃面人吃面}
}
int main(){pthread_mutex_init(&g_bowl, NULL);pthread_t eat_tid[countman];pthread_t make_tid[countman];for(int i=0; i<countman; ++i){int ret = pthread_create(&eat_tid[i], NULL, eat_thread_start, NULL);if(ret < 0){perror("pthread_create");}ret = pthread_create(&make_tid[i], NULL, make_thread_start, NULL);if(ret < 0){perror("pthread_create");}}for(int i=0; i<countman; ++i){pthread_join(eat_tid[i], NULL);pthread_join(make_tid[i], NULL);}pthread_mutex_destroy(&g_bowl);pthread_cond_destroy(&g_cond);return 0;
}

执行结果:

在这里插入图片描述

改进如下:

在这里插入图片描述

执行结果:我们虽然解决了刚刚多个吃面线程乱吃的问题,但是这里发现运行到最后程序不跑了

在这里插入图片描述

查看一下调用栈:

在这里插入图片描述

这样的一个场景就是所有线程都进入了等待队列中,但是没有人通知。说到底产生这样的问题是因为所有做面线程已经进入等待队列当中去了,但是此时吃面进程欲要通知等待队列中的做面线程但是它有可能将吃面线程通知出来,导致所有线程进入等待状态。

解决程序卡死方式:我们想要解决这个问题就要让做面线程每次唤醒的都是吃面线程,而吃面线程每次唤醒的都是做面线程。(当然也可以用int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒PCB等待队列中所有的线程,但是种不推荐使用,因为太浪费CPU资源)

改进如下:

在这里插入图片描述

执行结果:

在这里插入图片描述

发现不会再卡死或者疯狂吃面或者疯狂做面的情况,针对不同的资源,可以设置不同的条件变量

条件变量常见问题

条件变量的等待接口第二个参数为什么会有互斥锁?

当线程被放入等待队列后,就不会再执行后续代码了,也就不会再进行解锁了,锁将永远被锁上,其他线程就不会获得这把锁来进行操作,所以我们要将互斥锁传入条件变量等待函数,在该函数内部进行解锁操作,让其他线程可以拿到锁

pthread_cond_wait的内部是针对互斥锁做了上什么操作?先释放互斥锁还是先将线程放入到PCB等待队列?

pthread_cond_wait在调用的时候先要将线程放入等待队列当中。然后再释放互斥锁。

我们假设是先解锁然后再让线程入等待队列,这样的话,有可能这个线程刚解锁,就有其他线程拿到锁,并且修改了临界区资源,而此时的临界区资源被修改后恰好满足条件,所以这个线程就唤醒等待队列当中等待资源的线程,但是现在等待队列中还没有线程,它唤醒之后也进入了等待队列进行等待,此时当此线程进入等待队列当中时,之前的线程也刚刚到等待队列,那么此时两个线程就同时进到等待队列中了,谁也出不去

线程被唤醒之后会执行什么代码,为什么需要获取互斥锁?

pthread_cond_wait在返回之前一定会在其内部进行加锁操作,就是当一个线程在调用pthread_cond_wait函数进入等待队列中后,然后被唤醒时一定会在pthread_cond_wait函数中执行加锁操作。
而在加锁操作时:

  • 抢到了:pthread_cond_wait函数就真正的执行完毕,函数返回。
  • 没抢到:pthread_cond_wait函数就没有被真正的执行完成,还处于函数内部抢锁的逻辑,然后一直来进行抢锁操作。

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

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

相关文章

从零开始的Hadoop学习(五)| HDFS概述、shell操作、API操作

1. HDFS 概述 1.1 HDFS 产出背景及定义 1&#xff09;HDFS 产生背景 随着数据量越来越大&#xff0c;在一个操作系统存不下所有的数据&#xff0c;那么就分配到更多的操作系统管理的磁盘中&#xff0c;但是不方便管理和维护&#xff0c;迫切 需要一种系统来管理多台机器上的…

基于飞腾芯片的设计与调试入门指导

一、啥是自主可控 国产CPU现在厂家细算起来其实有很多,现在华为、小米也在做自己的CPU,瑞芯微、全志等的SoC现在也是广泛应用。但是真正能叫做自主可控的CPU厂商,只有6家。那啥是自主可控?首先来不严谨的讲下现在数字芯片是怎么做的设计。FPGA大家都知道,可以通过Verilog…

SOD-123FL贴片整流二极管,有哪些型号?

近日发现&#xff0c;客户对整流二极管的需求特别大。常有客户前来东沃电子咨询整流二极管型号参数、选型、替代、价格、交期、样品等方面的问题。那么&#xff0c;关于DS1A、DS1B、DS1D、DS1G、DS1J、DS1K、DS1M贴片整流二极管&#xff0c;您知道多少呢&#xff1f;东沃电子推…

【微服务部署】06-日志集成

文章目录 1. EFK日志三件套集成1.1 核心组件1.2 部署 2. Exceptionless日志系统2.1 Exceptionless核心特性2.2 Exceptionless部署文件2.3 K8s中使用Exceptionless 1. EFK日志三件套集成 1.1 核心组件 Elasticsearch&#xff08;存储&#xff09;Fluentd&#xff08;收集器&am…

W5100S-EVB-PICO主动PING主机IP检测连通性(十)

前言 上一章节我们用我们开发板在UDP组播模式下进行数据回环测试&#xff0c;本章我们用开发板去主动ping主机IP地址来检测与该主机之间网络的连通性。 什么是PING&#xff1f; PING是一种命令&#xff0c; 是用来探测主机到主机之间是否可通信&#xff0c;如果不能ping到某台…

LeetCodeHot100python版本:单调栈,栈,队列,堆

单调栈 739. 每日温度 42. 接雨水 双指针 单调栈(横向求解) ​​​​​​84. 柱状图中最大的矩形 栈和队列 队列:先入先出 栈:先入后出 两个栈 模拟 队列 一个队列 可以模拟 栈 20. 有效的括号 ​​​​​​155. 最小栈 394. 字符串解码 堆 215. 数组中的第K个最大元素 (…

STM32 Cubemx 同名外设中断及回调

文章目录 前言示例工程个人理解 前言 最近在学习STM32&#xff0c;采用HAL库开发方式。记录一下同名外设中断及回调。 这里提及的同名外设指USART1/2之类的相同外设&#xff0c;但不是同一个instance。 示例工程 以使用cubemx配置两个同名外设EXTI0/EXT4为例。 在NVIC配置…

JS三座大山 —— 原型和原型链

系列文章目录 内容链接2023前端面试笔记HTML52023前端面试笔记CSS3 文章目录 系列文章目录前言一、原型是什么&#xff1f;二、原型链是什么&#xff1f;2.1 原型链全方面解析2.2 为什么构造函数也有原型&#xff1f; 总结 前言 理解原型和原型链可以帮助我们更好地理解 Java…

传统分拣弊端明显,AI机器视觉赋能物流行业包裹分类产线数智化升级

随着电子商务的快速发展&#xff0c;物流行业的包裹数量持续增长&#xff0c;给物流企业带来了巨大的运营压力。目前&#xff0c;国内大型物流运转中心已开始采用机器视觉自动化设备&#xff0c;但多数快递公司处于半自动化状态&#xff0c;中小型物流分拣中心目前仍靠人工录入…

每日一题——旋转图像

旋转图像 题目链接 方法一&#xff1a;利用辅助数组 通过对示例的观察和分析&#xff0c;我们可以得到这样的结论&#xff1a; 对于原数组的下标为i行元素&#xff0c;顺时针旋转九十度后&#xff0c;都变成了下标为&#xff08;n-1-i&#xff09;列元素。如图所示&#xff…

代替forever下一个部署node的持久化工具---pm2

最近有个后端项目&#xff0c;用的是node&#xff0c;在持久化的时候会挂掉&#xff0c;详细了解到用的是nohup&#xff0c;然后先详细了解了一下nohup nohup是一个Linux命令&#xff0c;用于在系统后台不挂断地运行命令&#xff0c;退出终端不会影响程序的运行1nohup的英文全称…

基于RabbitMQ的模拟消息队列需求文档

文章目录 一、项目背景二、需求分析1.核心概念2.BrokerServer核心组件3.核心API4.交换机类型5.持久化6.网络通信7.消息应答 三、消息队列模块划分 一、项目背景 什么是消息队列&#xff1f; 消息队列就是&#xff0c;基于阻塞队列&#xff0c;封装成一个独立的服务器程序&#…

Nginx百科之gzip压缩、黑白名单、防盗链、零拷贝、跨域、双机热备

引言 早期的业务都是基于单体节点部署&#xff0c;由于前期访问流量不大&#xff0c;因此单体结构也可满足需求&#xff0c;但随着业务增长&#xff0c;流量也越来越大&#xff0c;那么最终单台服务器受到的访问压力也会逐步增高。时间一长&#xff0c;单台服务器性能无法跟上业…

three.js渲染带动画的glb文件

1. 准备工作 将下列文件在three.js的包中找到&#xff0c;注意的是我这里使用的是模块化版本的&#xff0c;这里不知道模块化的&#xff0c;可以先去看一下es6的模块化。 控制器&#xff1a; OrbitControls.js 加载器&#xff1a;GLTFLoader.js 材质&#xff1a; RoomEnvironm…

数据时代的必备利器:数据可视化工具的崭新价值

在当今信息时代&#xff0c;数据扮演着越来越重要的角色&#xff0c;而数据的可视化呈现正是一种强大的工具&#xff0c;能够帮助我们更好地理解和利用这些数据。虽然Excel和PPT在处理数据方面有着不可否认的作用&#xff0c;但在处理大规模、复杂数据时&#xff0c;数据可视化…

微信小程序 基于Android的美容理发师预约管理系统

&#xff0c;本系统主要根据管理员、用户及理发师的实际需要&#xff0c;方便用户利用互联网实现对商品信息进行立即订购&#xff0c;同时让管理者可以通过这个系统对用户实际需求以及各信息进行管理。设计该系统主要目的是为了方便用户、理发师可以有一个非常好的平台体验&…

文心一言 VS 讯飞星火 VS chatgpt (84)-- 算法导论8.2 1题

一、用go语言&#xff0c;参照图8-2 的方法&#xff0c;说明 COUNTING-SORT 在数组 A(6&#xff0c;0&#xff0c;2&#xff0c;0&#xff0c;1&#xff0c;3&#xff0c;4&#xff0c;6&#xff0c;1&#xff0c;3&#xff0c;2)上的操作过程。 文心一言&#xff1a; Counti…

现货黄金走势图中的止盈点

对平仓时机的把握能力&#xff0c;是衡量现货黄金投资者水平的重要标志&#xff0c;止盈点设置得是否合理&#xff0c;在行情兑现的时候能否及时地离场&#xff0c;是事关投资者账户浮盈最终能否落袋为安的“头等大事”&#xff0c;要在现货黄金走势图中把握止盈点&#xff0c;…

Linux-安装redis6.2.1及主备复制模式(replication)

Linux-安装redis6.2.1 下载redis6.2.1资源上传至安装目录解压及编译解压修改名称编译 修改配置文件主节点从节点 启动及测试启动主节点从节点 测试 下载redis6.2.1资源 地址》https://redis.io/download/ 上传至安装目录 例&#xff1a;/data/replication/ 解压及编译 解…

【kubernetes系列】Calico原理及配置

概述 Calico是针对容器&#xff0c;虚拟机和基于主机的本机工作负载的开源网络和网络安全解决方案。 Calico支持广泛的平台&#xff0c;包括Kubernetes&#xff0c;OpenShift&#xff0c;Docker EE&#xff0c;OpenStack和裸机服务。 Calico在每个计算节点都利用Linux Kernel实…