Linux — 线程池及多线程结尾

news2024/7/6 20:02:23

目录

一、线程池

线程池的应用场景:

线程池示例:

二、线程安全的单例模式

什么是单例模式

什么是设计模式

单例模式的特点

饿汉实现方式和懒汉实现方式

三、STL,智能指针和线程安全

 四、其他常见的各种锁

五、读者写者问题(了解)

读写锁

 读写锁接口


 

一、线程池

线程池一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

一句话就是我们事先创建好一批线程,有任务到来时就唤醒线程执行任务,没有任务时就休眠。

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误。

线程池示例:

* 1. 创建固定数量线程组成线程池,循环从任务队列中获取任务对象
* 2. 获取到任务对象后,执行任务对象中的任务接口
 

//pthreadPool.hpp
#pragma once 

#include "thread.hpp"
#include "lockGroud.hpp"
#include "Task.hpp"

#include <vector>
#include <queue>
#include <unistd.h>

using namespace ThreadNS;
static const int gnum = 5;

template<class T>
class ThreadPool;
template<class T>
class ThreadData    //大号结构体,方便我们传入线程的名字,后续再想传入一些内容可以在此添加
{
public:
    ThreadData(ThreadPool<T>* in,const string& name)
        :threadpool(in),_name(name)
        {}
public:
    ThreadPool<T>* threadpool;
    string _name;
};

template<class T>
class ThreadPool
{
public:
    ThreadPool(const int& num = gnum)
        :_num(num)
    {
        pthread_mutex_init(&_mutex,nullptr);    //初始化锁
        pthread_cond_init(&_cond,nullptr);      //初始化条件变量
        for(int i = 0;i<_num;++i)
        {
            _threads.push_back(new Thread());  //创建线程 
        }

    }
    void push(const T& in)
    {
        // pthread_mutex_lock(&_mutex); 
        lockGroud lockgroud(&_mutex);   //RAII的锁
        _Task_queue.push(in);           //给任务队列里插入任务
        pthread_cond_signal(&_cond);    //唤醒一个线程
        // pthread_mutex_unlock(&_mutex);
    }
    void run()
    {
        for(auto& t:_threads)
        {
            //将要传入执行方法的参数打包成结构体传入
            ThreadData<T>* td = new ThreadData<T>(this,t->threadname());   

            cout << t->threadname() << "  准备就绪" << endl;
            t->start(handlerTask,td);   //执行方法
        }
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);     //销毁锁
        pthread_cond_destroy(&_cond);       //销毁条件变量
        for(auto& t:_threads) delete t;     //销毁线程
    }
    T pop()
    {
        T t = _Task_queue.front();      //取任务
        _Task_queue.pop();              
        return t;
    }
    //一批接口
    pthread_mutex_t* Mutex(){ return &_mutex;}
    void threadlock() { pthread_mutex_lock(&_mutex); }
    void threadunlock() { pthread_mutex_unlock(&_mutex); }
    bool threadempty() { return _Task_queue.empty();}
    void threadwait() { pthread_cond_wait(&_cond,&_mutex);}
private:
    static void* handlerTask(void* args)
    {
        ThreadData<T>* td = static_cast<ThreadData<T>*>(args);
        // ThreadPool<T>* threadpool = static_cast<ThreadPool<T>*>(args);

        while(true)
        {
            T t;
            {
                // td->threadpool->threadlock();
                lockGroud lockgroud(td->threadpool->Mutex()); //RAII
                while(td->threadpool->threadempty())    //任务队列为空
                {
                    td->threadpool->threadwait();       //线程就挂起等待
                }
                t = td->threadpool->pop();              //获取任务
                // td->threadpool->threadunlock();
            }
            
            cout << td->_name << "获取任务,计算结果:"<< t() << endl;            //执行任务
            // sleep(1);
        }
        return nullptr;
    }
private:
    int _num;                   //线程数量
    vector<Thread*> _threads;   //线程池
    queue<T> _Task_queue;       //任务队列
    pthread_mutex_t _mutex;     //互斥锁
    pthread_cond_t _cond;       //条件变量
};
//main.cc
#include "ThreadPool.hpp"
#include <memory>
#include <unistd.h>


int main()
{
    unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
    tp->run();      //线程先跑起来,等待任务的到来

    int x,y;
    char op;
    while(true)
    {
        cout<< "请输入你要计算的内容"<< endl;
        cout<< "格式如下:x+y" << endl<<">:";
        cin >> x >> op >> y;
        Task t(x,y,op,mymath);  //  创建任务
        tp->push(t);            //  插入任务

        sleep(1);
        cout<< "---------------------------------------"<<endl;
    }
    return 0;
}

// 监控脚本指令->循环查看存在的线程
//while :; do ps -aL | grep Threadpool; sleep 1;echo "##################"; done

 上述代码只是一部分,线程封装的接口,任务的创建,以及锁的接口之前也都有写过,具体完整的代码自己查看吧:

lesson11/7_线程池 · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)

二、线程安全的单例模式

什么是单例模式

单例模式是一种 "经典的, 常用的, 常考的" 设计模式。

什么是设计模式

IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式。

单例模式的特点

某些类, 只应该具有一个对象(实例), 就称之为单例。
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中。此时往往要用一个单例的类来管理这些数据。
 

饿汉实现方式和懒汉实现方式

这个在之前的文章里有说过,感兴趣的可以去看一看:C++进阶 — 特殊类设计_晚风不及你的笑427的博客-CSDN博客

在这里我们对线程池的代码做一点小小的修改让其变成懒汉模式:构造私有,禁止拷贝构造和赋值重载实现,

//PthreadPool.hpp
#pragma once 

#include "thread.hpp"
#include "lockGroud.hpp"
#include "Task.hpp"

#include <vector>
#include <queue>
#include <mutex>
#include <unistd.h>

using namespace ThreadNS;
static const int gnum = 5;

template<class T>
class ThreadPool;
template<class T>
class ThreadData    //大号结构体,方便我们传入线程的名字,后续再想传入一些内容可以在此添加
{
public:
    ThreadData(ThreadPool<T>* in,const string& name)
        :threadpool(in),_name(name)
        {}
public:
    ThreadPool<T>* threadpool;
    string _name;
};

template<class T>
class ThreadPool
{
public:

    void push(const T& in)
    {
        // pthread_mutex_lock(&_mutex); 
        lockGroud lockgroud(&_mutex);   //RAII的锁
        _Task_queue.push(in);           //给任务队列里插入任务
        pthread_cond_signal(&_cond);    //唤醒一个线程
        // pthread_mutex_unlock(&_mutex);
    }
    void run()
    {
        for(auto& t:_threads)
        {
            //将要传入执行方法的参数打包成结构体传入
            ThreadData<T>* td = new ThreadData<T>(this,t->threadname());   

            cout << t->threadname() << "  准备就绪" << endl;
            t->start(handlerTask,td);   //执行方法
        }
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);     //销毁锁
        pthread_cond_destroy(&_cond);       //销毁条件变量
        for(auto& t:_threads) delete t;     //销毁线程
    }
    T pop()
    {
        T t = _Task_queue.front();      //取任务
        _Task_queue.pop();              
        return t;
    }
    static ThreadPool<T>* GetInstance() //返回对象
    {
        if(nullptr == _tp)//双重判断,降低锁冲突的概率, 提高性能
        {
            _mtx.lock();    //使用互斥锁, 保证多线程情况下也只调用一次 new.
            if(nullptr == _tp)
            {
                _tp = new ThreadPool<T>();
            }
            _mtx.unlock();
        }
        
        return _tp;
    }
    //一批接口
    pthread_mutex_t* Mutex(){ return &_mutex;}
    void threadlock() { pthread_mutex_lock(&_mutex); }
    void threadunlock() { pthread_mutex_unlock(&_mutex); }
    bool threadempty() { return _Task_queue.empty();}
    void threadwait() { pthread_cond_wait(&_cond,&_mutex);}
private:
    ThreadPool(const int& num = gnum)
        :_num(num)
    {
        pthread_mutex_init(&_mutex,nullptr);    //初始化锁
        pthread_cond_init(&_cond,nullptr);      //初始化条件变量
        for(int i = 0;i<_num;++i)
        {
            _threads.push_back(new Thread());  //创建线程 
        }

    }
    ThreadPool(const T&) = delete;
    void operator=(const T&) = delete;
    
    static void* handlerTask(void* args)
    {
        ThreadData<T>* td = static_cast<ThreadData<T>*>(args);
        // ThreadPool<T>* threadpool = static_cast<ThreadPool<T>*>(args);

        while(true)
        {
            T t;
            {
                // td->threadpool->threadlock();
                lockGroud lockgroud(td->threadpool->Mutex()); //RAII
                while(td->threadpool->threadempty())    //任务队列为空
                {
                    td->threadpool->threadwait();       //线程就挂起等待
                }
                t = td->threadpool->pop();              //获取任务
                // td->threadpool->threadunlock();
            }
            
            cout << td->_name << "获取任务,计算结果:"<< t() << endl;            //执行任务
            // sleep(1);
        }
        return nullptr;
    }
private:
    int _num;                   //线程数量
    vector<Thread*> _threads;   //线程池
    queue<T> _Task_queue;       //任务队列
    pthread_mutex_t _mutex;     //互斥锁
    pthread_cond_t _cond;       //条件变量
    volatile static ThreadPool<T>* _tp;  
    static mutex _mtx;
};
template<class T>
ThreadPool<T>* ThreadPool<T>::_tp = nullptr;
template<class T>
mutex ThreadPool<T>::_mtx;
//main.cc
#include "ThreadPool.hpp"
#include <memory>
#include <unistd.h>


int main()
{
    // unique_ptr<ThreadPool<Task>> tp(new ThreadPool<Task>());
    // tp->run();      //线程先跑起来,等待任务的到来

    ThreadPool<Task>::GetInstance()->run();

    int x,y;
    char op;
    while(true)
    {
        cout<< "请输入你要计算的内容"<< endl;
        cout<< "格式如下:x+y" << endl<<">:";
        cin >> x >> op >> y;
        Task t(x,y,op,mymath);  //  创建任务
        // tp->push(t);            //  插入任务
        ThreadPool<Task>::GetInstance()->push(t); 
        sleep(1);
        cout<< "---------------------------------------"<<endl;
    }
    return 0;
}

// 监控脚本指令->循环查看存在的线程
//while :; do ps -aL | grep Threadpool; sleep 1;echo "##################"; done

 完整代码:lesson11/8_单例线程池 · 晚风不及你的笑/MyCodeStorehouse - 码云 - 开源中国 (gitee.com)


三、STL,智能指针和线程安全

STL中的容器是否是线程安全的?
        不是。原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全。

智能指针是否是线程安全的?
        对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题。
        对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.


 四、其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。之前用的互斥锁就属于悲观锁
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁:轮询式的去申请锁,直至申请成功。

这些锁的等待方式是不同的,是什么决定了最终的等待方式呢?是线程要等待的时长决定的。比如说一个线程申请到锁,访问临界资源的时间很长,那么就需要让其他线程挂起等待,如果访问时间很短,其他线程就可以用自旋的方式去等。如果不知道时间长短怎么决定等待方式呢?可以两种方式都用,分别测试一下,看哪个效率更高用哪个。


五、读者写者问题(了解)

读写锁

在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。        ​​​​​​​注:写独占,读共享,读锁优先级高

“321原则”

3种关系:写者和写者 -> 互斥        写者和读者 -> 互斥与同步         读者和读者 -> 没有关系

2种角色:读者和写者

1个交易场所:一段缓冲区

读者写者模型和生产消费模型的本质区别就是 消费者会拿走数据,而读者不会。

使用场景:一次发布,很长时间不做修改,大部分时间都是被读取的,比如写作,或者写博客。

 读写锁接口

  • 初始化

int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t* restrict attr);

  • 销毁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

  • 加锁和解锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);        //读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);       //写者加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);       //不论是读者还是写者,直接解锁

  • 设置读写优先

int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
    PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
    PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
    PTHREAD_RWLOCK_PREFER_READER_NP 一致
    PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/


#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>

volatile int ticket = 1000;
pthread_rwlock_t rwlock;

void* reader(void* arg)
{
	char* id = (char*)arg;
	while (1) {
		pthread_rwlock_rdlock(&rwlock);
		if (ticket <= 0) {
			pthread_rwlock_unlock(&rwlock);
			break;
		} 
		printf("%s: %d\n", id, ticket);
		pthread_rwlock_unlock(&rwlock);
		usleep(1);
	} 
	return nullptr;
} 
void* writer(void* arg)
{
	char* id = (char*)arg;
	while (1) {
		pthread_rwlock_wrlock(&rwlock);
		if (ticket <= 0) {
			pthread_rwlock_unlock(&rwlock);
			break;
		} 
		printf("%s: %d\n", id, --ticket);
		pthread_rwlock_unlock(&rwlock);
		usleep(1);
	} 
	return nullptr;
} 
struct ThreadAttr
{
	pthread_t tid;
	std::string id;
};
std::string create_reader_id(std::size_t i)
{
	// 利用 ostringstream 进行 string 拼接
	std::ostringstream oss("thread reader ", std::ios_base::ate);
	oss << i;
	return oss.str();
} 
std::string create_writer_id(std::size_t i)
{
	// 利用 ostringstream 进行 string 拼接
	std::ostringstream oss("thread writer ", std::ios_base::ate);
	oss << i;
	return oss.str();
} 
void init_readers(std::vector<ThreadAttr>& vec)
{
	for (std::size_t i = 0; i < vec.size(); ++i) {
		vec[i].id = create_reader_id(i);
		pthread_create(&vec[i].tid, nullptr, reader, (void*)vec[i].id.c_str());
	}
} 
void init_writers(std::vector<ThreadAttr>& vec)
{
	for (std::size_t i = 0; i < vec.size(); ++i) {
		vec[i].id = create_writer_id(i);
		pthread_create(&vec[i].tid, nullptr, writer, (void*)vec[i].id.c_str());
	}
} 
void join_threads(std::vector<ThreadAttr> const& vec)
{
	// 我们按创建的 逆序 来进行线程的回收
	for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it !=
		vec.rend(); ++it) {
		pthread_t const& tid = it->tid;
		pthread_join(tid, nullptr);
	}
} 
void init_rwlock()
{
#if 0 // 写优先
	pthread_rwlockattr_t attr;
	pthread_rwlockattr_init(&attr);
	pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
	pthread_rwlock_init(&rwlock, &attr);
	pthread_rwlockattr_destroy(&attr);
#else // 读优先,会造成写饥饿
	pthread_rwlock_init(&rwlock, nullptr);
#endif
} 
int main()
{
	// 测试效果不明显的情况下,可以加大 reader_nr
	// 但也不能太大,超过一定阈值后系统就调度不了主线程了
	const std::size_t reader_nr = 1000;
	const std::size_t writer_nr = 2;
	std::vector<ThreadAttr> readers(reader_nr);
	std::vector<ThreadAttr> writers(writer_nr);
	init_rwlock();
	init_readers(readers);
	init_writers(writers);
	join_threads(writers);
	join_threads(readers);
	pthread_rwlock_destroy(&rwlock);
}
g++ -std=c++11 -Wall -Werror $^ -o $@ -lpthread

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

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

相关文章

搭建Stable Diffusion WebUI详细过程

文章目录 1、环境搭建1.1、GPU服务器选择1.2、配置服务器环境 2、源码和模型下载3、安装依赖库文件4、运行项目5、视频教程 1、环境搭建 为了方便&#xff0c;这里直接选择Vultr提供的已安装Anaconda的Ubuntu 22.04系统。 如果你自己电脑有足够的显存&#xff0c;你也可以在自…

Nacos Config 配置帐号密码加密后,注入过程中如何解密?

背景 线上项目规定不能在配置文件中出现帐号、密码的明文信息&#xff0c;所以必须加密。 引入 Nacos Config 配置后&#xff0c;Nacos Config 帐号密码、加密&#xff0c;服务注册发现也用相同的 nacos 帐号密码&#xff0c;那么如何解密才能保证 Nacos Config 服务能够正确…

【C语言】typedef关键字

文章目录 一. 使用介绍1. 对一般类型进行重命名2. 对结构体类型进行重命名3. 对指针进行重命名4. 对数组进行重命名 二. typedef 和 #define 的区别 一. 使用介绍 typedef 的作用就是对类型进行重命名&#xff0c;具体作用在以下几个方面&#xff1a; 对一般类型进行重命名对…

IP协议的相关特性

IP协议的相关特性 &#x1f50e;地址管理动态分配NATIPv6 &#x1f50e;IP 地址的组成网络主机号的划分IP 地址分类(A, B, C, D, E)子网掩码特殊的 IP 地址 &#x1f50e;路由选择 图片来自网络 &#x1f50e;地址管理 IP地址, 本质上是一个32位的整数 通常会把32位的整数, 转…

基于ChatGLM-Med与HuaTuo的微调部署

文章目录 ChatGLM-Med推理过程微调过程 HuaTuo配置环境模型下载推理过程微调过程 如何基于领域知识对类ChatGPT模型进行微调&#xff0c;以提升类ChatGPT模型在领域的问答效果&#xff1f; 有下面两个模型&#xff0c;一起来看看微调后的效果如何。 ChatGLM-Med: 基于中文医学知…

2023亚马逊云科技研究,数字化技能为中国企业和员工带来经济效益

在中国&#xff0c;信息技术在个人、企业和宏观经济层面都推动着重大变革。为了研究这些变化所带来的影响&#xff0c;盖洛普咨询公司(Gallup)和亚马逊云科技开展了关于数字化技能的调研。 研究表明&#xff0c;数字化技能正在为中国企业和在职人员带来巨大的经济价值&#x…

【Python】贪吃蛇 —— 无聊必备的小项目

作者主页&#xff1a;爱笑的男孩。的博客_CSDN博客-深度学习,活动,YOLO领域博主爱笑的男孩。擅长深度学习,活动,YOLO,等方面的知识,爱笑的男孩。关注算法,python,计算机视觉,图像处理,深度学习,pytorch,神经网络,opencv领域.https://blog.csdn.net/Code_and516?typecollect个人…

【python】keras包:深度学习( RNN循环神经网络 Recurrent Neural Networks)

RNN循环神经网络 应用&#xff1a; 物体移动位置预测、股价预测、序列文本生成、语言翻译、从语句中自动识别人名、 问题总结 这类问题&#xff0c;都需要通过历史数据&#xff0c;对未来数据进行预判 序列模型 两大特点 输入&#xff08;输出&#xff09;元素具有顺序关系…

透过金瑞基金一季度运营报告,看满帮创新故事背后的长期价值

投资中国市场该投哪些行业、哪些公司&#xff1f;在投资界&#xff0c;KraneShares金瑞基金长期致力于为这个问题提供答案。中概投资者都十分熟悉的KWEB——中概互联网指数ETF&#xff0c;就来自金瑞基金。 近日&#xff0c;金瑞基金发布了2023年第一季度运营报告&#xff0c;…

入门款但配置高 极米投影仪Z6X Pro轻松打造家庭影院

近年来&#xff0c;智能投影仪凭借大屏沉浸式体验以及使用场景灵活多变的便利性深受消费者欢迎。现如今&#xff0c;智能投影仪既能替代电视的职能&#xff0c;也能灵活融入小居室、出租屋等生活场景&#xff0c;顺理成章成为年轻人的“潮品”。京东电器2022年发布的《年轻人潮…

BetaFlight统一硬件配置文件研读之dma命令

BetaFlight统一硬件配置文件研读之dma命令 1. 源由2. 代码分析2.1 cliDma2.2 showDma2.3 cliDmaopt 3. 实例分析4. 配置情况4.1 dma4.2 dma show4.3 dma device list4.4 dma pin list4.5 dma device id4.5.1 dma adc id4.5.2 dma TIMUP id4.5.3 dma pin id 4.6 dma device id s…

BI技巧丨计算组单位切换

PowerBI自带的数据显示单位有千、百万、十亿等&#xff0c;很明显这些数据单位有些时候是不太符合国人的使用习惯的。 在计算组出来之前&#xff0c;我们习惯利用配置表的方式&#xff0c;将这种数据单位转换为符合我们习惯的方式&#xff1b;在计算组出来之后&#xff0c;我们…

石油化工企业防雷工程应用解决方案

随着现代石油化工行业的不断发展&#xff0c;防雷工程的重要性也越来越凸显。在石油化工行业中&#xff0c;防雷工程是一项至关重要的工作&#xff0c;因为石油化工行业常常面临雷电等自然灾害的威胁&#xff0c;这些灾害可能导致严重的安全事故和经济损失。石化企业其生产过程…

Word处理控件Aspose.Words功能演示:使用 C# 在 Word 文档中创建和修改 VBA 宏

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c; Aspose API支持流行文件格式处…

Vue版本2+模拟VueRouter的history模式

文章目录 分步骤实现创建 VueRouter 类创建静态方法&#xff0c;实现 install实现构造函数实现 createRouteMap实现 initComponents 方法 - router-link实现 initComponents 方法 - router-view实现 initEvents 完整代码 分步骤实现 创建 VueRouter 类 /*** VueRouter Class*…

RHEL软件包管理

3.1 RHEL软件包管理 完善的软件包管理机制对于操作系统来说是非常重要的&#xff0c;没有软件包管理器&#xff0c;用户使用操作系统将会变得非常困难&#xff0c;也不利于操作系统的推广。用户要使用Linux&#xff0c;需要了解Linux的包管理机制。随着Linux的发展&#xff0…

初识Vue-数据

目录 响应式 data prop 单向数据流 Prop属性校验 计算属性&#xff08;computed&#xff09; 侦听器&#xff08;watch&#xff09; 数组操作 数组操作-解决方案 响应式 data data为什么是函数&#xff1f; 因为只有返回一个生成data的函数&#xff0c;这个组件产生的…

精妙绝伦的算法之舞:解密力扣“删除有序数组中的重复项”

本篇博客会讲解力扣“26. 删除有序数组中的重复项”这道题&#xff0c;这是题目链接。 老规矩&#xff0c;先来审题&#xff1a; 题目有对判题标准的详细解释&#xff1a; 接下来是2个示例&#xff1a; 还有提示&#xff1a; 其实这道题考察的是“去重算法”&#xff0c;即…

【Linux】基础IO_文件描述符与重定向

环境&#xff1a;centos7.6&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅 相关文章推荐&#xff1a; 【Linux】冯.诺依曼体系结构与操作系统 【C/进阶】如何对文件进行读写&#xff08;含二进制&#xff09;操作&#xff1f; 【Linux】基础…

SuperMap GIS基础产品WebGIS FAQ集锦(2)

SuperMap GIS基础产品WebGIS FAQ集锦&#xff08;2&#xff09; 【iClient】Vue中该如何使用inject传递Map容器&#xff1f; 【解决方案】provide和inject绑定是不可响应的&#xff0c;所以传递时需要传递对象的property&#xff0c;使它变为可响应&#xff0c;示例如下&#…