线程池的实现

news2024/11/17 5:41:12

目录

一、线程池的实现

1.什么是线程池

2.设计线程类

3.设计线程池类

4.运行

5.RAII加锁改造

二、利用单例模式改造线程池

1.复习

2.饿汉模式

3.懒汉模式


关于系统编程的知识我们已经学完了,最后我们需要利用之前写过的代码实现一个线程池,彻底结束系统编程部分。

一、线程池的实现

1.什么是线程池

线程池是一种多线程处理形式,先将任务添加到队列,创建线程后线程池会自动启动线程处理这些任务。

线程池有以下特点:

  • 线程池是一种线程使用模式,如果线程过多会,就会带来调度开销,进而影响缓存局部性和整体性能。
  • 线程池维护着多个线程,可以处理监督管理者分配的可并发执行的任务。
  • 线程池维护的多个线程在无任务时会处于阻塞等待状态,当有任务需要处理时,线程就会被唤醒,处理完成后继续阻塞等待而不销毁。这样的设置避免了处理短时间任务时调用系统调用创建与销毁线程的开销。
  • 线程池不仅能够保证内核的充分利用,还能防止过分调度。
  • 可用线程数量取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

2.设计线程类

设计线程池就需要一个线程类管理线程,之前封装的classThread我们增加了一个Context类,这次我们想只使用一个类就完成线程的管理。

虽然需要重新设计,但是原来的函数有一部分也能使用。

线程类的设计思路:

  • 成员变量包括线程的名字、线程的tid、外部定义的线程执行函数对象、线程执行函数需要传递的参数还有一个静态的线程的编号,静态变量需要在类外定义。
  • 对函数对象的类型重命名也可以增加代码可读性。
  • 构造函数只需要初始化线程名即可,线程的创建会在其他地方实现。
  • 我们创建一个start函数负责线程的创建,它有两个参数func和args,func是我们需要传入的线程处理函数,args是处理函数的参数。
  • 在线程创建的时候,具体的执行函数不要设为_func,这样会增加程序的耦合度。
  • 我们再创建一个start_routine函数负责线程的处理,由于它的返回值应该为void*,只有一个参数void* args,该函数必须设置为静态才能去掉默认的this指针参数。
  • start_routine的args还是要用于传递this指针,只是因为线程处理函数的特殊要求,我们才没有直接将其定义为成员函数。而且这个函数也应该定义为私有函数,防止使用者在类外调用该函数。
  • start_routine使用回调函数callback调用_func并传回结果。
  • 注意只有在回调函数中传递的是_args,另一个args是this指针。
  • 最后加上一个获取线程名的函数。

最终实现的Thread.hpp

#include<pthread.h>
#include<assert.h>
#include<functional>
#define NUM 64

class Thread
{
    typedef std::function<void*(void*)> func_t;
public:
    //构造函数创建线程
    Thread()
    {
        //对线程进行规范化命名
        char buffer[NUM];
        snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
        _name = buffer;
    }

    //启动线程
    void start(func_t func, void* args = nullptr)
    {
        //初始化这两个变量
        _args = args;
        _func = func;
        //创建线程,线程的处理函数为start_routine
        int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
        //断言创建成功,这里也可以换成打印错误码的代码
        assert(n == 0);
    }

    //利用回调函数
    void* callback()
    {
        return _func(_args);
    }

    //获取线程的名字
    std::string threadname()
    {
        return std::string(_name);
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
    }
private:
    //处理函数的函数能只有一个确定的参数,所以只能定义为静态。
    static void* start_routine(void* args)//用args传递this指针
    {
        Thread* pt = static_cast<Thread*>(args);
        return pt->callback();
    }

    std::string _name;
    pthread_t _tid;
    func_t _func; 
    void* _args;
    static int _threadnum;
};

int Thread::_threadnum = 1;

我们测试该类

test.cc

#include<iostream>
#include<unistd.h>
#include"Thread.hpp"

using namespace std;

void* test(void* args)
{
    char* p = (char*)args;
    while(1)
    {
        printf("%s\n", p);
        sleep(1);
    }
}

int main()
{
    Thread t;
    t.start(test, (void*)"thread running");
    t.join();
    return 0;
}

线程可以正常运行:

3.设计线程池类

线程池类负责多线程的创建和维护,并且在基于阻塞队列的生产者消费者模型下运行。其中生产者是生成任务的线程,消费者是线程池维护的多个线程,数据结构为阻塞队列。线程池包括阻塞队列和所有消费者。

线程池类的设计思路:

  • 成员变量包含线程池维护的线程数量_num、存放线程类Threads的vector容器_threads,存放任务的阻塞队列_queue、保证消费者线程互斥访问任务队列的锁_cmutex、让多线程同步的条件变量_cond、保证生产者互斥生产数据的锁_pmutex。
  • 首先构造中设置线程数量_num为缺省值,这样可以让用户决定线程的数量。然后将互斥锁和条件变量初始化,最后后创建_num个Thread对象并将指针尾插到_threads容器中。此时线程只有名字,还未被创建。
  • 然后实现析构函数,在其中将互斥锁和条件变量销毁,循环释放_threads容器中的所有线程回收并将Thread对象也全部delete掉。
  • 实现一个接口run(),通过该接口真正创建对应数量的新线程,并且开始执行。每成功创建一个线程且开始执行后,都要打印该线程开始运行的信息,比如thread1 start...。
  • run()创建线程需要循环_threads容器内Thread变量的start()函数,所有的线程就会启动。
  • 所有线程都执行静态handerTask函数,与以前一样,还是需要去掉this指针。
  • handler_task函数需要使用线程名和this指针,所以我们在前面创建一个ThreadData结构体存放它们,this指针放在ThreadPool* threadpool中,线程的名字放在_threadname中。创建线程就直接把这个结构体指针传过去就可以了。

  • 消费线程执行handler_task,该函数会从任务队列_task_queue中取任务执行,并且它们要按照一定顺序去访问,所以线程是同步和互斥的关系。
  • 因为handler_task是Thread类对象在执行,所以它中不能直接访问ThreadPool中的条件变量和锁。所以在类内需要提供公有的接口允许线程访问私有成员,允许其进行加锁,解锁,条件判断,以及取任务等操作。

  • 在访问任务队列的时候先加锁,如果任务队列为空则挂起等待,如果不为空则取走任务并处理任务。
  • 从任务队列中获取任务后,不再操作临界资源,所以解锁在前,任务处理在后。线程获取到任务时,会将任务放到自己的独立栈结构中。所以线程处理任务是相互独立的。
  • 线程处理完任务后,将在堆区存放当前线程属性的ThreadData对象释放掉。
  • 倒数第二步定义push函数用于推送任务,由于推送任务是操作临界资源,在推送任务到任务队列前加锁,推送完成后唤醒在条件变量_cond下等待的一个消费线程,再进行解锁。
  • 最后将从队列中取数据的pop函数定义,由于其在加锁代码里,素以不用考虑线程安全问题。

4.运行

此时我们再根据任务类型在main函数和handler_task函数中设置打印函数显示线程调度就可以了。我们设置主线程每一秒推送一个计算任务,计算任务直接使用之前的类。

CalTask.hpp

#include<unistd.h>
#include<functional>
#include<stdio.h>
#define MAX_NUM 10
//计算任务类
class CalTask
{
    typedef std::function<int(int,int,char)> func_t;
public:
    //默认构造
    CalTask()
    {}
    //构造函数
    CalTask(int a, int b, char op, func_t func)
        :_a(a)
        ,_b(b)
        ,_op(op)
        ,_func(func)
    {}

    //仿函数
    std::string operator()()
    {
        int result = _func(_a, _b, _op);
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _a, _op, _b, result);
        std::string s(buffer);
        return s;
    }

    //显示任务
    std::string show_task()
    {
        char buffer[64];
        snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _a, _op, _b);
        std::string s(buffer);
        return s;
    }
private:
    func_t _func;
    int _a;
    int _b;
    char _op;
};

Thread.hpp

#include<pthread.h>
#include<assert.h>
#include<functional>
#define NUM 64

class Thread
{
    typedef std::function<void*(void*)> func_t;
public:
    //构造函数创建线程
    Thread()
    {
        //对线程进行规范化命名
        char buffer[NUM];
        snprintf(buffer, sizeof(buffer), "thread%d", _threadnum++);
        _name = buffer;
    }

    //启动线程
    void start(func_t func, void* args = nullptr)
    {
        //初始化这两个变量
        _args = args;
        _func = func;
        //创建线程,线程的处理函数为start_routine
        int n = pthread_create(&_tid, nullptr, start_routine, (void*)this);//将this指针传递到执行代码中
        //断言创建成功,这里也可以换成打印错误码的代码
        assert(n == 0);
    }

    //利用回调函数
    void* callback()
    {
        return _func(_args);
    }

    //获取线程的名字
    std::string threadname()
    {
        return std::string(_name);
    }

    void join()
    {
        int n = pthread_join(_tid, nullptr);
        assert(n == 0);
    }
private:
    //处理函数的函数能只有一个确定的参数,所以只能定义为静态。
    static void* start_routine(void* args)//用args传递this指针
    {
        Thread* pt = static_cast<Thread*>(args);
        return pt->callback();
    }

    std::string _name;
    pthread_t _tid;
    func_t _func; 
    void* _args;
    static int _threadnum;
};

int Thread::_threadnum = 1;

Threadpool.hpp

#include <vector>
#include <queue>
#include <string>

#define THREAD_NUM 10

//前面加上声明
template <class T>
class ThreadPool;

//线程数据类
template <class T>
class ThreadData
{
public:
    ThreadPool<T>* _pthreadpool;//线程池的this指针
    std::string _threadname;//线程的名字
    //构造函数
    ThreadData(ThreadPool<T>* tp, std::string name)
        :_pthreadpool(tp)
        ,_threadname(name)
    {}
};

//线程池
template <class T>
class ThreadPool
{
public:
    //构造函数
    ThreadPool(int num = THREAD_NUM)
    :_num(num)
    {
        pthread_mutex_init(&_cmutex, nullptr);//初始化消费互斥锁
        pthread_mutex_init(&_pmutex, nullptr);//初始化生产互斥锁
        pthread_cond_init(&_cond, nullptr);//初始化条件变量
        //创建多个线程
        for(size_t i = 0; i < _num; ++i)
        {
            _threads.push_back(new Thread());
        }
    }

    //析构函数
    ~ThreadPool()
    {
        pthread_mutex_destroy(&_cmutex);//销毁消费互斥锁
        pthread_mutex_destroy(&_pmutex);//销毁生产互斥锁
        pthread_cond_destroy(&_cond);//销毁条件变量
        //销毁多个线程
        for(size_t i = 0; i < _num; ++i)
        {
            _threads[i]->join();
            delete _threads[i];
        }
    }

    //将所有线程启动
    void run()
    {
        for(size_t i = 0; i < _num; ++i)
        {
            //由于线程函数需要使用线程池类内的函数和每一个线程的名字,所以将它们合起来构造一个线程数据类传递给线程操作函数
            ThreadData<T>* p = new ThreadData<T>(this, _threads[i]->threadname());
            _threads[i]->start(handler_task, (void*)p);//这里也可以设计一个类
            std::string s(p->_threadname);
            s += " start...\n";
            std::cout << s;
        }
    }

    //向线程池推送任务
    void push(const T& data)
    {
        pthread_mutex_lock(&_pmutex);
        _task_queue.push(data);
        pthread_cond_signal(&_cond);
        pthread_mutex_unlock(&_pmutex);
    }

    //消费线程取任务,加锁解锁已经在消费线程处理函数里进行了,不需要注意线程安全
    T pop()
    {
        T data = _task_queue.front();
        _task_queue.pop();
        return data;
    }

    //静态成员函数需要访问的非静态成员接口
    bool isQueueEmpty() {return _task_queue.empty();}//判断任务队列是否为空
    void lockQueue() {pthread_mutex_lock(&_cmutex);}//给任务队列加锁
    void unlockQueue() {pthread_mutex_unlock(&_cmutex);}//给任务队列解锁
    void threadWait() {pthread_cond_wait(&_cond,&_cmutex);}//将线程放入条件变量的等待队列中

private:
    //消费线程的处理函数
    static void* handler_task(void* args)
    {
        ThreadData<T>* p = (ThreadData<T>*)args;
        while(1)
        {
            p->_pthreadpool->lockQueue();
            //如果任务队列为空,消费者进程会被加入到条件变量的阻塞队列中
            while(p->_pthreadpool->isQueueEmpty())
            {
                p->_pthreadpool->threadWait();
            }
            T data = p->_pthreadpool->pop();
            p->_pthreadpool->unlockQueue();
            printf("%s接受了任务%s并处理完成,结果为:%s\n", p->_threadname.c_str(), 
            data.show_task().c_str(), data().c_str());
        }
        delete p;
        return nullptr;
    }

    int _num;//维护的线程数量
    std::vector<Thread*> _threads;//管理多个线程对象的容器
    std::queue<T> _task_queue;//任务队列
    pthread_mutex_t _cmutex;//消费者互斥锁
    pthread_cond_t _cond;//条件变量
    pthread_mutex_t _pmutex;//生成任务时的互斥锁
};

test.cc

#include<iostream>
#include<unistd.h>
#include"CalTask.hpp"
#include"Thread.hpp"
#include"Threadpool.hpp"

using namespace std;

//计算器函数
const string ops = "+-*/%";
int calculate(int a, int b, char op)
{
    int result = 0;
    switch(op)
    {
        case '+':
            result = a + b;
            break;
        case '-':
            result = a - b;
            break;
        case '*':
            result = a * b;
            break;
        case '/':
        {
            if(b == 0)
                cerr << "除数不能为0\n";
            else
                result = a / b;
        }
            break;
        case '%':
        {
            if(b == 0)
                cerr << "取模的数字不能为0\n";
            else
                result = a % b;
        }
            break;
        default:
            break;
    }
    return result;
}

int main()
{
    srand((unsigned int)time(nullptr));
    ThreadPool<CalTask>* tp = new ThreadPool<CalTask>();
    tp->run();
    for(;;)
    {
        //
        int a = rand()%10;
        int b = rand()%10;
        char op = ops[rand()%ops.size()];
        CalTask task(a, b, op, calculate);
        tp->push(task);
        printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
        sleep(1);
    }
    return 0;
}

运行结果:

5.RAII加锁改造

我们之前写了一个LockGuard类,我们可以将这个类在task_handler的加锁代码中就可以用起来。

#include<pthread.h>
class mutex
{
public:
    //构造函数
    mutex(pthread_mutex_t* p = nullptr)
        :_pmutx(p)
    {}
    //加锁
    void lock()
    {
        pthread_mutex_lock(_pmutx);
    }
    //解锁
    void unlock()
    {
        pthread_mutex_unlock(_pmutx);
    }    
private:
    pthread_mutex_t* _pmutx;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t* lock_p):_mutex(lock_p)
    {
        _mutex.lock();//构造函数内加锁
    }
    ~LockGuard()
    {
        _mutex.unlock();//析构函数内解锁
    }
private:
    mutex _mutex;
};

LockGuard构造函数加锁,析构函数解锁。通过{}控制该变量的生命周控制加锁。

由于该函数是由线程执行的,锁是线程池的私有变量,所以也需要增加一个函数获取锁。

最后改造的handler_task代码如下:

代码正常运行。

二、利用单例模式改造线程池

1.复习

饿汉模式和懒汉模式都是单例模式的实现方式,具体的介绍可以看这篇博客的第二章:C++特殊类设计及类型转换_聪明的骑士的博客-CSDN博客

现在我们要将线程池改为这两种单例模式。

2.饿汉模式

饿汉模式:不管以后会不会使用单例对象,只要程序一启动,程序就会先创建一个唯一的实例对象然后再执行其他代码。

首先,增加一个静态对象保证单例的唯一性,静态变量需要在根据自己的类型在类外初始化。

然后需要将构造函数设为私有,并且不允许生成拷贝构造和赋值运算符重载。

最后,添加一个静态函数GetInstance()获取单例的地址。

此时,更改一下主线程的代码:

int main()
{
    srand((unsigned int)time(nullptr));
    ThreadPool<CalTask>::GetInstance()->run();
    for(;;)
    {
        int a = rand()%10;
        int b = rand()%10;
        char op = ops[rand()%ops.size()];
        CalTask task(a, b, op, calculate);
        ThreadPool<CalTask>::GetInstance()->push(task);
        printf("主线程推送任务:%d %c %d = ?\n", a, op, b);
        sleep(1);
    }
    return 0;
}

程序正常运行。

3.懒汉模式

懒汉模式:单例只有在第一次被使用到时才被建立,就像一个懒汉一样,什么事都拖到截止日才干。

首先,在原来饿汉模式的基础上将单例对象改为单例对象指针,并且设置为空。

然后,为了保证单例的建立是线程安全的,还要增加一个C++11提供的锁并在类外初始化(需要包含mutex.h头文件,这里用C++标准提供的锁是因为它操作更方便)。

最后,使用之前的双检查加锁方式重新设计一下获取单例指针的接口。

主线程代码不需要改,直接运行就可以跑起来了。

最后,所有Linux系统编程的知识就讲解完毕了,接下来我们要进行网络编程的学习。

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

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

相关文章

如何理解张量、张量索引、切片、张量维度变换

Tensor 张量 Tensor&#xff0c;中文翻译“张量”&#xff0c;是一种特殊的数据结构&#xff0c;与数组和矩阵非常相似。在 PyTorch 中&#xff0c;使用张量对模型的输入和输出以及模型的参数进行编码。 Tensor 是一个 Python Class。PyTorch 官方文档中定义“Tensor&#xff0…

Datawhale × 和鲸科技丨《2023 中国人工智能人才学习白皮书》发布!

2023 是一个历史性的年份&#xff0c;它标志着人工智能技术的崛起与普及&#xff0c;这一年里&#xff0c;AI 不仅在科技、经济、社会、文化等各个领域取得突破性的进展&#xff0c;也在人类日常生活中扮演愈加重要的角色。随着人工智能时代的加速到来&#xff0c;我国 AI 人才…

msvcp140.dll丢失的有哪些解决方法,丢失msvcp140.dll是什么意思

在我们的日常生活中&#xff0c;电脑问题是无处不在的&#xff0c;而msvcp140.dll丢失又是其中比较常见的一种。msvcp140.dll是Microsoft Visual C运行时库的一部分&#xff0c;它包含了一些重要的函数和资源。当这个文件丢失时&#xff0c;可能会导致电脑出现各种问题&#xf…

链路追踪Skywalking快速入门

目录 1 Skywalking概述1.1 微服务系统监控三要素1.2 什么是链路追踪1.2.1 链路追踪1.2.2 OpenTracing1、数据模型&#xff1a;2、核心接口语义 1.3 常见APM系统1.4 Skywalking介绍1、SkyWalking 核心功能&#xff1a;2、SkyWalking 特点&#xff1a;3、Skywalking架构图&#x…

mysql之DML的select分组排序

目录 一、创建表employee和department表 1.创建department表 2.创建employee表 3.给employee表格和department表格建立外键 4.给department插入数据 5.给employee表插入数据 6.删除名字为那个的数据 二、分组查询和排序查询&#xff0c;以及对数据的处理&#xff08;av…

ARM/X86工业级数据采集 (DAQ) 与控制产品解决方案

I/O设备&#xff0c;包括信号调理模块、嵌入式PCI/PCIE卡、便携式USB模块、DAQ嵌入式计算机、模块化DAQ系统&#xff0c;以及DAQNavi/SDK软件开发包和DAQNavi/MCM设备状态监测软件。 工业I/O产品适用于各种工业自动化应用&#xff0c;从机器自动化控制、测试测量到设备状态监测…

Java“牵手”京东商品详情数据,京东商品详情API接口,京东API接口申请指南

京东平台商品详情接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取京东商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片等详细信息 。 获取商品详情接口API是一种用于获取电商平台上商品详情数据的接口&#xff0c;通过…

kuiper安装

1:使用docker方式安装 docker pull lfedge/ekuiper:latest docker run -p 9081:9081 -d --name kuiper -e MQTT_SOURCE__DEFAULT__SERVERtcp://127.0.0.1:1883 lfedge/ekuiper:latest这样就安装好了&#xff0c;但是操作只能通过命令完成&#xff0c;如果想要通过页面来操作&…

@DS注解方式springboot多数据源配置及失效场景解决

1.使用教程 导入依赖 <!--多数据源--><dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.5.0</version></dependency>配置数据源 datasource:…

MT36291 2.5A,高效型1.2MHz电流模式升压转换器芯片

MT36291 2.5A&#xff0c;高效型1.2MHz电流模式升压转换器芯片 特征 ●集成了80ms功率的MOSFET ●2.2V到16V的输入电压 ●1.2MHz固定开关频率 ●可调过电流保护&#xff1a; 0.5A ~2.5A ●内部2.5开关限流&#xff08;OC引脚浮动&#xff09; ●可调输出电压 ●内部补偿 ●过电…

PCL入门(三):矩阵变换实现平移和旋转

目录 1. pcl中的矩阵变换是什么2. 举例&#xff1a;如何做矩阵变换 1. pcl中的矩阵变换是什么 通过激光传感器等设备获得的3d点云在拼接成更大场景时&#xff0c;需要对点云数据进行旋转和平移操作。而旋转和平移操作&#xff0c;就可以通过矩阵变换来实现。 点的表示 对于点…

【每日一题】1523. 在区间范围内统计奇数数目,860. 柠檬水找零

1523. 在区间范围内统计奇数数目 - 力扣&#xff08;LeetCode&#xff09; 给你两个非负整数 low 和 high 。请你返回 low 和 high 之间&#xff08;包括二者&#xff09;奇数的数目。 示例 1&#xff1a; 输入&#xff1a;low 3, high 7 输出&#xff1a;3 解释&#xff1a;…

Excel文件损坏打不开怎么办?可用这三招解决!

当你的excel文件不可读&#xff0c;或者出现提示“文件已经被损坏&#xff0c;无法打开”&#xff0c;这种情况让人措手不及。而且还会给我们正常的工作带来很多麻烦&#xff0c;文件损坏打不开怎么办&#xff1f;来看看这3招&#xff0c;详细的图文教程&#xff0c;小白也能轻…

Notepad++ 的安装及配置

由于电脑重装了Win11系统&#xff0c;干脆重头开始&#xff0c;重新安装每一个软件~~~ 很多博客或者博主都会推荐notepad的官网&#xff1a;https://notepad-plus-plus.org/ 但大家亲自点开就会发现是无响应&#xff0c;如下图 同时&#xff0c;也会有很多博主直接给网盘地址…

【Flutter】Flutter 使用 table_calendar 实现自定义日历

【Flutter】Flutter 使用 table_calendar 实现自定义日历 文章目录 一、前言二、安装和基本使用三、日历的交互性四、日历事件五、自定义 UI 和 CalendarBuilders六、本地化和语言设置七、完整实际业务代码示例 一、前言 你好&#xff01;今天我要为你介绍一个非常实用的 Flut…

2023高教社杯数学建模国赛的工具箱准备

2023高教社杯数学建模国赛的工具箱准备 数学建模国赛工具箱&#xff08;私信领取&#xff09;&#xff01;&#xff01;&#xff01;小编仔细阅读了比赛官方网站上的规则和要求&#xff0c;以及比赛的题型和时间安排&#xff0c;现总结分享给大家。欢迎私信和评论&#xff0c;…

运维监控系统PIGOSS BSM 业务监控 大屏展现解析

“业务大屏”是 PIGOSS BSM&#xff08;IT运维监控工具&#xff09;的特色功能之一&#xff0c;旨在提供综合而直观的业务监控视图。该功能主要由三个组成部分构成&#xff1a;业务健康度雷达图、业务状态矩阵和多趋势对比图。 下面将对每个部分进行详细介绍&#xff1a; 业务健…

IDEA Java1.8通过sqljdbc4连接sqlserver插入语句

1. 下载sqljdbc4:https://mvnrepository.com/artifact/com.microsoft.sqlserver.jdbc/sqljdbc4/4.0 下载后在IDEA放入仓库内&#xff0c;可以放在resources下&#xff0c;右键“add as library”。 2. 在控制面板中开启Telnet客户端&#xff0c;默认是不开启的。 若报错“ ja…

VMware虚拟机安装运行MacOS系统

VMware虚拟机安装运行MacOS系统 1. VMware虚拟机安装运行MacOS系统1.1. 前期准备 2. 解锁虚拟机MacOS2.1. 解锁后效果 3. 开始安装MacOS系统3.1. 选择系统3.2. 虚拟机磁盘3.3. 镜像 4. 开机配置4.1. MacOS图标4.2. 磁盘4.2.1. 磁盘配置4.2.2. 抹掉数据 4.3. 安装系统4.3.1. 安装…