【Linux C | 多线程编程】线程同步 | 条件变量(万字详解)

😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀
🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C++、数据结构、音视频🍭
⏰发布时间⏰:2024-04-15 08:58:04

本文未经允许,不得转发!!!

目录

  • 🎄一、概述
  • 🎄二、为什么需要条件变量
  • 🎄三、条件变量相关函数
    • ✨3.1 条件变量的初始化
    • ✨3.2 条件变量的销毁
    • ✨3.3 条件变量的等待
    • ✨3.4 条件变量的唤醒
  • 🎄四、条件变量的使用
    • ✨4.1 条件等待的使用
    • ✨4.2 条件唤醒的使用
    • ✨4.3 条件变量使用例子
  • 🎄五、总结


相关文章:
【Linux C | 多线程编程】线程同步 | 互斥量(互斥锁)介绍和使用
【Linux C | 多线程编程】线程同步 | 条件变量(万字详解)
【Linux C | 多线程编程】线程同步 | 条件变量 的 使用总结


在这里插入图片描述

🎄一、概述

多线程编程中,涉及到线程并发,因此也衍生了一些问题,常见的有2类问题:

  • 1、多个线程访问同一个共享资源,这个可以用上篇文章 线程同步|互斥量的介绍和使用 介绍的互斥量来解决;
  • 2、某些线程运行后,有时需要等待一定的条件才会继续执行,如果条件不满足就会等待,而条件的达成, 很可能取决于另一个线程。

本文要介绍的条件变量主要是解决上面的第二个问题。条件变量用来阻塞一个线程,直到某条件满足为止。通常条件变量和互斥锁同时使用。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待"条件变量的条件成立"而挂起;另一个线程使"条件成立"(给出条件成立信号)。


在这里插入图片描述

🎄二、为什么需要条件变量

因为如果不使用条件变量,线程就需要 轮询+休眠 来查看是否满足条件,这样严重影响效率。

下面是不使用条件变量的代码:
1、创建一个线程作为生产者,一秒生成一个产品;
2、创建两个线程作为消费者,分别一秒消耗一个产品,此时消费者线程 不得不每隔一秒就查询是否有数据
3、需要添加"linux_list.h"头文件,文件在下面会给出,它是Linux内核使用的一个链表。需要了解使用方法的,可以看这文章【数据结构】list.h 详细使用教程

// 09_producer_consumer.c
// gcc 09_producer_consumer.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include "linux_list.h"#define  COMSUMER_NUM	2typedef struct _product
{struct list_head list_node;int product_id;
}product_t;struct list_head productList;// 头结点
pthread_mutex_t product_mutex = PTHREAD_MUTEX_INITIALIZER;	// productList 的互斥量// 生产者线程,1秒生成一个产品放到链表
void *th_producer(void *arg)
{int id = 0;while(1){product_t *pProduct = (product_t*)malloc(sizeof(product_t));pProduct->product_id = id++;pthread_mutex_lock(&product_mutex);list_add_tail(&pProduct->list_node, &productList);pthread_mutex_unlock(&product_mutex);sleep(1);}return NULL;
}// 消费者线程,1秒消耗掉一个产品
void *th_consumer(void *arg)
{while(1){pthread_mutex_lock(&product_mutex);if(!list_empty(&productList)) // 不为空,则取出一个{product_t* pProduct = list_entry(productList.next, product_t, list_node);// 获取第一个节点printf("consumer[%d] get product id=%d\n", *((int*)arg), pProduct->product_id);list_del(productList.next); // 删除第一个节点free(pProduct);}pthread_mutex_unlock(&product_mutex);sleep(1);}return NULL;
}int main()
{INIT_LIST_HEAD(&productList);	// 初始化链表// 创建生产者线程pthread_t producer_thid;pthread_create(&producer_thid, NULL, th_producer, NULL);// 创建消费者线程pthread_t consumer_thid[COMSUMER_NUM];int i=0, num[COMSUMER_NUM]={0,};for(i=0; i<COMSUMER_NUM; i++){num[i] = i;pthread_create(&consumer_thid[i], NULL, th_consumer, &num[i]);}// 等待线程pthread_join(producer_thid, NULL);for(i=0; i<COMSUMER_NUM; i++){pthread_join(consumer_thid[i], NULL);}return 0;
}

下面是"linux_list.h"的头文件:

// my_list.h 2023-09-24 23:07:43
#ifndef _LINUX_LIST_H
#define _LINUX_LIST_Hstruct list_head {struct list_head *next, *prev;
};#define INIT_LIST_HEAD(ptr) do { \(ptr)->next = (ptr); (ptr)->prev = (ptr); \
} while (0)/*** list_empty - tests whether a list is empty* @head: the list to test.*/
static inline int list_empty(const struct list_head *head)
{return head->next == head;
}/** Insert a new entry between two known consecutive entries.** This is only for internal list manipulation where we know* the prev/next entries already!*/
static inline void __list_add(struct list_head *new_node,struct list_head *prev,struct list_head *next)
{next->prev = new_node;new_node->next = next;new_node->prev = prev;prev->next = new_node;
}/*** list_add - add a new entry* @new: new entry to be added* @head: list head to add it after** Insert a new entry after the specified head.* This is good for implementing stacks.*/
static inline void list_add(struct list_head *new_node, struct list_head *head)
{__list_add(new_node, head, head->next);
}/*** list_add_tail - add a new entry* @new: new entry to be added* @head: list head to add it before** Insert a new entry before the specified head.* This is useful for implementing queues.*/
static inline void list_add_tail(struct list_head *new_node, struct list_head *head)
{__list_add(new_node, head->prev, head);
}/** Delete a list entry by making the prev/next entries* point to each other.** This is only for internal list manipulation where we know* the prev/next entries already!*/
static inline void __list_del(struct list_head * prev, struct list_head * next)
{next->prev = prev;prev->next = next;
}/*** list_del - deletes entry from list.* @entry: the element to delete from the list.* Note: list_empty on entry does not return true after this, the entry is* in an undefined state.*/
static inline void list_del(struct list_head *entry)
{__list_del(entry->prev, entry->next);//entry->next = (struct list_head *)LIST_POISON1;//entry->prev = (struct list_head *)LIST_POISON2;
}#ifndef offsetof
#define offsetof(type, f) ((size_t) \((char *)&((type *)0)->f - (char *)(type *)0))
#endif#ifndef container_of
#define container_of(ptr, type, member) ({ \const typeof( ((type *)0)->member ) *__mptr = (ptr);\(type *)( (char *)__mptr - offsetof(type,member) );})
#endif/*** list_entry - get the struct for this entry* @ptr:	the &struct list_head pointer.* @type:	the type of the struct this is embedded in.* @member:	the name of the list_struct within the struct.*/
#define list_entry(ptr, type, member) \container_of(ptr, type, member)#ifndef ARCH_HAS_PREFETCH
static inline void prefetch(const void *x) {;}
#endif/*** list_for_each	-	iterate over a list* @pos:	the &struct list_head to use as a loop counter.* @head:	the head for your list.*/
#define list_for_each(pos, head) \for (pos = (head)->next; prefetch(pos->next), pos != (head); \pos = pos->next)/*** list_for_each_safe	-	iterate over a list safe against removal of list entry* @pos:	the &struct list_head to use as a loop counter.* @n:		another &struct list_head to use as temporary storage* @head:	the head for your list.*/
#define list_for_each_safe(pos, n, head) \for (pos = (head)->next, n = pos->next; pos != (head); \pos = n, n = pos->next)/*** list_for_each_entry	-	iterate over list of given type* @pos:	the type * to use as a loop counter.* @head:	the head for your list.* @member:	the name of the list_struct within the struct.*/
#define list_for_each_entry(pos, head, member)				\for (pos = list_entry((head)->next, typeof(*pos), member);	\prefetch(pos->member.next), &pos->member != (head); 	\pos = list_entry(pos->member.next, typeof(*pos), member))#endif //_LINUX_LIST_H

在这里插入图片描述

🎄三、条件变量相关函数

这小节先介绍条件变量的相关函数接口,条件变量都是要结合互斥量一起使用的,具体用法在下个小节介绍。

✨3.1 条件变量的初始化

POSIX提供了两种初始化条件变量的方法。

  • 1、是将PTHREAD_COND_INITIALIZER赋值给定义的条件变量,如下:

    #include <pthread.h>
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    

    但这个方法没办法设置条件变量的属性,也不适用于动态分配的条件变量。

  • 2、使用 pthread_cond_init 初始化条件变量。如下:

    #include <pthread.h>
    int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
    

    第二个pthread_condattr_t指针的入参,是用来设定条件变量的属性的。大部分情况下,并不需要设置条件变量的属性,传递NULL即可,表示使用条件变量的默认属性。

注意:永远不要用一个条件变量对另一个条件变量赋值, 即pthread_cond_t cond_b=cond_a不合法, 这种行为是未定义的。


✨3.2 条件变量的销毁

使用pthread_cond_init初始化的条件变量,在确定不再使用的时候, 就要销毁它。 在销毁之前, 有三点需要注意:
1、使用PTHREAD_COND_INITIALIZE静态初始化的条件变量,不需要被销毁。
2、要调用pthread_cond_destroy销毁的条件变量可以调用pthread_cond_init重新进行初始化。
3、不要引用已经销毁的条件变量, 这种行为是未定义的。

销毁条件变量的接口如下:

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);

✨3.3 条件变量的等待

条件变量就是为了与某个条件关联起来使用的,如果条件不满足,就等待(pthread_cond_wait) ,或者等待一段有限的时间(pthread_cond_timedwait) 。POSIX提供了如下条件变量的等待接口:

#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);

函数描述:这两个函数都是让指定的条件变量进入等待状态,其工作机制是先解锁传入的互斥量,再让条件变量等待,从而使所在线程处于阻塞状态。这两个函数返回时,系统会确保该线程再次持有互斥量(加锁)。

两个函数的区别:pthread_cond_wait函数调用成功后,会一直阻塞等待,直到条件变量被唤醒。而 pthread_cond_timedwait 函数只会等待指定的时间,时间到了之后,条件变量仍未被唤醒的话,会返回一个错误码ETIMEDOUT,该错误码定义在<errno.h>头文件。

注意:pthread_cond_timedwait指定的时间是绝对时间,而不是相对时间。如果最多等待2分钟,那么这个值应该是当前时间加上2分钟。使用方法可以参考下面代码

struct timeval now;
struct timespec outtime;
memset(&outtime,0,sizeof(outtime));
memset(&now,0,sizeof(now));gettimeofday(&now, NULL);
outtime.tv_sec = now.tv_sec + sec;
outtime.tv_nsec = now.tv_usec * 1000;// pthread_cond_timedwait 执行后,先加锁pMutex、再等待;被唤醒后会加锁pMutex
if(pthread_cond_timedwait(pCond, pMutex, &outtime) == ETIMEDOUT)
{ret = -1;
}

✨3.4 条件变量的唤醒

上面说完了条件等待,接下来介绍条件变量的唤醒。调用完条件变量等待函数的线程处于阻塞状态,若要被唤醒,必须是其他线程来唤醒。POSIX提供了如下两个接口来唤醒指定的条件变量:

#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

pthread_cond_signal 负责唤醒等待在条件变量上的一个线程,如果有多个线程等待,是唤醒哪一个呢?Linux内核会为每个条件变量维护一个等待队列,调用了 pthread_cond_waitpthread_cond_timedwait 的线程会按照调用时间先后添加到该队列中。pthread_cond_signal会唤醒该队列的第一个。

pthread_cond_broadcast,就是广播唤醒等待在条件变量上的所有线程。前面说过,条件等待的两个函数返回时,系统会确保该线程再次持有互斥量(加锁),所有,这里被唤醒的所有线程都会去争夺互斥锁,没抢到的线程会继续等待,拿到锁后同样会从条件等待函数返回。所以,被唤醒的线程第一件事就是再次判断条件是否满足!


在这里插入图片描述

🎄四、条件变量的使用

要清楚条件变量怎样使用?就是要弄清楚条件变量的怎样初始化、销毁、等待、唤醒。通过上个小节的学习,条件变量初始化、销毁应该都可以弄懂,而条件变量的等待、唤醒都使用到互斥量,会显得比较复杂,刚学习时总会不清楚这个互斥量应该锁在哪个位置,也容易被 条件等待 的机制弄晕,这小节就看看写代码时,怎样写条件变量的等待、唤醒。

✨4.1 条件等待的使用

条件变量的等待函数都会用到两个参数,一个是条件变量指针,另一个是互斥量指针。其中,条件变量指针比较好理解,直接把初始化好的条件变量指针传入即可。而互斥量指针参数要求传入一个互斥量地址,那这个互斥量在哪里加锁,哪里解锁呢?

一般情况下,在调用条件等待(pthread_cond_wait)函数之前,会判断条件是否满足,不满足的话才进入条件等待。而这个条件是需要其他线程来使其满足的,这里就出现了多个线程访问共享资源,为了安全地获取和修改共享数据,就需要一个互斥量来同步。到这里可以得出,访问与条件相关的共享资源时,会有一个互斥量锁住这个访问的临界区。那这个互斥量 和 pthread_cond_wait需要的互斥量是不是同一个呢?为什么?假设这两个互斥量不是同一个的话,就会出现一个问题,在判断完条件之后,调用pthread_cond_wait之前,可能条件被修改为满足了,这显然不是我们想要的,我们需要的是“判断条件、不满足就等待”这两个操作直接不会被其他线程干扰到。

所以,使用条件变量等待时,会使用一个互斥量进行加锁,加锁的临界区包含了“①查看是否满足条件、②调用pthread_cond_wait进入条件等待”这两个操作。

🌰举例子:
1、productList是判断条件需要用到的共享资源;
2、product_mutex 是操作时用于加锁的互斥量 productList;
3、product_cond 是条件变量

pthread_mutex_lock(&product_mutex);
while(list_empty(&productList)) // 条件不满足
{pthread_cond_wait(&product_cond, &product_mutex);
}
// 不为空,则取出一个
pthread_mutex_unlock(&product_mutex);

✨4.2 条件唤醒的使用

上面介绍了怎么等待了,那唤醒等待条件就需要另一个线程在操作,这个线程在唤醒之前肯定是要有一个操作令前面等待的条件满足的,然后才是唤醒(pthread_cond_signal)。修改条件,肯定也是被互斥量加锁保护的,那唤醒是放在互斥量加锁的临界区内还是临界区外呢?答案是都可以,但放在临界区外,会效率更高。

从前面知道,pthread_cond_wait函数返回时,系统会确保该线程再次持有互斥量(加锁)。也就是说,条件等待的线程被唤醒后会再次对互斥量加锁。知道这点后,再看上面说的两个情况。

唤醒(pthread_cond_signal)在临界区内的代码如下,会出现线程被唤醒了,并没能第一时间获取到互斥量,需要等到下面第4行执行后,才获取到互斥量,所以效率低一点。

thread_mutex_lock(&product_mutex);
list_add_tail(&pProduct->list_node, &productList); // 改变了条件
pthread_cond_signal(&product_cond);
pthread_mutex_unlock(&product_mutex);

唤醒(pthread_cond_signal)在临界区外的代码如下,因为先解锁了条件变量,所以

thread_mutex_lock(&product_mutex);
list_add_tail(&pProduct->list_node, &productList); // 改变了条件
pthread_mutex_unlock(&product_mutex);
pthread_cond_signal(&product_cond);

✨4.3 条件变量使用例子

下面是第二小节的例子使用了条件变量的代码,可以看到不需要sleep来休眠了。

// 09_producer_consumer_cond.c
// gcc 09_producer_consumer_cond.c -lpthread
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include "linux_list.h"#define  COMSUMER_NUM	2typedef struct _product
{struct list_head list_node;int product_id;
}product_t;struct list_head productList;// 头结点
pthread_mutex_t product_mutex = PTHREAD_MUTEX_INITIALIZER;	// productList 的互斥量
pthread_cond_t  product_cond = PTHREAD_COND_INITIALIZER;	// 条件变量// 生产者线程,1秒生成一个产品放到链表
void *th_producer(void *arg)
{int id = 0;while(1){product_t *pProduct = (product_t*)malloc(sizeof(product_t));pProduct->product_id = id++;pthread_mutex_lock(&product_mutex);list_add_tail(&pProduct->list_node, &productList);pthread_cond_signal(&product_cond);pthread_mutex_unlock(&product_mutex);sleep(1);}return NULL;
}// 消费者线程,1秒消耗掉一个产品
void *th_consumer(void *arg)
{while(1){pthread_mutex_lock(&product_mutex);while(list_empty(&productList)) // 条件不满足{pthread_cond_wait(&product_cond, &product_mutex);}// 不为空,则取出一个product_t* pProduct = list_entry(productList.next, product_t, list_node);// 获取第一个节点printf("consumer[%d] get product id=%d\n", *((int*)arg), pProduct->product_id);list_del(productList.next); // 删除第一个节点free(pProduct);pthread_mutex_unlock(&product_mutex);}return NULL;
}int main()
{INIT_LIST_HEAD(&productList);	// 初始化链表// 创建生产者线程pthread_t producer_thid;pthread_create(&producer_thid, NULL, th_producer, NULL);// 创建消费者线程pthread_t consumer_thid[COMSUMER_NUM];int i=0, num[COMSUMER_NUM]={0,};for(i=0; i<COMSUMER_NUM; i++){num[i] = i;pthread_create(&consumer_thid[i], NULL, th_consumer, &num[i]);}// 等待线程pthread_join(producer_thid, NULL);for(i=0; i<COMSUMER_NUM; i++){pthread_join(consumer_thid[i], NULL);}return 0;
}

在这里插入图片描述

🎄五、总结

本文介绍了Linux多线程编程中常见的条件变量,先是介绍了条件变量相关接口函数,然后介绍了怎么使用条件变量,最后给出了使用条件变量的例子。

学习完上面的内容,看看是否知道下面几个问题的答案:
1、为什么需要条件变量?
2、条件变量为什么需要和互斥量一起使用?
3、pthread_cond_wait函数做了什么操作?
4、既然互斥量和条件变量关系如此紧密,为什么不干脆将互斥量变成条件变量的一部分呢?
5、互斥量加锁的临界区应该包含哪些操作?
6、先唤醒后解锁,还是先解锁后唤醒?
7、为什么条件等待时,使用while来判断条件,而不是用if ?

while(list_empty(&productList)) // 条件不满足
{pthread_cond_wait(&product_cond, &product_mutex);
}

在这里插入图片描述
如果文章有帮助的话,点赞👍、收藏⭐,支持一波,谢谢 😁😁😁

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

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

相关文章

电大搜题微信公众号:重庆开放大学学子的学习利器

在当今信息化时代&#xff0c;学习已经成为每个人不可或缺的一部分。然而&#xff0c;对于重庆开放大学的学子们来说&#xff0c;由于远程教育的特殊性&#xff0c;他们面临着更大的学习挑战。幸运的是&#xff0c;他们现在可以依靠一款强大的学习利器——电大搜题微信公众号&a…

单片机基础知识 07

一. 键盘检测 键盘分为编码键盘和非编码键盘。 编码键盘 &#xff1a;键盘上闭合键的识别由专用的硬件编码器实现&#xff0c;并产生键编码号或者键值&#xff0c;如计算机键盘。 非编码键盘&#xff1a;靠软件编程来识别。 在单片机组成的各种系统中&#xff0c;用的较多的…

Darknet框架优化介绍

一、DarkNet框架简介 1.DarkNet的简介 Darknet是一个完全使用C语言编写的人工智能框架&#xff0c;可以使用CUDA的开源框架。主要应用于图像识别领域。 它具有可移植性好&#xff0c;安装间接&#xff0c;查看源码方便等优势&#xff0c;提供了OpenCV等附加选项&#xff0c;还…

(七)Pandas时序数据 学习简要笔记 #Python #CDA学习打卡

一. 时序数据简介 1&#xff09;定义 时间序列&#xff08;time series&#xff09;&#xff0c;就是由时间构成的序列&#xff0c;它指的是在一定时间内按照时间顺序测量的某个变量的取值序列&#xff0c;比如一天内的温度会随时间而发生变化&#xff0c;或者股票的价格会随…

c语言题目之求最大公约数

题目内容&#xff1a;求最大公约数 给定两个数&#xff0c;求这两个数的最大公约数 例如&#xff1a; 输入&#xff1a;20 40 输出&#xff1a;20 什么叫最大公约数&#xff1f; 方法分析&#xff1a; 提示&#xff1a;这里我们用辗转相除法&#xff1a; 例如&#xff1a;输…

腾讯云APP备案指南:一站式完成备案手续,助您顺利上线

工信部最新通知要求所有互联网信息服务提供者完成移动互联网应用程序备案手续。腾讯云为开发者提供了简单易行的备案流程&#xff0c;本文详细解答如何在腾讯云平台完成备案&#xff0c;帮助开发者快速上线自己的APP。从验证备案域名到腾讯云审核&#xff0c;一步步指导您完成备…

vue 一键更换主题颜色

这里提供简单的实现步骤&#xff0c;具体看自己怎么加到项目中 我展示的是vue2 vue3同理 在 App.vue 添加 入口处直接修改 #app { // 定义的全局修改颜色变量--themeColor:#008cff; } // 组件某些背景颜色需要跟着一起改变&#xff0c;其他也是同理 /deep/ .ant-btn-primar…

响应式修改 页面字体字号【大 中 小 】

浅浅记录下&#xff0c;工作中遇到的问题&#xff0c;修改页面文本字号。 <p class"change_fontSize">[ 字号 <a href"javascript:doZoom(18)">大</a><a href"javascript:doZoom(16)">中</a><a href"ja…

『FPGA通信接口』汇总目录

Welcome 大家好&#xff0c;欢迎来到瑾芳玉洁的博客&#xff01; &#x1f611;励志开源分享诗和代码&#xff0c;三餐却无汤&#xff0c;顿顿都被噎。 &#x1f62d;有幸结识那个值得被认真、被珍惜、被捧在手掌心的女孩&#xff0c;不出意外被敷衍、被唾弃、被埋在了垃圾堆。…

墨子web3时事周报

蚂蚁集团Web3研发进展与布局 国内Web3赛道的领军企业——蚂蚁集团&#xff0c;凭借其在前沿科技领域的深耕不辍&#xff0c;已在Web3技术研发疆域缔造了卓越战绩。特别是在引领行业革新的关键时刻&#xff0c;集团于今年四月末震撼推出了颠覆性的Web3全套解决方案&#xff0c…

在 Ubuntu 12.10 安装 wxPython

安装 wxPython 可以使用 pip 工具&#xff0c;但在 Ubuntu 12.10 上需要首先安装 wxPython 的依赖项。请注意&#xff0c;Ubuntu 12.10 已于2013年终止支持&#xff0c;建议升级到更高版本的 Ubuntu。以下是在 Ubuntu 12.10 上安装 wxPython 的一般步骤&#xff1a; 一、问题背…

考研日常记录

由于实在太无聊了 &#xff0c; 所以记录以下考研备考日常 &#xff0c; 增加一点成就感 &#xff0c; 获得一点前进动力。 2024.4.18 周四 课程情况&#xff1a; 无课 时间规划&#xff1a; 上午&#xff1a;休息 下午&#xff1a; 事项耗时进度备注写作业1h复习英语单词…

深入浅出学习切片LOD——ArcGIS server模拟缓存切片(影像快显)

一、第一次实践 原理 免切片实现影像服务的模拟切片&#xff0c;主要原理是接收前端传过来的xyz(行列层级)以及切片方案&#xff0c;计算出该请求的切片的四至经纬度信息&#xff0c;通过mapserver的exportImage接口&#xff0c;传入每个模拟切片的四至经纬度信息得到图片返回…

Spark01

Spark01 一. Spark概述二. Spark环境部署 - Local三. Spark环境部署 - Standalone1. Standalone集群概述2. Standalone环境部署3. 测试环境 四. Spark环境部署 - Standalone-HA1. 安装部署Zookeeper1. 下载2. zookeeper安装3. 配置StandAlone-HA集群 五. Spark On YARN -- 重点…

ESP32S3在VScode中使用USB口调试

ESP32S3在VScode中使用USB口调试 安装USB驱动修改工程的配置文件launch.jsonsettings.json 启动GDB Server 安装USB驱动 在powershell中输入下面指令&#xff1a; Invoke-WebRequest https://dl.espressif.com/dl/idf-env/idf-env.exe -OutFile .\idf-env.exe; .\idf-env.exe…

js实现抽奖效果

<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>随机抽奖</title> </head> <body>…

锂电池寿命预测 | Matlab基于GRU门控循环单元的锂电池寿命预测

目录 预测效果基本介绍程序设计参考资料 预测效果 基本介绍 锂电池寿命预测 | Matlab基于GRU门控循环单元的锂电池寿命预测 Matlab基于GRU的锂电池剩余寿命预测 基于GRU的锂电池剩余寿命预测&#xff08;单变量&#xff09; 运行环境Matlab2020及以上 锂电池的剩余寿命预测是…

W11安装WSL2 ubuntu 攻略

W11安装WSL2 ubuntu 攻略 最近换了新电脑重新安装一遍wsl2&#xff0c;记录一下方便以后使用。 打开控制面板&#xff0c;选择程序 —— 启用或关闭Windows功能&#xff0c;勾选 适用于Linux的Windows子系统还有虚拟机平台选项&#xff0c;修改后需要重启 wsl2版本 wsl --se…

Rust腐蚀服务器清档多教程

Rust腐蚀服务器清档多教程 大家好我是艾西&#xff0c;一个做服务器租用的网络架构师。上期教了大家怎么搭建服务器以及安装插件等那么随着大家自己架设服或是玩耍的时间肯定会有小伙伴想要去新增开区数量或是把原本的服务器进行一些调整等&#xff0c;那么今天主要聊的就是怎…

如何在Windows 10中启用和使用上帝模式,这里有详细步骤

序言 上帝模式&#xff08;God Mode&#xff09;是一个特殊的文件夹&#xff0c;只在一个窗口中显示所有可用的操作设置。它可以节省搜索命令的时间&#xff0c;而无需知道通过“开始”菜单或“控制面板”查找命令的步骤。上帝模式默认情况下是隐藏的&#xff0c;所以我们需要…