POSIX信号量

目录

信号量的原理

信号量函数

使用信号量实现线程互斥功能

基于环形队列的生产消费模型 

生产者和消费者必须遵守的两个规则


信号量的原理


通过之前的学习,我们知道有的资源可能会被多个执行流同时申请访问,我们将这种资源叫做临界资源,临界资源需要被保护,否则可能会出现数据不一致的问题。当一份临界资源只能被当作整体来访问时,也就是同一时刻只允许一个执行流对这个临资源进行访问,这时只需用一个互方锁就可对临界资源进行保护。但对于一些资源我们可以将它们再细分为多个区域, 这时临界资源就可以同时被多年执行流访问,但在访问前要申请信号量。

信号量的概念:本质是一个计数器,用于述描临界资源中资源数目,信号量能够更细粒度的对临界资源进行管理。
每个执行流在对临界资源进行访问前,都应申请信号量即P操作,申请成功后,执行流就拥有对该临界资源的访问权限,当操作完成后,应释放信号量即V操作。
 

PV操作:

P操作:执行流申请信号量的操作称为P操作,申请信号量的本质就是申请使用临界资源中某一区域资源的权限,当申请成功时,信号量的数目减一。
V操作:执行流释放信号量的操作称为V操作,释放信号量的本质就是归还使用临界资源中是一区域资源的权限, 当释放完成后,信号量的数目加一。

注意:PV操作为原子性。

当执行流申请信号量失败时被挂起等待

        当执行流在申请信号量时,可能会出现信号量的值为0的情况,也就是说临界资源中的所有区域都在被其它执行流在使用中,这时该执行流会申请信号量失败,而被放到申请信号量的队列中进行申请等待,直到有其它执行流释放信号量。


信号量函数


初始化信号量:sem_init()。
函数原型:

int sem_init(sem_t *sem,int pshared,unsigned int value);

参数说明:
sem:初始化的信号量。
pshared:传入0表示用于线程间通信,传入非0表示用于进程间通信。
value:信号量的初始值(计数器的不始值)。
返回值:
初始做功返回0、失败返回1。


销毁信号量:sem_destroy()。
函数原型:

int sem_destroy(sem_t *sem);

参数说明:
sem:需要全销毁的信号量。
返回值:销毁信号量成功返回0,失败返回-1。


申请信号量:sem_wait()。
函数原型:

int sem_wait(sem_t*sem);

参数说明:
sem申请的信号量。
返回值说明:
申请成功返回0,信号量值减一,申请失败返回-1,信号量值保持不变。

释放信号量:sem_post( )。
函数原型:

int sem_post(sem_t*sem);


参数说明:
sem需要释放的信号量。
返回值说明:
释放成功返回0,信号值加一,释放失败返回一,信号量值保持不变。


使用信号量实现线程互斥功能

信号量本质是一个计数器,如果将信号量的初始值设置为1,那么该信号量就叫二元信号量。
当信号量的值为1,说明该信号量表示的临界资源只有一份,只能被整体访问,那么描述该资源的信号量就相当于互斥锁,使用二元信号量实现多线程互斥。

未引入二元信号量时,多线程对临界资源的访问操作会导致数据不一致。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>using namespace std;int tickets = 3000;void *RobTickest(void *arg)
{string name = (char *)arg;while (true){if (tickets > 0){usleep(1000);cout << name << "抢到了一张票,剩余票数:" << --tickets << endl;}else{break;}}cout << name << "退出...!" << endl;pthread_exit((void *)0);
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, RobTickest, (void *)"线程 1");pthread_create(&tid2, nullptr, RobTickest, (void *)"线程 2");pthread_create(&tid3, nullptr, RobTickest, (void *)"线程 3");pthread_create(&tid4, nullptr, RobTickest, (void *)"线程 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

在引入二元信号量后,多线程对临界资源的访问操作具有互斥性。

#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>using namespace std;int tickets = 3000;class Sem
{
public:Sem(int nums){sem_init(&_sem, 0, nums);}~Sem(){sem_destroy(&_sem);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}private:sem_t _sem;
};
Sem sem(1);
void *RobTickest(void *arg)
{string name = (char *)arg;while (true){sem.P();if (tickets > 0){usleep(1000);cout << name << "抢到了一张票,剩余票数:" << --tickets << endl;sem.V();}else{sem.V();break;}}cout << name << "退出...!" << endl;pthread_exit((void *)0);
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, RobTickest, (void *)"线程 1");pthread_create(&tid2, nullptr, RobTickest, (void *)"线程 2");pthread_create(&tid3, nullptr, RobTickest, (void *)"线程 3");pthread_create(&tid4, nullptr, RobTickest, (void *)"线程 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}

 

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
using namespace std;
#define NUM 8template <class T>
class RingQueue
{
private:// p操作void P(sem_t &p){sem_wait(&p);}//V操作void V(sem_t &v){sem_post(&v);}public:RingQueue(int cap = NUM) : _cap(cap), _p_pos(0), _c_pos(0){_q.resize(_cap);sem_init(&_blank_sem, 0, _cap);//构造时空间信号量为环形队列的容量sem_init(&_data_sem, 0, 0);//构造时数据信号量为0}~RingQueue(){sem_destroy(&_blank_sem);sem_destroy(&_data_sem);}//生产者生产数据void Push(const T&data){P(_blank_sem);//申请空间资源_q[_p_pos]=data;V(_data_sem);//释放数据信号_p_pos++;_p_pos=_p_pos%_cap;}void Pop(T&data){P(_data_sem);//申请数据资源data=_q[_c_pos];V(_blank_sem);//释放空间资源_c_pos++;_c_pos=_c_pos%_cap;}private:vector<T> _q;     // 使用vector实现环形队列int _cap;         // 环形队列的容量int _p_pos;       // 生产的位置int _c_pos;       // 消费的位置sem_t _blank_sem; // 空间信号量sem_t _data_sem;  // 数据信号量
};

#include "RingQueue.hpp"void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}int main()
{srand((unsigned int)time(nullptr));pthread_t producer, consumer;RingQueue<int> *rq = new RingQueue<int>;pthread_create(&producer, nullptr, Producer, rq);pthread_create(&consumer, nullptr, Consumer, rq);pthread_join(producer, nullptr);pthread_join(consumer, nullptr);delete rq;return 0;
}

生产快,消费慢。

#include "RingQueue.hpp"void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);//生产1秒后,才消费int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}

消费快,生产慢。

void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}

基于环形队列的生产消费模型 

在使用环形队列实现生产消费模型的实现设计中,我们应明白生产者和消费者关心的资源是什么?
对于生产者和消费者它们关注的资源是不同的,生产者关注的是环形队列中是否有空间可以存放生产者生产的数据,只要有空间生产者可以执行生产任务。


消费者关注的是环形队列中是否有数据可以行消费,只要有数据消费者就可以获取数据。

对空间信号量的初始化和对数据信号量的初始化的设置

使用信号量在环形队列中描述空间资源和数据资源时,它们的信号量的初始值是不同的。

我们用blank_sem表示空间信号量,用data_sem表示数据信号量。
blank_sem空间信号量的初始值应为环形队列的容量,因为在生产初期,环形队列中没有数据,dota_sem数据信号量的初始值应为0,因为生产者没有执行生产,环形队列中没有数据。

生产者和消费者对信号量的申请与释放。

1.生产者申请空间资源,释放数据资源。
对生产者来说,在生产前都需要生申请空间资源blank_sem信号量。
如果生产者申请信号量成功,则可以执行生产操,如果请失败,则需要在blank_sem的等待队列下进行阻塞等待,直到环形队列中有可以存放生产数据的空间,才会被唤醒,执行生产操作,当生产者执行完生产操作后,应释放数据信号量data_sem。虽然生产者在执行生产操作前申请的是空间资源,但生产完数据后,应对数据资源data_sem信号量进行释放操作,而不是释放空间资源,因为此时空间资源的数目还是环形队列容量-1,即生产者生产完数据后,空间资源blank_sem信号量少了一个,而数据资源data_sem信号量多了一个,因此对data_sem资源进行释放才是合理。
2.消费者申请 数据资源,释放空间资。
对消费者来说,在消费前都要申请数据资源data_sem信号量信号量。
如果消费者申请信号量成功,则同以执行消费操作,如果申请失则,则需要在data_sem的等待队列下进行阻塞等待,直前环形队列中有可以进行消费的数据资源才会被唤醒,执行消费操作,当消费者执行完消费操作后,应释放空间信号量blank_sem;
虽然消费者在执行消费操作前申请的是数据资源,但消费完数据后,应对空间资源blank_sem信号量进行释放操作,而不是释放数据信号量。此时数据资源的数目为0,即消费者消费完数据后,数据资源就少了一个,而空间资源blank_sem信号量多了一个,因此空间资源信号量由消费者进行释放才是合理。

生产者和消费者必须遵守的两个规则

规则1:消费者和生产者不能同时访问同一个位置。
生产者和消费者在访问环形队列时,如果访的是同一个位置, 这是不允许的,可能会出现数据不一致性。

规则2:无论是生产者还是消费者释放操作不能使对方的信号量超过对方的的初始值,即不能套圈生产或套圈消费。
生产者从消费者的位置,一直按顺时针方向进行生产,如果生产的速度大于消费的速度,那么用于存放生产数据的位置,很快就会和消费数据的位置重叠了。此时生产者就不该执行生产操作了,因为再生产就会覆盖还未被消费的数据,造成数据丢失。
同理,消费者从生产者的位置,一直按顺时针方向进生消费,如果消费的速度大于生产的速度,那么消费的位置很快就会和用于存放生产数据的位置重叠。此时消费者应停止消费操作,因为再消费就会就会造二次消费 。

 设计思想:
默认将环形队列的容量上限设置为6。
代码中使用STL库中的vector实现环形队列,生产者每次生产的数据放到vector下标为_p_pos的位置,消费者每次消费的数据来源于vector下标为_c_pos的位置。
生产者每次生产数据后_p_pos都会进行++,标记下一次生产数据的存放位置,++后的下标通过进行取模运算实现“环形”的效果。
消费者每次消费数据后_c_pos都会进行++,标记下一次消费数据的来源位置,++后的下标通过进行取模运算实现“环形”的效果。
_p_pos由生产者线程进行更新,_c_pos由消费者线程进行更新,对这两个变量访问时不需要进行保护,因此代码中将_p_pos和_c_pos的更新放到了V操作之后,就是为了尽量减少临界区的代码。
为了方便理解,我们这里实现单生产者、单消费者的生产者消费者模型。于是在主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。

基于环形队列的单生产者、单消费者的生产者消费者模型

实现代码:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>
using namespace std;
#define NUM 6template <class T>
class RingQueue
{
private:// p操作void P(sem_t &p){sem_wait(&p);}//V操作void V(sem_t &v){sem_post(&v);}public:RingQueue(int cap = NUM) : _cap(cap), _p_pos(0), _c_pos(0){_q.resize(_cap);sem_init(&_blank_sem, 0, _cap);//构造时空间信号量为环形队列的容量sem_init(&_data_sem, 0, 0);//构造时数据信号量为0}~RingQueue(){sem_destroy(&_blank_sem);sem_destroy(&_data_sem);}//生产者生产数据void Push(const T&data){P(_blank_sem);//申请空间资源_q[_p_pos]=data;V(_data_sem);//释放数据信号_p_pos++;_p_pos=_p_pos%_cap;}void Pop(T&data){P(_data_sem);//申请数据资源data=_q[_c_pos];V(_blank_sem);//释放空间资源_c_pos++;_c_pos=_c_pos%_cap;}private:vector<T> _q;     // 使用vector实现环形队列int _cap;         // 环形队列的容量int _p_pos;       // 生产的位置int _c_pos;       // 消费的位置sem_t _blank_sem; // 空间信号量sem_t _data_sem;  // 数据信号量
};

生产和消费同步

#include "RingQueue.hpp"void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{sleep(1);RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}int main()
{srand((unsigned int)time(nullptr));pthread_t producer, consumer;RingQueue<int> *rq = new RingQueue<int>;pthread_create(&producer, nullptr, Producer, rq);pthread_create(&consumer, nullptr, Consumer, rq);pthread_join(producer, nullptr);pthread_join(consumer, nullptr);delete rq;return 0;
}

 

 生产快,消费慢。

void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{sleep(1);RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}

 

 

消费快,生产慢。

void *Producer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){sleep(1);int data = rand() % 100 + 3;rq->Push(data);cout << "生产数据:" << data << endl;}
}
void *Consumer(void *arg)
{RingQueue<int> *rq = (RingQueue<int> *)arg;while (true){int data = rand() % 100 + 3;rq->Pop(data);cout << "消费数据:" << data << endl;}
}

 即使消费再快也要等到队列中有数据才能消费。

总结:

在用c++语言实现基于信号量和环形队列的生产消费模型的原理中,只声明空间信号量和数据信号量如何实现同步与互斥?

在用c++语言实现基于信号量和环形队列的生产消费模型中,只声明空间信号量和数据信号量,可以通过以下方式实现同步和互斥:

  1. 空间信号量:表示可用空间的数量。在生产者线程中,每次放入一个元素时,空间信号量减1;在消费者线程中,每次取出一个元素时,空间信号量加1。当空间信号量为0时,表示环形队列已满,生产者线程需要等待;当空间信号量为环形队列长度时,表示环形队列为空,消费者线程需要等待。

  2. 数据信号量:表示已有数据的数量。在生产者线程中,每次放入一个元素时,数据信号量加1;在消费者线程中,每次取出一个元素时,数据信号量减1。当数据信号量为0时,表示环形队列为空,消费者线程需要等待;当数据信号量为环形队列长度时,表示环形队列已满,生产者线程需要等待。

  3. 互斥:为了避免多个线程同时对环形队列进行操作,可以使用互斥锁来实现。在生产者和消费者线程中,通过获取互斥锁来独占对环形队列的访问权。当一个线程拥有互斥锁时,其他线程需要等待。

由于只使用了空间信号量和数据信号量来实现同步和互斥,这种实现方法需要考虑很多特殊情况,比如队列为空/满时的特殊处理、互斥锁的释放等问题。如果使用信号量和互斥锁的完整实现方式,可以更加简单和可靠地实现生产者-消费者模型。

 

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

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

相关文章

记录一次线下渗透电气照明系统(分析与实战)

项目地址:https://github.com/MartinxMax/S-Clustr 注意 本次行动未造成任何设备损坏,并在道德允许范围内测试 >ethical hacking< 发现过程 在路途中,发现一个未锁的配电柜,身为一个电工自然免不了好奇心(非专业人士请勿模仿,操作不当的话220V人就直了) 根据照片,简…

算法__中缀表达式转后缀表达式

文章目录 概念算法中缀转后缀案例讲解 后缀算值案例讲解 概念 中缀表达式就是日常生活中遇到的运算表达式&#xff0c;例如a*(b-c)&#xff1b; 后缀表达式则是另一种运算表达式&#xff0c;其特点在于运算符在对象后&#xff0c;且表达式中没有括号&#xff0c;例如abc-* 算…

观察者模式-对象间的联动

有个商城小程序&#xff0c;用户希望当有新品上市的时候能通知他们。这样用户就可以不要时刻盯着小程序了。在这个场景中&#xff0c;用户向小程序订阅了一个服务——发送新品短信。小程序在有新品上线时负责向订阅客户发出这个消息。 这就是发布-订阅模式&#xff0c;也称观察…

Python基础教程:内置函数之字典函数的使用方法

嗨喽~大家好呀&#xff0c;这里是魔王呐 ❤ ~! python更多源码/资料/解答/教程等 点击此处跳转文末名片免费获取 len(字典名)&#xff1a; 返回键的个数&#xff0c;即字典的长度 # len(字典名)&#xff1a; # 返回键的个数&#xff0c;即字典的长度dic {a:123,b:456,c:789…

Linux——shell外壳程序

shell外壳程序 1. 什么是shell外壳程序 Linux严格意义上说的是一个操作系统&#xff0c;我们称之为“核心 “ &#xff0c;但我们一般用户&#xff0c;不能直接使用核心。 而是通过核心的“外壳”程序&#xff0c;也就是所谓的shell。 shell是所有外壳程序的统称 平时程序员…

CLIP模型原理

CLIP模型 CLIP(Contrastive Language-Image Pre-Training) 模型是 OpenAI 在 2021 年初发布的用于匹配图像和文本的预训练神经网络模型&#xff0c;是近年来在多模态研究领域的经典之作。OpenAI 收集了 4 亿对图像 - 文本对&#xff08;一张图像和它对应的文本描述&#xff09…

shell的for循环与结构化

shell笔记 列表for循环不带列表for循环for循环举例1.例1 所有文件名大写替换为小写2. 例2 读取/etc/passwd文件&#xff0c;依次输出ip段3. 例3 读取/etc/hosts内容for循环&#xff0c;执行ping4. 例4 循环ip列表&#xff0c;输出对应编号5. 例5 批量添加用户 break1. 例1 brea…

FPGA project : IIC_wr_eeprom

简介&#xff1a; 简单双向二线制&#xff0c;同步串行总线。 scl&#xff1a;串行时钟线&#xff0c;用于同步通讯数据。 sda&#xff1a;双向串行数据线。 物理层&#xff1a; 1&#xff0c;支持挂载多设备。 2&#xff0c;二线制。 3&#xff0c;每个设备有其单独的地…

安装visual studio报错“无法安装msodbcsql“

在安装visual studio2022时安装完成后提示无法安装msodbcsql, 查看日志文件详细信息提示&#xff1a;指定账户已存在。 未能安装包“msodbcsql,version17.2.30929.1,chipx64,languagezh-CN”。 搜索 URL https://aka.ms/VSSetupErrorReports?qPackageIdmsodbcsql;PackageActi…

分布式缓存Spring Cache

一、缓存里的数据如何和数据库的数据保持一致&#xff1f; 缓存数据一致性1)、双写模式2)、失效模式1、缓存数据一致性-双写模式 2、 缓存数据一致性-失效模式 我们系统的一致性解决方案: 1、缓存的所有数据都有过期时间&#xff0c;数据过期下一次查询触发主动更新 2、读写数据…

Android 10 中的隐私权变更

Android 10 中的隐私权变更 重大变更外部存储访问权限范围限定为应用文件和媒体在后台运行时访问设备位置信息需要权限以 Android 9 或更低版本为目标平台时自动授予访问权限在设备升级到 Android 10 后访问针对从后台启动 Activity 的限制标识符和数据移除了联系人亲密程度信息…

JIT耗时优化

优质博文&#xff1a;IT-BLOG-CN 一、背景 业务流量突增&#xff0c;机器直接接入大量流量QPS2000&#xff0c;JIT和GC会消耗太多CPU资源&#xff0c;导致1-2分钟时间内的请求超时导致异常&#xff0c;因此采用流量预热的方式&#xff0c;让机器逐步接入流量&#xff0c;需要预…

go语言Array 与 Slice

有的语言会把数组用作常用的基本的数据结构&#xff0c;比如 JavaScript&#xff0c;而 Golang 中的数组(Array)&#xff0c;更倾向定位于一种底层的数据结构&#xff0c;记录的是一段连续的内存空间数据。但是在 Go 语言中平时直接用数组的时候不多&#xff0c;大多数场景下我…

【Lua语法】字符串

Lua语言中的字符串是不可变值。不能像在C语言中那样直接改变某个字符串中的某个字符&#xff0c;但是可以通过创建一个新字符串的方式来达到修改的目的 print(add2(1 , 2 ,15,3))a "no one"b string.gsub(a , "no" , "on1111")print(a) print…

微软正式发布开源应用平台 Radius平台

“ 10 月 18 日&#xff0c;微软 Azure 孵化团队正式发布开源应用平台 Radius&#xff0c;该平台将应用程序置于每个开发阶段的中心&#xff0c;重新定义应用程序的构建、管理与理解方式。” 简单的概括就是&#xff0c;它和Kubernetes不一样&#xff0c;Radius将应用程序放在每…

C语言--程序环境和预处理

前言 本章就是c语言的最后一个板块了&#xff0c;学完这章节&#xff0c;我们将知道写出的代码如何变成可执行程序的&#xff0c;这是非常重要的一个章节&#xff0c;那让我们一起进入本章的学习吧。 本章重点&#xff1a; 程序的翻译环境程序的执行环境详解&#xff1a;C语言程…

周立功ZCANPRO简介和使用

ZCANPRO目录 周立功ZCANPRO简介一、软件安装ZCANPRO官网链接&#xff1a;驱动官网链接 二、ZCANPRO使用1.设备管理2.选择CAN、CANFD波特率计算器使用方法&#xff08;可选&#xff09; 3.新建视图CAN视图DBC视图 4.发送数据普通发送DBC发送 三、高级功能UDS诊断 周立功ZCANPRO简…

【java爬虫】使用selenium获取某交易所公司半年报数据

引言 上市公司的财报数据一般都会进行公开&#xff0c;我们可以在某交易所的官方网站上查看这些数据&#xff0c;由于数据很多&#xff0c;如果只是手动收集的话可能会比较耗时耗力&#xff0c;我们可以采用爬虫的方法进行数据的获取。 本文就介绍采用selenium框架进行公司财…

HTML选项框的设计以及根据不同选项的值对应不同的事件

文章目录 HTML选项框的设计JS根据不同的选项框对应出不同的事件 HTML选项框的设计 在前端页面的设计中&#xff0c;多选框的设计用select标签完成实现 全部选项都显示的选项框 <form><select multiple"multiple"><option></option><opti…

视频怎么压缩?视频过大这样压缩变小

在日常生活中&#xff0c;我们常常会遇到需要压缩视频的情况&#xff0c;视频压缩不仅可以减小文件大小&#xff0c;方便存储和传输&#xff0c;还可以在保证质量的同时&#xff0c;满足不同的使用需求。那么&#xff0c;如何有效地压缩视频呢&#xff1f; 方法一&#xff1a;嗨…