Linux知识点 -- Linux多线程(四)

news2024/11/23 22:48:34

Linux知识点 – Linux多线程(四)

文章目录

  • Linux知识点 -- Linux多线程(四)
  • 一、线程池
    • 1.概念
    • 2.实现
    • 3.单例模式的线程池
  • 二、STL、智能指针和线程安全
    • 1.STL的容器是否是线程安全的
    • 2.智能指针是否是线程安全的
  • 三、其他常见的各种锁
  • 四、读者写者问题
    • 1.读写锁
    • 2.读写锁接口


一、线程池

1.概念

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

  • 预先申请资源,用空间换时间;
  • 预先申请一批线程,任务到来就处理;
  • 线程池就是一个生产消费模型;

2.实现

thread.hpp
线程封装:

#pragma once

#include<iostream>
#include<string>
#include<functional>
#include<cstdio>

typedef void* (*fun_t)(void*); // 定义函数指针类型,后面回调

class ThreadData  // 线程信息结构体
{
public:
    void* _args;
    std::string _name;
};

class Thread
{
public:
    Thread(int num, fun_t callback, void* args)
        : _func(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof(nameBuffer), "Thread-%d", num);
        _name = nameBuffer;

        _tdata._args = args;
        _tdata._name = _name;
    }

    void start() // 创建线程
    {
        pthread_create(&_tid, nullptr, _func, (void*)&_tdata); // 直接将_tdata作为参数传给回调函数
    }

    void join() // 线程等待
    {
        pthread_join(_tid, nullptr);
    }

    std::string name()
    {
        return _name;
    }

    ~Thread()
    {}

private:
    std::string _name;
    fun_t _func;
    ThreadData _tdata;
    pthread_t _tid;
};

lockGuard.hpp
锁的封装,构建对象时直接加锁,对象析构时自动解锁;

#pragma once

#include <iostream>
#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx)
        : _pmtx(mtx)
    {
    }

    void lock()
    {
        pthread_mutex_lock(_pmtx);
    }

    void unlock()
    {
        pthread_mutex_unlock(_pmtx);
    }

    ~Mutex()
    {}

private:
    pthread_mutex_t *_pmtx;
};


class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx)
        : _mtx(mtx)
    {
        _mtx.lock();
    }

    ~lockGuard()
    {
        _mtx.unlock();
    }
private:
    Mutex _mtx;
};

log.hpp

#pragma once

#include<iostream>
#include<cstdio>
#include<cstdarg>
#include<ctime>
#include<string>

//日志级别
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4

const char* gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log"

//完整的日志功能,至少需要:日志等级 时间 支持用户自定义(日志内容,文件行,文件名)

void logMessage(int level, const char* format, ...)
{
#ifndef DEBUG_SHOW
    if(level == DEBUG) return;
#endif

    char stdBuffer[1024];//标准部分
    time_t timestamp = time(nullptr);
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);

    char logBuffer[1024];//自定义部分
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    FILE* fp = fopen(LOGFILE, "a");
    fprintf(fp, "%s %s\n", stdBuffer, logBuffer);
    fclose(fp);
}
  • 注:
    (1)提取可变参数
    在这里插入图片描述
    使用宏来提取可变参数:
    在这里插入图片描述
    将可变参数格式化打印到对应地点:
    在这里插入图片描述
    format是打印的格式;
    在这里插入图片描述
    (2)条件编译:
    在这里插入图片描述
    条件编译,不想调试的时候,就不加DEBUG宏,不打印日志信息;
    在这里插入图片描述
    -D:在命令行定义宏 ;

threadPool.hpp

线程池封装:

#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"

const int g_thread_num = 3;

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &_lock;
    }

    bool isEmpty()
    {
        return _task_queue.empty();
    }

    void waitCond()
    {
        pthread_cond_wait(&_cond, &_lock);
    }

    T getTask()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

    ThreadPool(int thread_num = g_thread_num)
        : _num(thread_num)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 1; i <= _num; i++)
        {
            _threads.push_back(new Thread(i, routine, this));
            // 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数
        }
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    // 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁
    static void *routine(void *args)
    {
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                {
                    tp->waitCond();
                }
                // 读取任务
                task = tp->getTask();
                // 任务队列是共享的,将任务从共享空间,拿到私有空间
            }
            task(td->_name); // 处理任务
        }
    }

    void pushTask(const T &task)
    {
        lockGuard lockguard(&_lock); // 访问临界资源,需要加锁
        _task_queue.push(task);
        pthread_cond_signal(&_cond); // 推送任务后,发送信号,让进程处理
    }

    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<Thread *> _threads; // 线程池
    int _num;
    std::queue<T> _task_queue; // 任务队列
    pthread_mutex_t _lock;     // 锁
    pthread_cond_t _cond;      // 条件变量
};
  • 注:
    (1)如果回调函数routine放在thread类里面,由于成员函数会默认传this指针,因此参数识别的时候可能会出错,所以需要设置成静态成员;在这里插入图片描述
    在这里插入图片描述
    (2)如果设置成静态类内方法,这个函数只能使用静态成员,而不能使用其他类内成员;
    可以让routine函数拿到整体对象,在构造线程的时候,routine的参数传入this指针;

    在这里插入图片描述
    在构造函数的初始化列表中是参数的初始化,在下面的函数体中是赋值的过程,因此在函数体中对象已经存在了,就可以使用this指针了;
    (3)类内公有接口让静态成员函数routine通过this指针能够访问类内成员;
    在这里插入图片描述
    testMain.cc
#include"threadPool.hpp"
#include"Task.hpp"
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<unistd.h>

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->run();

    while(true)
    {
        //生产的时候,只做任务要花时间
        int x = rand()%100 + 1;
        usleep(7756);
        int y = rand()%30 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);

        //推送任务到线程池中
        tp->pushTask(t);

        sleep(1);
    }

    return 0;
}

运行结果:
在这里插入图片描述

3.单例模式的线程池

threadPool.hpp

#include "thread.hpp"
#include <vector>
#include <queue>
#include <unistd.h>
#include "log.hpp"
#include "Task.hpp"
#include "lockGuard.hpp"

const int g_thread_num = 3;

template <class T>
class ThreadPool
{
public:
    pthread_mutex_t *getMutex()
    {
        return &_lock;
    }

    bool isEmpty()
    {
        return _task_queue.empty();
    }

    void waitCond()
    {
        pthread_cond_wait(&_cond, &_lock);
    }

    T getTask()
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

//单例模式线程池:懒汉模式
private:
    //构造函数设为私有
    ThreadPool(int thread_num = g_thread_num)
        : _num(thread_num)
    {
        pthread_mutex_init(&_lock, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 1; i <= _num; i++)
        {
            _threads.push_back(new Thread(i, routine, this));
            // 线程构造传入的this指针,是作为ThreadData结构体的参数的,ThreadData结构体才是routine回调函数的参数
        }
    }

    ThreadPool(const ThreadPool<T> &other) = delete;
    const ThreadPool<T>& operator=(const ThreadPool<T> &other) = delete;

public:
    //创建单例对象的类内静态成员函数
    static ThreadPool<T>* getThreadPool(int num = g_thread_num)
    {
        //在这里再加上一个条件判断,可以有效减少未来必定要进行的加锁检测的问题
        //拦截大量的在已经创建好单例的时候,剩余线程请求单例而直接申请锁的行为
        if(nullptr == _thread_ptr)
        {
            //加锁
            lockGuard lockguard(&_mutex);
            //未来任何一个线程想要获取单例,都必须调用getThreadPool接口
            //一定会存在大量的申请锁和释放锁的行为,无用且浪费资源
            if(nullptr == _thread_ptr)
            {
                _thread_ptr = new ThreadPool<T>(num);
            }
        }
        return _thread_ptr;
    }

    void run()
    {
        for (auto &iter : _threads)
        {
            iter->start();
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    // 消费过程:线程调用回调函数取任务就是所谓的消费过程,访问了临界资源,需要加锁
    static void *routine(void *args)
    {
        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->_args; // 拿到this指针
        while (true)
        {
            T task;
            {
                lockGuard lockguard(tp->getMutex());
                while (tp->isEmpty())
                {
                    tp->waitCond();
                }
                // 读取任务
                task = tp->getTask();
                // 任务队列是共享的,将任务从共享空间,拿到私有空间
            }
            task(td->_name); // 处理任务
        }
    }

    void pushTask(const T &task)
    {
        lockGuard lockguard(&_lock); // 访问临界资源,需要加锁
        _task_queue.push(task);
        pthread_cond_signal(&_cond); // 推送任务后,发送信号,让进程处理
    }

    ~ThreadPool()
    {
        for (auto &iter : _threads)
        {
            iter->join();
            delete iter;
        }
        pthread_mutex_destroy(&_lock);
        pthread_cond_destroy(&_cond);
    }

private:
    std::vector<Thread *> _threads; // 线程池
    int _num;
    std::queue<T> _task_queue; // 任务队列

    static ThreadPool<T>* _thread_ptr;
    static pthread_mutex_t _mutex;

    pthread_mutex_t _lock;     // 锁
    pthread_cond_t _cond;      // 条件变量
};

//静态成员在类外初始化
template<class T>
ThreadPool<T>* ThreadPool<T>::_thread_ptr = nullptr;

template<class T>
pthread_mutex_t ThreadPool<T>::_mutex = PTHREAD_MUTEX_INITIALIZER;

在这里插入图片描述
在这里插入图片描述
多线程同时调用单例过程,由于创建过程是非原子的,有可能被创建多个对象,是非线程安全的;
需要对创建对象的过程加锁,就可以保证在多线程场景当中获取单例对象;
但是未来任何一个线程想调用单例对象,都必须调用这个成员函数,就会存在大量申请和释放锁的行为;
可以在之间加一个对单例对象指针的判断,若不为空,就不进行对象创建;

在这里插入图片描述
testMain.cc

#include"threadPool.hpp"
#include"Task.hpp"
#include<ctime>
#include<cstdlib>
#include<iostream>
#include<unistd.h>

int main()
{
    srand((unsigned long)time(nullptr) ^ getpid());

    //ThreadPool<Task>* tp = new ThreadPool<Task>();
    //tp->run();    
    ThreadPool<Task>::getThreadPool()->run();//创建单例对象

    

    while(true)
    {
        //生产的时候,只做任务要花时间
        int x = rand()%100 + 1;
        usleep(7756);
        int y = rand()%30 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

        logMessage(DEBUG, "制作任务完成:%d+%d=?", x, y);

        //推送任务到线程池中
        ThreadPool<Task>::getThreadPool()->pushTask(t);

        sleep(1);
    }

    return 0;
}

运行结果:
在这里插入图片描述

二、STL、智能指针和线程安全

1.STL的容器是否是线程安全的

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

2.智能指针是否是线程安全的

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

三、其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等) ,当其他线程想要访问数据时,被阻塞挂起;
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作;
    CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试;
  • 自旋锁
    临界资源就绪的时间决定了线程等待的策略;
    不断检测资源是否就绪就是自旋(轮询检测);
    自旋锁本质就是通过不断检测锁状态,来检测资源是否就绪的方案

    在这里插入图片描述
    互斥锁是检测到资源未就绪,就挂起线程;
    临界资源就绪的时间决定了使用哪种锁;

四、读者写者问题

1.读写锁

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

  • 读者写者模型与生产消费模型的本质区别:
    生产消费模型中消费者会取走数据,而读者写者模型中读者不会取走数据;

  • 读锁的优先级高

2.读写锁接口

  • 初始化:
    在这里插入图片描述

  • 读者加锁:
    在这里插入图片描述

  • 写者加锁:

在这里插入图片描述
生产消费模型中,生产者和消费者的地位是对等的,这样才能达到最高效的状态
而读写者模型中,写者只有在读者全部退出的时候才能写,是读者优先的,这样就会发生写者饥饿问题;
读者写者问题中读锁的优先级高,是因为这种模型的应用场景为:数据的读取频率非常高,而被修改的频率特别低,这样有助于提升效率;

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

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

相关文章

Window11下载安装jdk8-jdk11与环境变量的配置

目录 一、下载jdk 二、安装jdk 三、配置环境变量 四、检查JDK是否配置成功 一、下载jdk jdk8下载链接&#xff1a;请点击网址 jdk11下载链接&#xff1a;请点击网址 二、安装jdk 按照提示一步一步安装即可。 默认安装位置&#xff1a;C:\Program Files\Java 三、配置…

NetSuite海鲜书 - 知识会汇编 用户篇 2023

NetSuite2021年初夏&#xff0c;NetSuite知识会成立。它由本人&#xff0c;上海德之匠信息技术有限公司的毛岩喆&#xff08;江湖人称Rick&#xff09;发起建立。建立的初衷秉承Rick个人博客“学问思辨&#xff0c;企业信息化路上的行者”的理念&#xff0c;期望能够在NetSuite…

linux操作系统中的动静态库(未完)

1. 静态库与动态库 静态库&#xff08;.a&#xff09;&#xff1a;程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库动态库&#xff08;.so&#xff09;&#xff1a;程序在运行的时候才去链接动态库的代码&#xff0c;多个程序共享使用库的…

复旦国际MBA《人力分析战略》课程深度探讨最优解决方案

“近年来的各种社会变化和不确定性极大地促使我们真正重新审视我们的工作条件&#xff0c;以及思考如何呈现最好的工作和生活状态。”MIT斯隆管理学院Emilio J. Castilla教授指出&#xff0c;“由于期望发生了变化&#xff0c;无论是管理者还是员工&#xff0c;对工作场所的看法…

Java后端开发面试题——多线程

创建线程的方式有哪些&#xff1f; 继承Thread类 public class MyThread extends Thread {Overridepublic void run() {System.out.println("MyThread...run...");}public static void main(String[] args) {// 创建MyThread对象MyThread t1 new MyThread() ;MyTh…

C++ - 继承

继承的概念 继承机制是面向对象程序设计当中&#xff0c;非常重要且好用的手段&#xff0c;这种手段可以允许程序员在原有的类的特性基础之上&#xff0c;进行拓展从而产生新的类&#xff0c;这些新产生的类&#xff0c;我们成为派生类。在以前&#xff0c;我们实现的复用是函数…

【计算机基础】Git从安装到使用,详细每一步!扩展Github\Gitlab

&#x1f4e2;&#xff1a;如果你也对机器人、人工智能感兴趣&#xff0c;看来我们志同道合✨ &#x1f4e2;&#xff1a;不妨浏览一下我的博客主页【https://blog.csdn.net/weixin_51244852】 &#x1f4e2;&#xff1a;文章若有幸对你有帮助&#xff0c;可点赞 &#x1f44d;…

Java网络编程-Socket实现数据通信

文章目录 前言网络编程三要素IP地址和端口号传输协议Socket 使用Scoket实现网络通信TCPTCP通信-发送方TCP通信-接收方结果 UDPUDP通信-发送方UDP通信-接收方结果 总结 前言 本文主要是为下一篇Websockt做铺垫&#xff0c;大家了解socket的一些实现。 网络编程三要素 网络编程…

最小二乘法处理线性回归

最小二乘法是一种数学优化技术&#xff0c;用于查找最适合一组数据点的函数。 该方法主要用于线性回归分析&#xff0c;当然&#xff0c;也可用于非线性问题。 开始之前&#xff0c;我们先理解一下什么是回归。 回归&#xff1a;回归是一种监督学习算法&#xff0c;用于建模和…

中国行政区域带坐标经纬度sql文件及地点获取经纬度方法

文章目录 前言一、如何获取某地的经纬度&#xff1f;1.1 搜索百度地图1.2 在下方找到地图开放平台1.3 下滑找到坐标拾取器1.4 使用 二、sql文件2.1 创建表2.2 插入数据 前言 当工作业务上需要涉及地图&#xff0c;给前端返回经纬度等场景&#xff0c;需要掌握区域经纬度的获取…

Unity中Transform移动相关

路程 方向 * 速度 * 时间 参数一&#xff1a;表示位移多少 路程 方向 * 速度 * 时间 参数二&#xff1a;表示 相对坐标系 默认 该参数 是相对于自己坐标系的 相对于世界坐标系的 Z轴 动 始终是朝 世界坐标系 的 Z轴正方向移动 this.transform.Translate(Vector3.forwar…

经管博士必备基础【12】包络定理

当我们知道一个函数的最优解时&#xff0c;我们要求解这一个函数的值函数关于函数中某一个参数的导数&#xff0c;那么就可以使用包络定理。 1. 无约束条件下的包络定理 函数在其极值点处对一个参数&#xff08;参数不是自变量&#xff09;取偏导数的结果&#xff0c;等价于这…

【vue2第九章】组件化开发和根组件

组件化开发和根组件 什么是组件化开发&#xff1f; 一个页面可以拆分为多个组件&#xff0c;每个组件有自己的样式&#xff0c;结构&#xff0c;行为&#xff0c;组件化开发的好处就是&#xff0c;便于维护&#xff0c;利于重复利用&#xff0c;提升开发的效率。 便于维护&…

openGauss学习笔记-58 openGauss 高级特性-资源池化

文章目录 openGauss学习笔记-58 openGauss 高级特性-资源池化58.1 特性简介58.2 架构介绍58.3 功能特点58.4 适用场景与限制58.5 手动安装示例58.6 OCK RDMA使用示例58.7 OCK SCRLock使用示例 openGauss学习笔记-58 openGauss 高级特性-资源池化 58.1 特性简介 资源池化特性主…

Mybatis-plus使用@TableField(fill = FieldFill.UPDATE)完成自动填充字段如保存更新时自动更新时间

&#x1f4da;目录 填充策略枚举介绍自定义填充策略枚举的数据测试用例插入填充更新填充 结尾 填充策略枚举介绍 使用mybatis-plus完成字段的填充,使用起来也比较方便,当我们往数据库插入一条数据时我们不需要在给指定字段上new Date() ,而是使用mybatis-plus的注解完成TableFi…

VMware 安装 Centos7 超详细过程

CentOS系统&#xff0c;安装教程可参考以下&#xff1a; 哪些模型需要在Linux下运行&#xff0c;需提前预装Linux系统呢&#xff0c;评论区讨论吧 比如Noah-MP 5.0模型 1.软硬件准备 软件&#xff1a;推荐使用 VMware&#xff0c;我用的是 VMware 12 镜像&#xff1a;CentO…

全脑建模:过去、现在和未来

什么是全脑建模&#xff1f; 全脑建模(WBM)是计算神经科学的子领域&#xff0c;涉及近似全脑神经活动的计算和理论模型。该方法的目标是研究神经活动的宏观时空模式如何由解剖连接结构、内在神经动力学和外部扰动(感觉、认知、药理、电磁等)的相互作用产生。这种宏观现象及其模…

在firefox浏览器下破解hackbar

目录 一、介绍&#xff1a; 二、安装教程 1、打开firefox浏览器插件管理扩展 2、在firefox浏览器下安装老版本Hackbar &#xff08;1&#xff09;首先删除之前安装的Hackbar插件&#xff1a; &#xff08;2&#xff09;采用从文件安装附件添加组&#xff1a; &#xff08;3…

Xubuntu16.04系统中解决无法识别exFAT格式的U盘

问题描述 将exFAT格式的U盘插入到Xubuntu16.04系统中&#xff0c;发现系统可以识别到此U盘&#xff0c;但是打不开&#xff0c;查询后发现需要安装exfat-utils库才行。 解决方案&#xff1a; 1.设备有网络的情况下 apt-get install exfat-utils直接安装exfat-utils库即可 2.设备…

Python安装指南(Windows版)

安装python环境 官网下载地址&#xff1a; Download Python | Python.org 我选择3.10.4版本&#xff0c;当然你也可以选择其他版本 安装 安装完成&#xff0c;需要验证是否安装成功。 打开CMD窗口&#xff0c;输入python命令&#xff0c;如果进入如下python窗口则安装成功&…