Linux之线程池

news2024/9/25 11:13:57

线程池

  • 线程池概念
  • 线程池的应用场景
  • 线程池实现原理
  • 单例模式下线程池实现
  • STL、智能指针和线程安全
  • 其他常见的各种锁

线程池概念

线程池:一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。

这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

线程池的应用场景

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

线程池实现原理

线程池通过一个线程安全的阻塞任务队列加上一个或一个以上的线程实现,线程池中的线程可以从阻塞队列中获取任务进行任务处理,当线程都处于繁忙状态时可以将任务加入阻塞队列中,等到其它的线程空闲后进行处理。
在这里插入图片描述

testMain.cc

主线程任务逻辑启动线程,不断向任务队列中push任务就可以了,此时线程接收到任务就会进行处理:

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

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    ThreadPool<Task>* tp = new ThreadPool<Task>();

    //启动线程
    tp->run();

    //主线程执行任务
    while(true)
    {
        int x = rand() % 100 + 1;
        usleep(1000);
        int y = rand() % 50 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

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

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

        sleep(1);
    }
    return 0;
}

thread.hpp

我们对创建线程进行封装,包含线程名,线程个数,回调函数,线程ID等;

#pragma once

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

typedef void *(*func_t)(void *);

class ThreadData
{
public:
    std::string name_;
    void *args_;
};

class Thread
{
public:
    Thread(int num, func_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_);
    }

    void join()
    {
        pthread_join(tid_, nullptr);
    }

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

    ~Thread()
    {
    }

private:
    std::string name_; // 线程名
    int num_;          // 线程个数
    func_t func_;      // 回调函数
    pthread_t tid_;    // 线程ID
    ThreadData tdata_;
};

threadPool.hpp

线程池中我们需要用注意的是:

  1. 需要用到条件变量与互斥锁,因为线程池中的任务队列会被多个执行流访问,所以我们必须引入互斥锁;
  2. 当线程池中任务队列为满时,我们此时push任务就无法push进去,此时就需要挂起等待,直到线程将某一任务执行完毕,唤醒等待队列,才可以继续进行push,我们执行任务也是一样,只有当任务队列中有任务时,我们才可以执行,否则就需要挂起等待,直到有任务生成才去获取任务;
  3. 线程执行例程需要设置为静态方法,原因如下:
  • 使用pthread_create函数创建线程时,需要为创建的线程传入一个routine(执行例程),该routine只有一个参数类型为void的参数,以及返回类型为void的返回值。因为我们将线程池封装为一个类,此时routine函数就包含两个参数,第一个参数就是隐含的this指针,直接用来创建线程程序是会报错的;
  • 静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将routine设置为静态方法,此时routine函数才真正只有一个参数类型为void*的参数。
  • 但是在静态成员函数内部无法调用非静态成员函数,而我们需要在routine函数当中调用该类的某些非静态成员函数,比如pop。因此我们需要在创建线程时,向routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在routine函数内部调用非静态成员函数了。
#pragma once

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

#define 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;
    }

public:
    ThreadPool(int thread_num = 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));
        }
    }

    // 生产
    void run()
    {
        for (auto &iter : threads_)
        {
            iter->start();
            // std::cout << iter->name() << "启动成功" << std::endl;
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    static void *routine(void *args)
    {

        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;

        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;  // 条件变量
};

lockGuard.hpp

为了代码更加的模块化,我们将互斥锁进行一个封装成一个RAII风格的锁,创建对象是调用构造函数加锁,出作用域调用析构函数解锁:

#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_;
};

Task.hpp

这是一个加法的计算任务:

#pragma once

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

typedef std::function<int(int, int)> tfunc_t;

class Task
{
public:
    Task()
    {
    }

    Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func)
    {
    }

    void operator()(const std::string& name)
    {
        // std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;
        logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);
    }

private:
    int x_;
    int y_;
    tfunc_t func_;
};

log.hpp

此处我们在设置一个日志文件,完整的日志功能,至少: 日志等级 时间 支持用户自定义(日志内容, 文件行,文件名);

#pragma once

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

typedef std::function<int(int, int)> tfunc_t;

class Task
{
public:
    Task()
    {
    }

    Task(int x, int y, tfunc_t func) : x_(x), y_(y), func_(func)
    {
    }

    void operator()(const std::string& name)
    {
        // std::cout << "线程 " << name << " 处理完成, 结果是: " << x_ << "+" << y_ << "=" << func_(x_, y_) << std::endl;
        logMessage(WARNING, "%s处理完成:%d+%d = %d | %s | %d", name.c_str(), x_, y_, func_(x_, y_), __FILE__, __LINE__);
    }

private:
    int x_;
    int y_;
    tfunc_t func_;
};

运行代码后,我们就会发现此时就有4个线程,其中1个为主线程:
在这里插入图片描述
并且我们会发现这3个线程在处理时会呈现出一定的顺序性,因为主线程是每秒push一个任务,这3个线程只会有一个线程获取到该任务,其他线程都会在等待队列中进行等待,当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后,当主线程再次push一个任务后会唤醒等待队列首部的一个线程,这个线程处理完任务后又会排到等待队列的最后,因此这3个线程在处理任务时会呈现出一定的顺序性。
在这里插入图片描述

单例模式下线程池实现

单例模式:指的就是一个类只能创建一个对象,该模式可以保证系统中该类只有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。

接下来我们以懒汉模式为例,来实现我们的线程池:

  1. 首先,我们需要将线程池中构造函数设置为私有,因为我们不想让他被多次访问,同时我们也要防止赋值和拷贝的情况发生,我们需要将拷贝构造函数与赋值运算符重载函数设置为私有或者删除;
  2. 提供一个指向单例对象的static指针,并在程序入口之前先将其初始化为空;
  3. 提供一个全局访问点获取单例对象。

通过上述三点就可以将我们的代码做出如下改变:

threadPool.hpp

#pragma once

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

#define 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 = 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));
        }
    }

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

public:
    static ThreadPool<T> *getThreadPool(int num = NUM)
    {
        if (thread_ptr == nullptr)
        {
            LockGuard lockguard(&mutex);

            if (thread_ptr == nullptr)
            {
                thread_ptr = new ThreadPool<T>(num);
            }
        }

        return thread_ptr;
    }

    // 生产
    void run()
    {
        for (auto &iter : threads_)
        {
            iter->start();
            // std::cout << iter->name() << "启动成功" << std::endl;
            logMessage(NORMAL, "%s %s", iter->name().c_str(), "启动成功");
        }
    }

    static void *routine(void *args)
    {

        ThreadData *td = (ThreadData *)args;
        ThreadPool<T> *tp = (ThreadPool<T> *)td->args_;

        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;  // 条件变量

    static ThreadPool<T> *thread_ptr;
    static pthread_mutex_t mutex;
};

template <typename T>
ThreadPool<T> *ThreadPool<T>::thread_ptr = nullptr;

template <typename T>
pthread_mutex_t ThreadPool<T>::mutex = PTHREAD_MUTEX_INITIALIZER;

我们需要注意的是getThreadPool函数在创建对象过程中需要双检查加锁,因为简单的在if语句前后进行加锁解锁操作的话,后续在获取创建的单例对象操作时就会进行大量无意义的加锁解锁操作,我们进行双检查操作以后,就会加锁之前在进行一次判断,不为空就直接返回,就避免了后序无意义的加锁解锁操作;

testMain.cc

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

int main()
{
    srand((unsigned int)time(nullptr) ^ getpid());
    // ThreadPool<Task>* tp = new ThreadPool<Task>();

    //启动线程
    ThreadPool<Task>::getThreadPool()->run();

    //主线程执行任务
    while(true)
    {
        int x = rand() % 100 + 1;
        usleep(1000);
        int y = rand() % 50 + 1;
        Task t(x, y, [](int x, int y)->int{
            return x + y;
        });

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

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

        sleep(1);
    }
    return 0;
}

STL、智能指针和线程安全

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

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

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

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

其他常见的各种锁

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

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

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

相关文章

【JavaSE语法】运算符

一、 什么是运算符 计算机的最基本的用途之一就是执行数学运算&#xff0c;运算符即对操作数进行操作时的符号&#xff0c;不同运算符操作的含义不同 Java中运算符可分为以下&#xff1a;算术运算符( - * /)、关系运算符(< > )、逻辑运算符、位运算符、移位运算符以及条…

第十六章 反射与注解

所有 Java 类均继承了 bjet 类&#xff0c;在 Object 类中定义了一个 getClass0方法&#xff0c;该回一个类型为Class的对象。例如下面的代码: JTextField textField new JTextField();//创建JTextField对象 Class textFieldC textField.getClass();//获取Class对象 利用Cla…

Java练习题2020-3

统计从1到N的整数中,所有立方值的平方根为整数的数的个数 输入说明&#xff1a;整数 N(N<10000)&#xff1b; 输出说明&#xff1a;符合条件的数的个数&#xff0c;如4^3648^2 输入样例&#xff1a;10 输出样例&#xff1a;3 (说明&#xff1a;样例中符合条件的3个数是1、4、…

【Linux】部署单机项目以及前后端分离项目

Linux部署单机项目&#xff1a; 优点&#xff1a; 简化了系统管理&#xff1a;由于所有服务都在同一台机器上运行&#xff0c;因此可以简化系统管理和维护。 提高了性能&#xff1a;由于没有网络延迟和其他因素的影响&#xff0c;所以可以提高系统的性能。 缺点&#xff1a; 容…

C/C++宏定义和宏函数

1.概述 C/C 的宏定义和宏函数非常的有用&#xff0c;由于最近看Android 的jni代码时老是会看见如下图所示的宏定义&#xff1a; 定义完后使用的时候直接如下使用就行了&#xff1a; JMI_DECLARE_CLASS(Context,android.content);这样就能很简洁的将一些逻辑重复的代码管理起来…

71 搜索二维矩阵

搜索二维矩阵 题解1 Z字查找(tricky)题解2 一次二分查找题解3 两次二分查找 给你一个满足下述两条属性的 m x n 整数矩阵&#xff1a; 每行中的整数从左到右按非严格递增顺序排列。 每行的第一个整数大于前一行的最后一个整数。 给你一个整数 target &#xff0c;如果 target …

OpenAI 组建安全 AGI 新团队!应对AI“潘多拉魔盒”

夕小瑶科技说 原创 作者 | 小戏 一旦谈及未来 AI&#xff0c;除了天马行空的科幻畅想&#xff0c;不可避免的也有未来 AI 时代的末日预言。从 AI 武器化到 AI 欺骗&#xff0c;从邪恶 AI 到 AI 掌权&#xff0c;人工智能&#xff0c;尤其是通用人工智能的风险始终都清清楚楚的…

nrf52832 开发板入手笔记:资料搜集

前言 最近翻箱&#xff0c;发现了两块几年前买的 NRF52832 与 NRF52840 的开发板&#xff0c;打算搭个 BLE 的开发环境 NRF52832 与 NRF51822 之前用过&#xff0c; NRF52840 没有用过&#xff0c;好像是 BLE4 与 BLE5 的区别吧 相关介绍 除了开发板&#xff0c;最重要的还是…

有线网卡通过无线网卡使其它设备上网

我现在的网络是无线路由器连接公网&#xff0c;电脑上的无线网卡连接路由器使电脑上网&#xff0c;这是完全正常的连接方式。 我现在又有了一台嵌入式设备&#xff0c;它只有有线网口&#xff0c;所以就只能用有线的方式连网&#xff0c;但是我的无线路由器不在电脑旁边&#x…

【23种设计模式】依赖倒置原则

个人主页&#xff1a;金鳞踏雨 个人简介&#xff1a;大家好&#xff0c;我是金鳞&#xff0c;一个初出茅庐的Java小白 目前状况&#xff1a;22届普通本科毕业生&#xff0c;几经波折了&#xff0c;现在任职于一家国内大型知名日化公司&#xff0c;从事Java开发工作 我的博客&am…

openpnp - 程序发布包的制作

文章目录 openpnp - 程序发布包的制作概述笔记程序发布 - 简易打包备注程序发布 - 用install4j来打包END openpnp - 程序发布包的制作 概述 openpnp自带了intall4j的安装脚本. 官方说明这是intall4j 8.x的工程. 下载了intall4j 8.x(找不到注册码, 只能是90天试用版) 和 10.x…

小黑子—spring:第二章 注解开发

spring入门2.0 二 小黑子的spring注解开发1. Bean的基本注解开发1.1 注解版本1.2 Component使用和作用范围1.2.1 作用范围等注解使用1.2.2 Component的三个衍生注解 2. Bean依赖注入注解开发2.1 依赖注入相关注解2.2 Autowired扩展 3. 非自定义Bean注解开发4. Bean配置类的注解…

计算机网络【CN】子网划分与子网掩码

一个子网定义(X.X.X.X/n) 子网掩码为 n 个 1&#xff0c;32-n 个 0包含的 IP 地址数&#xff1a;232−n 主机号全 0 表示本网段主机号全 1 表示网段的广播地址可分配的 IP 地址数 :232−&#x1d45b;−2 子网划分原则 满足子网定义子网&#x1d434;1…&#x1d434;&#x…

牛客网刷题-(6)

&#x1f308;write in front&#x1f308; &#x1f9f8;大家好&#xff0c;我是Aileen&#x1f9f8;.希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流. &#x1f194;本文由Aileen_0v0&#x1f9f8; 原创 CSDN首发&#x1f412; 如…

QT OpenGL (1)2D Painting Example

2D Painting Example 为方便查阅&#xff0c;此文是原网站文档翻译与整理&#xff0c;如有侵权&#xff0c;请与本人联系。 官网 目录 2D Painting Example概述Helper类定义Helper类实现Widget类定义Widget类实现GLWidget类定义GLWidget类实现Window 类定义Window 类实现运行示…

《利息理论》指导 TCP 拥塞控制

欧文费雪《利息原理》第 10 章&#xff0c;第 11 章对利息的几何说明是普适的&#xff0c;任何一个负反馈系统都能引申出新结论。给出原书图示&#xff0c;本文依据于此&#xff0c;详情参考原书&#xff1a; 将 burst 看作借贷是合理的&#xff0c;它包含成本(报文)&#xf…

代码随想录算法训练营第三十五天丨 贪心算法part06

738.单调递增的数字 思路 暴力解法 题意很简单&#xff0c;那么首先想的就是暴力解法了【超时】。 贪心算法 题目要求小于等于N的最大单调递增的整数&#xff0c;那么拿一个两位的数字来举例。 例如&#xff1a;98&#xff0c;一旦出现strNum[i - 1] > strNum[i]的情况…

通过Vue自带服务器实现Ajax请求跨域(vue-cli)

通过Vue自带服务器实现Ajax请求跨域&#xff08;vue-cli&#xff09; 跨域 原理&#xff1a;从A页面访问到B页面&#xff0c;并且要获取到B页面上的数据&#xff0c;而两个页面所在的端口、协议和域名中哪怕有一个不对等&#xff0c;那么这种行为就叫跨域。注意&#xff1a;类…

大厂面试题-Java并发编程基础篇(二)

目录 一、wait和notify这个为什么要在synchronized代码块中&#xff1f; 二、ThreadLocal是什么&#xff1f;它的实现原理呢&#xff1f; 三、基于数组的阻塞队列ArrayBlockingQueue原理 四、怎么理解线程安全&#xff1f; 五、请简述一下伪共享的概念以及如何避免 六、什…

【Qt之控件QKeySequenceEdit】分析及使用

描述 QKeySequenceEdit小部件允许输入一个QKeySequence。 该小部件允许用户选择一个QKeySequence&#xff0c;通常用作快捷键。当小部件获取焦点时&#xff0c;录制将开始&#xff0c;并在用户释放最后一个键后的一秒钟结束。 用户可以使用输入键盘来输入键序列。通过调用get…