【Linux】信号量
🎇Linux:
- 博客主页:一起去看日落吗
- 分享博主的在Linux中学习到的知识和遇到的问题
博主的能力有限,出现错误希望大家不吝赐教
- 分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持的意义,祝我们都能在鸡零狗碎里找到闪闪的快乐🌿🌞🐾。
🍁 🍃 🍂 🌿
目录
- 🍁 1. POSIX信号量
- 🍂1.1 信号量的原理
- 🍂1.2 信号量的概念
- 🍂1.3 信号量函数
- 🍁 2. 二元信号量模拟实现互斥功能
- 🍁 3. 基于环形队列的生产消费模型
- 🍂3.1 空间资源和数据资源
- 🍂3.2 生产者和消费者申请和释放资源
- 🍂3.3 必须遵守的两个规则
- 🍂3.4 代码实现
- 🍂3.5 信号量保护环形队列的原理
🍁 1. POSIX信号量
🍂1.1 信号量的原理
- 我们将可能会被多个执行流同时访问的资源叫做临界资源,临界资源需要进行保护否则会出现数据不一致等问题。
- 当我们仅用一个互斥锁对临界资源进行保护时,相当于我们将这块临界资源看作一个整体,同一时刻只允许一个执行流对这块临界资源进行访问。
- 但实际我们可以将这块临界资源再分割为多个区域,当多个执行流需要访问临界资源时,如果这些执行流访问的是临界资源的不同区域,那么我们可以让这些执行流同时访问临界资源的不同区域,此时不会出现数据不一致等问题。
🍂1.2 信号量的概念
信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
每个执行流在进入临界区之前都应该先申请信号量,申请成功就有了操作特点的临界资源的权限,当操作完毕后就应该释放信号量。
信号量的PV操作:
- P操作:我们将申请信号量称为P操作,申请信号量的本质就是申请获得临界资源中某块资源的使用权限,当申请成功时临界资源中资源的数目应该减一,因此P操作的本质就是让计数器减一。
- V操作:我们将释放信号量称为V操作,释放信号量的本质就是归还临界资源中某块资源的使用权限,当释放成功时临界资源中资源的数目就应该加一,因此V操作的本质就是让计数器加一。
- PV操作必须是原子操作
多个执行流为了访问临界资源会竞争式的申请信号量,因此信号量是会被多个执行流同时访问的,也就是说信号量本质也是临界资源。
但信号量本质就是用于保护临界资源的,我们不可能再用信号量去保护信号量,所以信号量的PV操作必须是原子操作。
注意: 内存当中变量的++、–操作并不是原子操作,因此信号量不可能只是简单的对一个全局变量进行++、–操作。
- 申请信号量失败被挂起等待
当执行流在申请信号量时,可能此时信号量的值为0,也就是说信号量描述的临界资源已经全部被申请了,此时该执行流就应该在该信号量的等待队列当中进行等待,直到有信号量被释放时再被唤醒。
注意: 信号量的本质是计数器,但不意味着只有计数器,信号量还包括一个等待队列。
🍂1.3 信号量函数
- 初始化信号量
初始化信号量的函数叫做sem_init
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数说明:
- sem:需要初始化的信号量。
- pshared:传入0值表示线程间共享,传入非零值表示进程间共享。
- value:信号量的初始值(计数器的初始值)。
返回值说明:
- 初始化信号量成功返回0,失败返回-1。
注意: POSIX信号量和System V信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的,但POSIX信号量可以用于线程间同步。
- 销毁信号量
销毁信号量的函数叫做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,信号量的值保持不变。
🍁 2. 二元信号量模拟实现互斥功能
信号量本质是一个计数器,如果将信号量的初始值设置为1,那么此时该信号量叫做二元信号量。
信号量的初始值为1,说明信号量所描述的临界资源只有一份,此时信号量的作用基本等价于互斥锁。
下面我们实现一个多线程抢票系统,其中我们用二元信号量模拟实现多线程互斥。
我们在主线程当中创建四个新线程,让这四个新线程执行抢票逻辑,并且每次抢完票后打印输出此时剩余的票数,其中我们用全局变量tickets记录当前剩余的票数,此时tickets是会被多个执行流同时访问的临界资源,在下面的代码中我们并没有对tickets进行任何保护操作。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>int tickets = 2000;
void* TicketGrabbing(void* arg)
{std::string name = (char*)arg;while (true){if (tickets > 0){usleep(1000);std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;}else{break;}}std::cout << name << " quit..." << std::endl;pthread_exit((void*)0);
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}
运行代码后可以看到,线程打印输出剩余票数时出现了票数剩余为负数的情况,这是不符合我们预期的。
下面我们在抢票逻辑当中加入二元信号量,让每个线程在访问全局变量tickets之前先申请信号量,访问完毕后再释放信号量,此时二元信号量就达到了互斥的效果。
#include <iostream>
#include <string>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>class Sem{
public:Sem(int num){sem_init(&_sem, 0, num);}~Sem(){sem_destroy(&_sem);}void P(){sem_wait(&_sem);}void V(){sem_post(&_sem);}
private:sem_t _sem;
};Sem sem(1); //二元信号量
int tickets = 2000;
void* TicketGrabbing(void* arg)
{std::string name = (char*)arg;while (true){sem.P();if (tickets > 0){usleep(1000);std::cout << name << " get a ticket, tickets left: " << --tickets << std::endl;sem.V();}else{sem.V();break;}}std::cout << name << " quit..." << std::endl;pthread_exit((void*)0);
}int main()
{pthread_t tid1, tid2, tid3, tid4;pthread_create(&tid1, nullptr, TicketGrabbing, (void*)"thread 1");pthread_create(&tid2, nullptr, TicketGrabbing, (void*)"thread 2");pthread_create(&tid3, nullptr, TicketGrabbing, (void*)"thread 3");pthread_create(&tid4, nullptr, TicketGrabbing, (void*)"thread 4");pthread_join(tid1, nullptr);pthread_join(tid2, nullptr);pthread_join(tid3, nullptr);pthread_join(tid4, nullptr);return 0;
}
运行代码后就不会出现剩余票数为负的情况了,因为此时同一时刻只会有一个执行流对全局变量tickets进行访问,不会出现数据不一致的问题。
🍁 3. 基于环形队列的生产消费模型
🍂3.1 空间资源和数据资源
生产者关注的是空间资源,消费者关注的是数据资源
对于生产者和消费者来说,它们关注的资源是不同的:
- 生产者关注的是环形队列当中是否有空间(blank),只要有空间生产者就可以进行生产。
- 消费者关注的是环形队列当中是否有数据(data),只要有数据消费者就可以进行消费。
blank_sem和data_sem的初始值设置
现在我们用信号量来描述环形队列当中的空间资源(blank_sem)和数据资源(data_sem),在我们初始信号量时给它们设置的初始值是不同的:
- blank_sem的初始值我们应该设置为环形队列的容量,因为刚开始时环形队列当中全是空间。
- data_sem的初始值我们应该设置为0,因为刚开始时环形队列当中没有数据。
🍂3.2 生产者和消费者申请和释放资源
生产者申请空间资源,释放数据资源
对于生产者来说,生产者每次生产数据前都需要先申请blank_sem:
- 如果blank_sem的值不为0,则信号量申请成功,此时生产者可以进行生产操作。
- 如果blank_sem的值为0,则信号量申请失败,此时生产者需要在blank_sem的等待队列下进行阻塞等待,直到环形队列当中有新的空间后再被唤醒。
当生产者生产完数据后,应该释放data_sem:
- 虽然生产者在进行生产前是对blank_sem进行的P操作,但是当生产者生产完数据,应该对data_sem进行V操作而不是blank_sem。
- 生产者在生产数据前申请到的是blank位置,当生产者生产完数据后,该位置当中存储的是生产者生产的数据,在该数据被消费者消费之前,该位置不再是blank位置,而应该是data位置。
- 当生产者生产完数据后,意味着环形队列当中多了一个data位置,因此我们应该对data_sem进行V操作。
消费者申请数据资源,释放空间资源
对于消费者来说,消费者每次消费数据前都需要先申请data_sem:
- 如果data_sem的值不为0,则信号量申请成功,此时消费者可以进行消费操作。
- 如果data_sem的值为0,则信号量申请失败,此时消费者需要在data_sem的等待队列下进行阻塞等待,直到环形队列当中有新的数据后再被唤醒。
当消费者消费完数据后,应该释放blank_sem:
- 虽然消费者在进行消费前是对data_sem进行的P操作,但是当消费者消费完数据,应该对blank_sem进行V操作而不是data_sem。
- 消费者在消费数据前申请到的是data位置,当消费者消费完数据后,该位置当中的数据已经被消费过了,再次被消费就没有意义了,为了让生产者后续可以在该位置生产新的数据,我们应该将该位置算作blank位置,而不是data位置。
- 当消费者消费完数据后,意味着环形队列当中多了一个blank位置,因此我们应该对blank_sem进行V操作。
🍂3.3 必须遵守的两个规则
在基于环形队列的生产者和消费者模型当中,生产者和消费者必须遵守如下两个规则
第一个规则:生产者和消费者不能对同一个位置进行访问。
生产者和消费者在访问环形队列时:
- 如果生产者和消费者访问的是环形队列当中的同一个位置,那么此时生产者和消费者就相当于同时对这一块临界资源进行了访问,这当然是不允许的。
- 而如果生产者和消费者访问的是环形队列当中的不同位置,那么此时生产者和消费者是可以同时进行生产和消费的,此时不会出现数据不一致等问题。
第二个规则:无论是生产者还是消费者,都不应该将对方套一个圈以上。
- 生产者从消费者的位置开始一直按顺时针方向进行生产,如果生产者生产的速度比消费者消费的速度快,那么当生产者绕着消费者生产了一圈数据后再次遇到消费者,此时生产者就不应该再继续生产了,因为再生产就会覆盖还未被消费者消费的数据。
- 消费者从生产者的位置开始一直按顺时针方向进行消费,如果消费者消费的速度比生产者生产的速度快,那么当消费者绕着生产者消费了一圈数据后再次遇到生产者,此时消费者就不应该再继续消费了,因为再消费就会消费到缓冲区中保存的废弃数据。
🍂3.4 代码实现
#pragma once#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <vector>#define NUM 8template<class T>
class RingQueue
{
private://P操作void P(sem_t& s){sem_wait(&s);}//V操作void V(sem_t& s){sem_post(&s);}
public:RingQueue(int cap = NUM): _cap(cap), _p_pos(0), _c_pos(0){_q.resize(_cap);sem_init(&_blank_sem, 0, _cap); //blank_sem初始值设置为环形队列的容量sem_init(&_data_sem, 0, 0); //data_sem初始值设置为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 %= _cap;}//从环形队列获取数据(消费者调用)void Pop(T& data){P(_data_sem); //消费者关注数据资源data = _q[_c_pos];V(_blank_sem);//更新下一次消费的位置_c_pos++;_c_pos %= _cap;}
private:std::vector<T> _q; //环形队列int _cap; //环形队列的容量上限int _p_pos; //生产位置int _c_pos; //消费位置sem_t _blank_sem; //描述空间资源sem_t _data_sem; //描述数据资源
};
相关说明:
- 当不设置环形队列的大小时,我们默认将环形队列的容量上限设置为8。
- 代码中的RingQueue是用vector实现的,生产者每次生产的数据放到vector下标为p_pos的位置,消费者每次消费的数据来源于vector下标为c_pos的位置。
- 生产者每次生产数据后p_pos都会进行++,标记下一次生产数据的存放位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果。
- 消费者每次消费数据后c_pos都会进行++,标记下一次消费数据的来源位置,++后的下标会与环形队列的容量进行取模运算,实现“环形”的效果。
- p_pos只会由生产者线程进行更新,c_pos只会由消费者线程进行更新,对这两个变量访问时不需要进行保护,因此代码中将p_pos和c_pos的更新放到了V操作之后,就是为了尽量减少临界区的代码。
为了方便理解,我们这里实现单生产者、单消费者的生产者消费者模型。于是在主函数我们就只需要创建一个生产者线程和一个消费者线程,生产者线程不断生产数据放入环形队列,消费者线程不断从环形队列里取出数据进行消费。
#include "RingQueue.hpp"void* Producer(void* arg)
{RingQueue<int>* rq = (RingQueue<int>*)arg;while (true){sleep(1);int data = rand() % 100 + 1;rq->Push(data);std::cout << "Producer: " << data << std::endl;}
}
void* Consumer(void* arg)
{RingQueue<int>* rq = (RingQueue<int>*)arg;while (true){sleep(1);int data = 0;rq->Pop(data);std::cout << "Consumer: " << data << std::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;
}
相关说明:
- 环形队列要让生产者线程向队列中Push数据,让消费者线程从队列中Pop数据,因此这个环形队列必须要让这两个线程同时看到,所以我们在创建生产者线程和消费者线程时,需要将环形队列作为线程执行例程的参数进行传入。
- 代码中生产者生产数据就是将获取到的随机数Push到环形队列,而消费者就是从环形队列Pop数据,为了便于观察,我们可以将生产者生产的数据和消费者消费的数据进行打印输出。
生产者消费者步调一致
如果想步调不一致只需要把sleep删掉就可以实现
🍂3.5 信号量保护环形队列的原理
在blank_sem和data_sem两个信号量的保护后,该环形队列中不可能会出现数据不一致的问题。
因为只有当生产者和消费者指向同一个位置并访问时,才会导致数据不一致的问题,而此时生产者和消费者在对环形队列进行写入或读取数据时,只有两种情况会指向同一个位置:
- 环形队列为空时。
- 环形队列为满时。
但是在这两种情况下,生产者和消费者不会同时对环形队列进行访问:
- 当环形队列为空的时,消费者一定不能进行消费,因为此时数据资源为0。
- 当环形队列为满的时,生产者一定不能进行生产,因为此时空间资源为0。
也就是说,当环形队列为空和满时,我们已经通过信号量保证了生产者和消费者的串行化过程。而除了这两种情况之外,生产者和消费者指向的都不是同一个位置,因此该环形队列当中不可能会出现数据不一致的问题。并且大部分情况下生产者和消费者指向并不是同一个位置,因此大部分情况下该环形队列可以让生产者和消费者并发的执行
相关文章:

【Linux】信号量
🎇Linux: 博客主页:一起去看日落吗分享博主的在Linux中学习到的知识和遇到的问题博主的能力有限,出现错误希望大家不吝赐教分享给大家一句我很喜欢的话: 看似不起波澜的日复一日,一定会在某一天让你看见坚持…...

android-java同步方法和异步方法
接口 Java接口是一系列方法的声明,是一些方法特征的集合,一个接口只有方法的特征没有方法的实现,因此这些方法可以在不同的地方被不同的类实现,而这些实现可以具有不同的行为(功能)。 两种含义:…...

Flask入门(5):请求和响应
目录5.请求和响应5.1 请求5.2 响应5.请求和响应 5.1 请求 request对象封装解析了请求报文中的数据,其大部分功能是由依赖包werkzeug完成的,并且每个request对象都是线程隔离的,保证了数据的安全性。 request对象的属性 1.request.method …...

记进组后第五次组会汇报
2023年2月14日 日记一、小组组会二、实验室组会1、汇报内容(1)参考文献(2)CQF机制a.研究现状b.相关思考(3)研究计划2、汇报反馈一、小组组会 上午十点整,小组组会开始,有两个同学我…...

nil Foundation的Placeholder证明系统(2)
前序博客: nil Foundation的Placeholder证明系统(1) nil; Foundation团队2022年11月论文《Placeholder证明系统》。[2022年11月29日版本] 8. 优化 8.1 Batched FRI 不同于单独检查每个commitment,可对其进行FRI聚合。如对多项…...

QHash源码解读
QT版本 v5.12.10 元素 // 重点说明QHashData的函数,QHashData是QHash的基础 struct QHashData {struct Node {Node *next;uint h;};Node *fakeNext; // 永为nullNode **buckets; // Node *数组QtPrivate::RefCount ref;int size; // node个数int nodeSize; /…...

【Unity细节】RigidBody中Dynamic和Kinematic的区别
👨💻个人主页:元宇宙-秩沅 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 本文由 秩沅 原创 收录于专栏:unity细节和bug ⭐Dynamic和Kinematic的区别⭐ 文章目录⭐Dynamic和Kinematic的区别⭐dz…...

【C++、数据结构】哈希 — 闭散列与哈希桶的模拟实现
文章目录📖 前言1. STL中哈希表的两个应用⚡1.1 🌟unordered_set1.2 🌟unordered_map2. 常见查找的性能对比💥3. 哈希表模拟实现🏁3.1 哈希的概念:3.2 哈希函数:3.3 哈希冲突:3.4 闭…...

vue 开发环境 卸载node 版本 切换新的 node 版本 mac电脑
注意:操作的机器当前是mac,先卸载,再安装 1.查看现有 node 版本 node -v2.卸载现有 node 版本, 1.卸载从node官网下载pkg安装的node sudo rm -rf /usr/local/{bin/{node,npm},lib/node_modules/npm,lib/node,share/man/*/node…...

在Linux和Windows上安装Nacos-2.1.1
记录:377场景:在CentOS 7.9操作系统安装Nacos-2.1.1。在Windows操作系统上安装Nacos-2.1.1。Nacos:Nacos: Dynamic Naming and Configuration Service。Nacos提供动态配置服务、服务发现及管理、动态DNS服务功能。版本:JDK 1.8 Na…...

解决QML debugging is enabled.Only use this in a safe environment.警告
系列文章目录 文章目录系列文章目录前言一、警告原因二、解决办法参考前言 我试图运行一个非常简单的程序,当单击退出按钮时关闭窗口,但获取以下输出,前提是包含按钮的应用程序窗口不显示: 您已启用QML调试(实际上它默认启用)&…...

华为OD机试真题JAVA实现【N进制减法】真题+解题思路+代码(20222023)
🔥系列专栏 华为OD机试(JAVA)真题目录汇总华为OD机试(Python)真题目录汇总华为OD机试(C++)真题目录汇总华为OD机试(JavaScript)真题目录汇总文章目录 🔥系列专栏题目输入输出描述示例一输入输出说明解题思路Code代码运行结果版权说明<...

ACM第一周---周训---题目合集.
🚀write in front🚀 📝个人主页:认真写博客的夏目浅石.CSDN 🎁欢迎各位→点赞👍 收藏⭐️ 留言📝 📣系列专栏:ACM周训练题目合集.CSDN 💬总结:…...

SCI学术论文的基本架构,以及Results、Discussion、Conclusion这三者的区别
SCI论文七大部分,各自应包含哪些内容 SCI写作——论文的结构 一篇SCI论文的大致框架包括Title, Abstract, Introduction, Methods/Methodology, Results, Discussion, Conclusion。不同的学科会有细微的变化,但大体框架基本不变。 1、标题Title 标题用…...

二叉树性质
在二叉树的第i层上至多有2^(i-1)个结点(i≥1)深度为k的二叉树至多有2^k-1个结点(k≥1)对任何一颗二叉树T,如果其叶子数为n0,度为2的结点数位n2,则n0n21满二叉树ÿ…...

二维数组操作示例
给定一个二维字符串数组,求对其按每个一维数组升序排列并按矩阵输出 //创建 String[][] twoDimension {{"A1","A2","A3"},{"B1","B2","B3"}}; List<String> arrayToList null; List<St…...

Spring Boot邮件发送(powernode CD2207)(内含教训视频+源代码)
Spring Boot邮件发送(powernode CD2207)(内含教训视频源代码) 教学视频源代码下载链接地址:https://download.csdn.net/download/weixin_46411355/87452056 目录Spring Boot邮件发送(powernode CD2207&…...

FortiTalk | “三英论安全”之OT安全热门话题解读
OT安全热门话题解读 在数字化转型时代,OT/IT融合已经成为主旋律,可能很多人还没有意识到“工厂”已经不是以前的“工厂”。从封闭走向互联、从现场走向远程、从手动走向自动,这种变革带来的不仅是便捷和效率,更潜藏着巨大的网络安…...

前端开发:关于diff算法详解
前言 前端开发中,关于JS原生的内容和前端算法相关的内容一直都是前端工作中的核心,不管是在实际的前端业务开发还是前端求职面试,都是非常重要且必备的内容。那么本篇博文来分享一个关于前端开发中必备内容:diff算法,d…...

如何为报表开发工具 FastReport .NET 设置 Apache 2 Web 服务器?
FastReport .NET是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案,使用FastReport .NET可以创建独立于应用程序的.NET报表,同时FastReport .Net支持中文、英语等14种语言,可以让你的产品保证真正的国际性。专业版和企业版包括Fast…...

华为OD机试真题JAVA实现【出租车计费】真题+解题思路+代码(20222023)
🔥系列专栏 华为OD机试(JAVA)真题目录汇总华为OD机试(Python)真题目录汇总华为OD机试(C++)真题目录汇总华为OD机试(JavaScript)真题目录汇总文章目录 🔥系列专栏题目输入输出示例一输入输出说明示例二输入输出说明...

MySQL 查看版本的 5 种方法
MySQL 提供了几种用于查看服务器版本的方法,本文给大家做个简单的介绍。 方法一:登录 MySQL 每次通过 mysql 客户端连接服务器之后,都会显示一个欢迎信息,里面包含了服务器的版本: mysql -uroot Enter password: **…...

【软件测试】稳定性测试怎么做,这篇文章彻底讲透了~
稳定性对产品的重要性不言而喻。 而作为质量保障,在稳定性测试方面的探索也在不断演化。记得两年前我们做稳定性测试还是基于恒定的压力,7*24小时长时间运行,关注的指标无非是吞吐量TPS的抖动、响应时间的变化趋势,以及各种资源是…...

Leetcode:198. 打家劫舍、213. 打家劫舍 II、337. 打家劫舍 III(C++)
目录 198. 打家劫舍 问题描述: 实现代码与解析: 动态规划(版本一): 原理思路: 动态规划(版本二): 原理思路: 213. 打家劫舍 II 问题描述:…...

【每日随笔】手指训练 ( 手指训练作用 | 哪些人需要手指训练 | 手指操 | 手指康复训练器材 )
文章目录一、手指训练作用二、哪些人需要手指训练三、手指操四、手指康复训练器材产品需求探索 , 研究下手指训练的市场 , 前景 , 是否可以开发 ; 一、手指训练作用 手指训练作用 : 改善 上肢协调性手眼 协调性训练提高 手指 抓握 能力提高 手指 灵活性提高 上肢运动 准确性 和…...

ATR指标在外汇交易中的另类运用方法
当涉及到外汇交易时,有许多不同的指标可以使用。然而,ATR指标可能是一个被低估的工具,可以帮助您发现有利可图的交易机会。本文将介绍ATR指标是什么,如何使用它来识别价格波动和制定交易策略,以及如何在外汇市场中另辟…...

SQL Server 数据批量导出处理
在实际项目环境中,有时会遇到需要将大量数据(这里所指百万级别以上的数据量)从一台服务器迁移到另外一台数据库服务器的情况。SQL Server有很多方式可以进行数据迁移:备份还原、导入/导出数据、生成脚本(包含数据&…...

虹科分享 | CANopen协议基础知识——LSS服务
CANopen是一种架构在CAN串行总线系统上的高层通讯协议,常被用于嵌入式系统与工业控制领域,包括电机控制、机器人制造、医疗、汽车等多个行业领域。本篇文章将主要介绍CANopen的LSS服务。 一. LSS概述 Layer setting service (LSS)是CANopen的设置服务与…...

JS混淆和解混淆
在今天的数字时代,知识产权和商业机密对于企业的成功非常重要。JavaScript代码可以包含许多敏感信息,例如商业逻辑、客户数据和加密密钥。为了保护这些重要信息,JavaScript混淆和解混淆已经成为一种必要的技术。 什么是JavaScript混淆&#…...

MySQL-数值函数
绝对值函数语法格式:ABS(X)例:查看三个数值的绝对值(负的绝对值为它的正整数,0的绝对值为0,正的绝对值为它本身)。mysql> select abs(2),abs(-32),abs(-0.5); ----------------------------- | abs(2) |…...