【Linux】线程池线程安全的单例模式和STL读者写者问题

需要云服务器等云产品来学习Linux的同学可以移步/–>腾讯云<–/官网,轻量型云服务器低至112元/年,优惠多多。(联系我有折扣哦)

文章目录

  • 1. 线程池
    • 1.1 线程池是什么
    • 1.2 为什么要有线程池
    • 1.3 线程池的应用场景
    • 1.4 线程池的任务
    • 1.5 线程池的代码实现
  • 2. 线程安全的单例模式
  • 3. STL、智能指针和线程安全
  • 4. 其他常见锁的了解
  • 5. 读者写者问题

1. 线程池

1.1 线程池是什么

线程池是一种线程的使用方式,是一种池化技术,在很早之前,我们其实接触过一些池化技术的例子,比如STL容器内部实现的alloctor就是一种内存的池化技术,将要用的空间首先申请一大批,然后再按照容器的需要从这个内存池中申请。还有在学习进程的时候,我们也尝试写过一些进程池的代码,一次创建一大批进程,按照要求分别执行不同的任务。当然这里也可以创建线程池,用线程来执行任务

1.2 为什么要有线程池

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

1.3 线程池的应用场景

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

1.4 线程池的任务

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

1.5 线程池的代码实现

/* ThreadPool.hpp 线程池的代码实现 */
#pragma once
#include "LockGuard.hpp"
#include "Thread.hpp"
#include <vector>
#include <queue>
#include <string>
#include <iostream>

const int gnum = 5; // 线程池中默认的线程个数

template <class T>
class ThreadPool; // 线程池类的声明

/* 线程数据类,保存线程对应的内容包括线程池对象的指针和线程名 */
template <class T>
class ThreadData
{
public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n){};

public:
    ThreadPool<T> *threadpool;
    std::string name;
};

/* 线程池类的实现 */
template <class T>
class ThreadPool
{
public:
    static void *handleTask(void *args) // 线程需要执行的回调函数
    {
        ThreadData<T> *td = static_cast<ThreadData<T> *>(args);
        while (true)
        {
            T t; // 构建任务对象
            {
                LockGuard lockGuard(td->threadpool->mutex()); // 上锁
                while (td->threadpool->isQueueEmpty())
                {
                    // 如果任务队列为空,线程挂起,等待队列中被填充任务
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // 如果队列中有任务,就拿出任务
            }
            // 任务在锁外执行
            std::cout << "获取了一个任务" << t.toTaskString() << "并执行了任务,结果是 " << t() << std::endl;
        }
        delete td;
        return nullptr;
    }

public: // 给handleTask调用的外部接口
    pthread_mutex_t *mutex() { return &_mutex; }
    bool isQueueEmpty() { return _task_queue.empty(); }
    void threadWait() { pthread_cond_wait(&_cond, &_mutex); }
    T pop() // 获取线程池中任务队列里需要执行的下一个任务
    {
        T t = _task_queue.front();
        _task_queue.pop();
        return t;
    }

public:                               // 需要暴露给外部的接口
    ThreadPool(const int &num = gnum) // 构造函数,初始化互斥量和条件变量,构建指定个数的Thread对象
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < num; ++i)
        {
            _threads.push_back(new Thread());
        }
    }
    void run() // 为所有线程对象创建真正的执行流,并执行对应的回调函数
    {
        for (const auto &thread : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, thread->GetTaskName()); // 构造handleTask的参数对象
            thread->start(handleTask, td);                                      // 调用该线程的start函数,创建新线程执行指定的handleTask任务
            std::cout << thread->GetTaskName() << " start..." << std::endl;
        }
    }
    void push(const T &in) // 将指定任务push到队列中
    {
        // 加锁
        LockGuard lockGuard(&_mutex); // 自动加锁,在当前代码段结束之后调用LockGuard的析构函数解锁
        _task_queue.push(in);
        pthread_cond_signal(&_cond); // 发送信号表示此时task_queue中有值,让消费者可以使用
    }
    ~ThreadPool() // 析构函数,销毁互斥量和条件变量,delete所有thread对象指针,自动调用thread对象的析构函数
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (auto &thread : _threads)
        {
            delete thread;
        }
    }

private:
    std::vector<Thread *> _threads; // 保存所有线程对象的指针
    std::queue<T> _task_queue;      // 需要被分配的任务队列
    pthread_mutex_t _mutex;         // 任务队列需要被互斥的访问
    pthread_cond_t _cond;           // 生产任务和消费任务之间需要进行同步
};

构建和测试线程池用到的小组件

/* Task.hpp 线程对象,C++风格的线程库*/
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
#include <cassert>

class Thread
{
public:
    using func_t = std::function<void *(void *)>; // 定义func_t类型
    static int number;                            // 线程编号,按照一次运行时的调用次数计数
public:
    Thread()
    {
        char *buffer = new char[64];
        name_ = "thread-" + std::to_string(++number);
    }
    static void *start_routine(void *args)
    {
        Thread *_this = static_cast<Thread *>(args);
        void *ret = _this->run(_this->args_);
        return ret;
    }
    void *run(void *arg)
    {
        return func_(arg);
    }
    void start(func_t func, void *args)
    {
        func_ = func;
        args_ = args;
        int n = pthread_create(&tid_, nullptr, start_routine, this);
        assert(n == 0);
        (void)n;
    }
    void join()
    {
        int n = pthread_join(tid_, nullptr);
        assert(n == 0);
        (void)n;
    }
    std::string GetTaskName()
    {
        return name_;
    }
    ~Thread() {}

private:
    std::string name_; // 线程名
    pthread_t tid_;    // 线程id
    func_t func_;      // 线程调用的函数
    void *args_;       // 线程调用函数的参数
};
int Thread::number = 0;
/* LockGuard.hpp RAII风格的互斥锁 */
#pragma once

#include <pthread.h>

class Mutex
{
public:
    Mutex(pthread_mutex_t *lock_p = nullptr) : _lock_p(lock_p) {} // 构造函数

    void lock() // 加锁
    {
        if (_lock_p)
            pthread_mutex_lock(_lock_p);
    }
    void unlock() // 解锁
    {
        if (_lock_p)
            pthread_mutex_unlock(_lock_p);
    }

    ~Mutex() {} // 析构函数

private:
    pthread_mutex_t *_lock_p;
};

class LockGuard // RAII风格的锁的实现
{
public:
    LockGuard(pthread_mutex_t *mutex) : _mutex(mutex) // 构造函数
    {
        _mutex.lock(); // 在构造函数中加锁
    }
    ~LockGuard() // 析构函数
    {
        _mutex.unlock(); // 在析构函数中解锁
    }

private:
    Mutex _mutex;
};
/* Task.hpp 测试用到的测试任务 */
#pragma once

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

static std::string oper = "+-*/%";

class CalTask
{
public:
    using func_t = std::function<int(int, int, char)>;

public:
    CalTask() {}
    CalTask(int x, int y, char op, func_t func)
        : _x(x), _y(y), _op(op), _callback(func)
    {
    }
    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[64];
        snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[64];
        snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};
class SaveTask
{
    typedef std::function<void(const std::string)> func_t;

public:
    SaveTask() {}
    SaveTask(const std::string &msg, func_t func)
        : _msg(msg), _func(func)
    {
    }
    void operator()()
    {
        _func(_msg);
    }

private:
    std::string _msg;
    func_t _func;
};
int mymath(int a, int b, char op)
{
    int ans = 0;
    switch (op)
    {
    case '+':
        ans = a + b;
        break;
    case '-':
        ans = a - b;
        break;
    case '*':
        ans = a * b;
        break;
    case '/':
    {
        if (b == 0)
        {
            std::cerr << "div zero error!" << std::endl;
            ans = -1;
        }
        else
            ans = a / b;
    }
    break;
    case '%':
    {
        if (b == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            ans = -1;
        }
        else
            ans = a % b;
    }
    break;
    default:
        break;
    }
    return ans;
}
void Save(const std::string &msg)
{
    FILE *fp = fopen("./log.txt", "a+");
    if (fp == NULL)
    {
        std::cerr << "open file error" << std::endl;
        return;
    }
    fputs(msg.c_str(), fp);
    fputs("\n", fp);

    fclose(fp);
}

测试代码:

/* main.cc */
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <string>
#include <memory>
#include <unistd.h>

int main()
{
    // 创建线程用于等待输入,执行任务
    std::unique_ptr<ThreadPool<CalTask>> tp(new ThreadPool<CalTask>());
    tp->run();

    // 主线程用于输入任务
    int x, y;
    char op;
    while (true)
    {
        std::cout << "请输入数据1# ";
        std::cin >> x;
        std::cout << "请输入数据2# ";
        std::cin >> y;
        std::cout << "请输入你要进行的运算#";
        std::cin >> op;
        CalTask t(x, y, op, mymath);
        tp->push(t);
        sleep(1);
    }
    return 0;
}

运行结果:

image-20240208212419101

2. 线程安全的单例模式

关于设计模式和单例模式的讲解,我们在C++专栏中已经有过了解,感兴趣的可以自行探究,这里附上链接【C++】特殊类设计

我们知道单例模式的设计有饿汉模式懒汉模式两种,饿汉模式的实现会拖慢启动时间,所以这里我们采用懒汉模式来实现我们刚刚创建的线程池。

在一个进程中,有一个线程池即可,所以这里我们的线程池要改写成单例模式的,我们使用懒汉模式来改写

/* 懒汉模式的实现 */
// 头文件 ...

// ...

/* 线程池类的实现 */
template <class T>
class ThreadPool
{
public:
	// ...
public: // 给handleTask调用的外部接口
	// ...
public:                               // 需要暴露给外部的接口
	// ...
    static ThreadPool<T> *getInstance()
    {
        if(nullptr == tp)
        {
            std::lock_guard<std::mutex> lck(_singletonlock);
            if(nullptr == tp)
            {
                tp = new ThreadPool<T> ();
            }
        }
        return tp;
    }
private: // 单例模式需要私有化的接口
    ThreadPool(const int &num = gnum) // 构造函数,初始化互斥量和条件变量,构建指定个数的Thread对象
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 0; i < num; ++i)
        {
            _threads.push_back(new Thread());
        }
    }
    //delete拷贝构造和析构函数
    ThreadPool(const ThreadPool<T> &) = delete;
    ThreadPool<T> *operator=(const ThreadPool<T> &) = delete;

private:
    std::vector<Thread *> _threads; // 保存所有线程对象的指针
    std::queue<T> _task_queue;      // 需要被分配的任务队列
    pthread_mutex_t _mutex;         // 任务队列需要被互斥的访问
    pthread_cond_t _cond;           // 生产任务和消费任务之间需要进行同步

    static ThreadPool<T> *tp; // 静态成员,存放ThreadPool指针
    static std::mutex _singletonlock; // 创建线程安全的单例对象要加的锁
};
template<class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template<class T>
std::mutex ThreadPool<T>::_singletonlock;

3. STL、智能指针和线程安全

STL中的容器是否是线程安全的?

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

智能指针是否是线程安全的?

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

4. 其他常见锁的了解

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

5. 读者写者问题

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

image-20240208221244264

读写锁的分析:读写锁一共是两个锁,分别为读锁和写锁,对应着读者和写者。当写者需要写数据时,请求写锁,然后再写数据,此时读者不能读,当写锁不存在时,多个读者可以并发的访问同一数据,提高了效率

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

//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

//销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//读加锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

//写加锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

//解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

本章完…

分别为读锁和写锁,对应着读者和写者。当写者需要写数据时,请求写锁,然后再写数据,此时读者不能读,当写锁不存在时,多个读者可以并发的访问同一数据,提高了效率

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

//初始化读写锁
pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);

//销毁读写锁
pthread_rwlock_destroy(pthread_rwlock_t *rwlock);

//读加锁
pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);

//写加锁
pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);

//解锁
pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

本章完…

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux 文件比较工具

在Linux系统中&#xff0c;文件比较是一种常见的任务&#xff0c;用于比较两个文件之间的差异。文件比较可以帮助我们找出两个文件的不同之处&#xff0c;或者确定它们是否完全相同。在Linux中&#xff0c;有多种方法可以进行文件比较。 1. diff 在Linux中&#xff0c;diff命…

逐鹿比特币生态,Elastos 携新作 BeL2「重出江湖」

撰文&#xff1a;Babywhale&#xff0c;Techub News 文章来源Techub News&#xff0c;搜Tehub News下载查看更多Web3资讯。 刚刚过去的 2023 年&#xff0c;「比特币生态」成为了市场的绝对焦点之一。从铭文开始&#xff0c;到重新走进大众视野的 Stacks 与比特币闪电网络&am…

Apktool任意文件写入漏洞分析 CVE-2024-21633

前置知识 在复现该漏洞前&#xff0c;有必要了解Apktool和resources.arsc相关的基础知识&#xff0c;方便理解后续POC的构造。 Apktool是一款流行的开源逆向工程软件&#xff0c;用于反编译和编译Android应用&#xff0c;因此&#xff0c;Apktool被许多其他逆向工程软件集成。…

Conda历史版本下载地址和python对应关系

一、前言 因为Conda安装版本问题&#xff0c;带来了很多问题&#xff0c;虽然不能直接确定二者之间的关系&#xff0c;但是安装指定版本的conda,确实是一个比较好的方法。特此记忆。 二、下载地址 下载最新版本&#xff1a;Free Download | Anaconda 下载历史版本&#xff…

ssm+vue的校园一卡通密钥管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的校园一卡通密钥管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系…

【JS逆向八】逆向某企查网站的headers参数,并模拟生成 仅供学习

逆向日期&#xff1a;2024.02.07 使用工具&#xff1a;Node.js 加密方法&#xff1a;未知 / 标准库Hmac-SHA512 文章全程已做去敏处理&#xff01;&#xff01;&#xff01; 【需要做的可联系我】 可使用AES进行解密处理&#xff08;直接解密即可&#xff09;&#xff1a;AES加…

PdfFactory Pro软件下载以及序列号注册码生成器

PdfFactory Pro注册机是一款针对同名虚拟打印机软件所推出的用户名和序列号生成器。PdfFactory Pro是一款非常专业的PDF虚拟打印软件&#xff0c;通过使用这款注册机&#xff0c;就能帮助用户免费获取注册码&#xff0c;一键激活&#xff0c;永久免费使用。 pdffactory7注册码如…

传输层协议 ——— TCP协议

TCP协议 TCP协议谈谈可靠性为什么网络中会存在不可靠&#xff1f;TCP协议格式TCP如何将报头与有效载荷进行分离&#xff1f;序号与确认序号 确认应答机制&#xff08;ACK&#xff09;超时重传机制连接管理机制三次握手四次挥手 流量控制滑动窗口拥塞控制延迟应答捎带应答面向字…

Java并发基础:LinkedTransferQueue全面解析!

内容概要 LinkedTransferQueue类实现了高效的线程间数据传递&#xff0c;支持等待匹配的生产者-消费者模式&#xff0c;基于链表的无界设计使其在高并发场景下表现卓越&#xff0c;且无需担心队列溢出&#xff0c;丰富的方法和良好的可扩展性满足了各种复杂应用场景的需求。 …

【Godot4.2】文件系统自定义控件 - FileSystemTree

FileSystemTree B站【Godot4.2】文件系统自定义节点 - FileSystemTree 概述 在Godot设计编辑器插件或应用程序时&#xff0c;可能需要涉及文件系统的显示&#xff0c;比如文件夹或文件的树形列表。 我们可以用Godot的Tree控件快速书写相应的功能&#xff0c;但是为了复用到…

华为OD机试C卷 - 最富裕的小家庭( Python C C++ JavaGo JS PHP)

题目描述 在一颗树中&#xff0c;每个节点代表一个家庭成员&#xff0c;节点的数字表示其个人的财富值。一个小家庭由一个节点及其直接相连的子节点组成。 现在给定一颗树&#xff0c;我们需要计算最富裕的小家庭的财富和。 输入描述 输入包括以下几行&#xff1a; 一个整…

Project2013下载安装教程,保姆级教程,附安装包和工具

前言 Project是一款项目管理软件&#xff0c;不仅可以快速、准确地创建项目计划&#xff0c;而且可以帮助项目经理实现项目进度、成本的控制、分析和预测&#xff0c;使项目工期大大缩短&#xff0c;资源得到有效利用&#xff0c;提高经济效益。软件设计目的在于协助专案经理发…

2024年【广东省安全员B证第四批(项目负责人)】考试及广东省安全员B证第四批(项目负责人)考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 广东省安全员B证第四批&#xff08;项目负责人&#xff09;考试考前必练&#xff01;安全生产模拟考试一点通每个月更新广东省安全员B证第四批&#xff08;项目负责人&#xff09;考试题题目及答案&#xff01;多做几…

基于AST实现一键自动提取替换国际化文案

背景&#xff1a;在调研 formatjs/cli 使用&#xff08;使用 formatjs/cli 进行国际化文案自动提取 &#xff09;过程中&#xff0c;发现有以下需求formatjs/cli 无法满足&#xff1a; id 需要一定的语义化&#xff1b; defaultMessage和Id不能直接hash转换&#xff1b; 需要…

MySQL篇----第七篇

系列文章目录 文章目录 系列文章目录前言一、水平分区二、分库分表之后,id 主键如何处理三、存储过程(特定功能的 SQL 语句集)前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你…

仰暮计划|“舅舅的大女儿失踪了,当时找遍了整个村庄,也报了警”

我的舅舅是1961年出生在一个偏僻的小山沟里&#xff0c;我只在很小的时候跟着我的妈妈回去过&#xff0c;我对于那里的印象很模糊&#xff0c;只有半镶在土窑里的小平房&#xff0c;门前的一条栽满樱桃树的很深的土沟&#xff0c;通往门前的陡峭的小路和露天的院子里那一颗茂盛…

LabVIEW动平衡测试与振动分析系统

LabVIEW动平衡测试与振动分析系统 介绍了利用LabVIEW软件和虚拟仪器技术开发一个动平衡测试与振动分析系统。该系统旨在提高旋转机械设备的测试精度和可靠性&#xff0c;通过精确测量和分析设备的振动数据&#xff0c;以识别和校正不平衡问题&#xff0c;从而保证机械设备的高…

图数据库 之 Neo4j - 环境搭建(2)

运行环境&#xff1a; centos7 Docker version 18.09.6 下载镜像 docker search neo4j docker pull neo4j 创建 neo4j 用户 # 创建 neo4j 用户 # -M 不创建用户的主目录 sudo useradd -M neo4j # usermod 用于修改用户属性命令 # -L 锁定用户&#xff0c;用户无法登录系统 user…

深入Pandas:精通文本数据处理的20+技巧与应用实例【第68篇—python:文本数据处理】

文章目录 Pandas文本数据处理方法详解1. str/object类型转换2. 大小写转换3. 文本对齐4. 获取长度5. 出现次数6. 编码方向7. 字符串切片8. 字符串替换9. 字符串拆分10. 字符串连接11. 字符串匹配12. 去除空格13. 多条件过滤14. 字符串排序15. 字符串格式化16. 多列文本操作17. …

Android Studio安装过程遇到SDK无法安装问题解决

首次打开studio遇到该类问题&#xff0c;需要下载SDK文件&#xff0c;后又发现SDK由于是Google源&#xff0c;无法进行正常安装&#xff0c;故转而进行SDK的镜像安装。 一、下载SDK Tools 地址&#xff1a;AndroidDevTools - Android开发工具 Android SDK下载 Android Studio…