【Linux】线程池|单例模式|STL、智能指针线程安全|读者写者问题

news2024/9/21 2:31:18

文章目录

  • 线程池
  • 线程池代码
  • 线程池单例模式
  • STL,智能指针和线程安全
  • 其他锁(了解)
  • 读者写者问题(了解)

线程池

我们去处理任务时,一个任务对应一个创建一个线程进行处理,效率是比较低的。我们可以预先创建一批线程,任务队列里没有任务的时候,每个线程都休眠,当队里中有任务的时候,就可以环形线程进行处理了。唤醒线程的成本比创建整个线程的成本小,这就是线程池的逻辑思想。

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

线程池常见的应用场景如下:

需要大量的线程来完成任务,且完成任务的时间比较短。

对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

线程池代码

整体框架结构:

image-20230424194947899

Thread.hpp的简单封装线程,下面我们进行简单的验证

Thread类主要成员变量是线程名,函数,线程参数,参数ID以及对应编号
Thread类提供了一个无参构造,完成对成员变量name的赋值。

同时,对外主要提供了start接口和join接口,对于join接口就是线程等待,而对于start接口就是创建线程的接口,在外部如果调用的话我们需要传入对应的函数以及线程对应的参数。

#pragma once
#include <pthread.h>
#include <iostream>
#include <cassert>
#include <string>
#include <functional>
namespace ThreadNs
{
    typedef std::function<void*(void*)> func_t;
    const int num  =1024;
    class Thread
    {
    private:
        static void* start_routine(void*args)
        {
            Thread* _this = static_cast<Thread*>(args);
            return _this->callback();
        }
    public:
        Thread()
        {
            char namebuffer[num];
            snprintf(namebuffer,sizeof namebuffer,"thread-%d",threadnum++);
            name_ = namebuffer;
        }

        void start(func_t func,void*args = nullptr)
        {
            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 threadname()
        {
            return name_;
        }

        ~Thread()
        {}

        void* callback()
        {
            return func_(args_);
        }
    private:
        std::string name_;
        func_t func_;
        void *args_;
        pthread_t tid_;
        static int threadnum;
    };
    int Thread::threadnum = 1;
}
//进行调用
using namespace ThreadNs;
void* handler(void*args)
{
    string ret = static_cast<const char*>(args);
    while(true)
    {
        cout<<ret<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    Thread t1;
    Thread t2;
    t1.start(handler,(void*)"thread1");
    t2.start(handler,(void*)"thread2");
    t1.join();
    t1.join();
    return 0;
}

image-20230424203114255

对于任务队列,可以由多个线程进行访问,我们就需要加锁保护了,把之前写过的锁的小组件引入进来:

LockGuard.hpp

#include <iostream>
#include <mutex>
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
{
public:
    LockGuard(pthread_mutex_t*mutex)
    :mutex_(mutex)
    {
        mutex_.lock();
    }

    ~LockGuard()
    {
        mutex_.unlock();
    }
private:
    Mutex mutex_;
};

线程池代码如下:创建一批线程时,我们需要实现线程的运行函数static void*handlerTask,之所以是静态的,是因为我们要把这个运行函数传递给Thread类中的func_,不能有this指针,所以是静态成员函数。而没有this指针,我们无法访问ThreadPool里面的成员变量,所以需要封装接口供其调用。

ThreadPool.hpp

#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <mutex>
#include <unistd.h>
using namespace ThreadNs;
const int gnum = 3;
template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;
    std::string name;
public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n)
    { }
};
template <class T>
class ThreadPool
{
private:
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = (ThreadData<T> *)args;
        ThreadPool<T> *threadpool = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            T t;
            {
                LockGuard lockguard(td->threadpool->mutex());
                while(td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); 
            }
            std::cout << td->name << " 获取了一个任务" << t.toTaskString() << "并处理完成,结果是: " << t() << std::endl;
        }
        delete td;
        return nullptr;
    }
public:
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_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;
    }
    
    void Push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }

    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

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 run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            std::cout << t->threadname() << "start..." << std::endl;
        }
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }
private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

我们将线程池进行了模板化,因此线程池当中存储的任务类型可以是任意的;现在我们想像之前处理各种数据的计算,那么先引入任务组件Task.hpp:

Task.hpp

#pragma once
#include <iostream>
#include <functional>
class Task
{
    using func_t = std::function<int(int,int ,char)>;
public:
    Task(){}

    Task(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[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = %d",_x,_op,_y,result);
        return buffer;
    }

    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer,sizeof buffer,"%d %c %d = ?",_x,_op,_y);
        return buffer;
    }
private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

const std::string oper = "+-*/%";
int mymath(int x,int y,char op)
{
    int result = 0;
    switch(op)
    {
    case '+':
        result = x+y;
        break;
    case '-':
        result = x-y;
        break;
    case '*':
        result = x*y;
        break;
    case '/':
        if(y==0)
        {
            std::cerr<<"div zero error!"<<std::endl;
            result = -1;
        }
        else
        {
            result = x/y;
        }
        break;
    case '%':
        if(y==0)
        {
            std::cerr<<"mod zero error!"<<std::endl;
            result = -1;
        }
        else
        {
            result = x%y;
        }
        break;
    default:
        break;
    }
    return result;
}

main.cc

#include "ThreadPool.hpp"
#include "Thread.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <ctime>
int main()
{
   ThreadPool<Task>* tp = new ThreadPool<Task>();
   tp->run();
   srand(time(0));
   int x,y;
   char op;
   while(true)
   {
    x = rand()%10+1;
    y = rand()%20+1;
    op  =oper[rand()%oper.size()];
    Task t(x,y,op,mymath);
    tp->Push(t);
    sleep(1);
   }
    return 0;
}

image-20230425171937402

线程池单例模式

单例模式是一种创建型设计模式,它保证一个类只有一个实例存在,并且提供一个全局访问点来访问该实例。饿汉方式和懒汉方式是两种常见的单例模式的实现方式。单例模式的主要特点包括:

只能有一个实例。
全局访问点,方便访问该实例。
在很多服务器开发场景中,经常需要让服务器加载很多的数据 (上百G) 到内存中,此时往往要用一个单例的类来管理这些数据。

我们要做的第一步就是把构造函数私有,再把拷贝构造和赋值运算符重载delete:

image-20230425184056441

接下来就要在成员变量中定义一个静态指针,方便获取单例对象。:

image-20230425183045650

在设置获取单例对象的函数的时候,注意要设置成静态成员函数,因为在获取对象前根本没有对象,无法调用非静态成员函数(无this指针):

image-20230425183113446

主函数进行调用:

image-20230425183858039

image-20230425184131497

不过也许会出现多个线程同时申请资源的场景,所以还需要一把锁来保护这块资源,而这把锁也得设置成静态,因为GetSingle()函数是静态的

image-20230426000144946

运行结果:

image-20230426000321053

线程池单例模式完整代码:

#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
#include <mutex>
#include <unistd.h>
using namespace ThreadNs;
const int gnum = 3;
template <class T>
class ThreadPool;

template <class T>
class ThreadData
{
public:
    ThreadPool<T> *threadpool;
    std::string name;

public:
    ThreadData(ThreadPool<T> *tp, const std::string &n) : threadpool(tp), name(n)
    {
    }
};
template <class T>
class ThreadPool
{
private:
    static void *handlerTask(void *args)
    {
        ThreadData<T> *td = (ThreadData<T> *)args;
        ThreadPool<T> *threadpool = static_cast<ThreadPool<T> *>(args);
        while (true)
        {
            T t;
            {
                // td->threadpool->lockQueue();
                LockGuard lockguard(td->threadpool->mutex());

                while (td->threadpool->isQueueEmpty())
                {
                    td->threadpool->threadWait();
                }
                t = td->threadpool->pop(); // pop的本质是将任务从公共队列中拿到当前线程独立的栈中
                // td->threadpool->unlockQueue();
            }
            std::cout << td->name << " 获取了一个任务" << t.toTaskString() << "并处理完成,结果是: " << t() << std::endl;
        }
        delete td;
        return nullptr;
    }

    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 operator=(const ThreadPool &) = delete;
    ThreadPool(const ThreadPool &) = delete;

public:
    void lockQueue() { pthread_mutex_lock(&_mutex); }
    void unlockQueue() { pthread_mutex_unlock(&_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;
    }

    pthread_mutex_t *mutex()
    {
        return &_mutex;
    }

public:
    void run()
    {
        for (const auto &t : _threads)
        {
            ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
            t->start(handlerTask, td);
            std::cout << t->threadname() << "start..." << std::endl;
        }
    }

    void Push(const T &in)
    {
        LockGuard lockguard(&_mutex);
        _task_queue.push(in);
        pthread_cond_signal(&_cond);
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
        for (const auto &t : _threads)
            delete t;
    }
    static ThreadPool<T> *getInstance()
    {
        if (nullptr == tp)
        {
            _lock.lock();
            if (tp == nullptr)
            {
                tp = new ThreadPool<T>();
            }
            _lock.unlock();
        }
        return tp;
    }
private:
    int _num;
    std::vector<Thread *> _threads;
    std::queue<T> _task_queue;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
    static ThreadPool<T> *tp;
    static std::mutex _lock;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
template <class T>
std::mutex ThreadPool<T>::_lock;

STL,智能指针和线程安全

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

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

其他锁(了解)

悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试

自旋锁:使用自旋锁时,当多线程发生竞争锁的情况时,加锁失败的线程会忙等待(这里的忙等待可以用 while 循环等待实现),直到它拿到锁。而互斥锁加锁失败后,线程会让出 CPU 资源给其他线程使用,然后该线程会被阻塞挂起。如果成功申请临界资源的线程,临界区代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和临界区代码执行的时间是成正比的关系。如果临界区代码执行的时间很短,就不应该使用互斥锁,而应该选用自旋锁。因为互斥锁加锁失败,是需要发生上下文切换的,如果临界区执行的时间比较短,那可能上下文切换的时间会比临界区代码执行的时间还要长。

读者写者问题(了解)

读写锁由读锁和写锁两部分构成,如果只读取共享资源用读锁加锁,如果要修改共享资源则用写锁加锁。

image-20230426092116804

如果写锁没被写线程持有,多个读线程能够并发持有锁,提高共享资源的访问效率,因为读锁用于读取共享资源,所以多个线程持有读锁也不会破坏共享资源的数据。

一旦写锁被线程持有后,读线程获取锁的操作会被阻塞,而其他写线程的获取写锁的操作也会被阻塞

注意:写独占,读共享,读锁优先级高

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

读写锁接口

//初始化读写锁
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);

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

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

相关文章

软件框架技术-实现在数据库中建立信息表,在id当中进行编写显示、查询、增加、修改、删除数据的代码,最后在localhost8080端口进行输出并显示在网页上

友情提示&#xff1a;本文代码较长&#xff0c;逻辑上较为复杂&#xff0c;若有需要建议详细阅读!!! 目录 前言 一、代码目录结构 二、数据库Student表的建立 三、idea代码配置 3.1 Student.java类&#xff08;entity软件包中&#xff09; 3.2 StudentDao接口类&#xff…

一图看懂 openai 模块:ChatGPT的API python库, 资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 openai 模块&#xff1a;ChatGPT的API python库, 资料整理笔记&#xff08;大全&#xff09; 摘要模块图类关系图结束 摘要 全文介绍系统内置 openai ——ChatGPT的API pyt…

300张现场照片,揭秘移动云大会!

今天&#xff0c;中国移动主办的移动云大会&#xff0c;在苏州金鸡湖国际会议中心正式开幕。 移动云这两年发展很猛&#xff0c;营收从2020年的110多亿&#xff0c;到2021年的240多亿&#xff0c;2022年更是飙到了500多亿&#xff0c;每年翻番。据说&#xff0c;未来三年&#…

《面试1v1》java多线程

我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 面试官&#xff1a; 说说你对多线程的理解? 候选人&#xff1a; 多线程就是同时运行多个线程,实现一件事的并行处理。比如开个程序,同时下载多个文件,同时处理多个…

linux-基础语法

本篇博客使用的是 Xshell 登录的云服务器。 Xshell 使用 Alt Enter 的快捷键就可以自动全屏和 取消全屏。 Linux 基础语法 空文件也是需要存储空间的&#xff0c;假设我们创建一个空的 txt 文本文档&#xff0c;虽然我们看着文件大小是 0kb 但是 文件当中时候文件属性的&…

智能AI文档管理新方式,手把手教你打造自己的文档聊天机器人

一个快速指南&#xff0c;为您构建一个聊天机器人网站&#xff0c;可以接受外部文档作为上下文。 随着每天涌现的信息和知识在我的屏幕上呈现&#xff0c;我们面临着人类阅读和记忆自然限制的挑战&#xff0c;这使得跟上信息更新变得越来越困难。现在&#xff0c;像ChatGPT和Ll…

游戏企业如何做用户行为序列分析?

用户行为序列是指固定时间内单个用户按照时间顺序依次触发的部分或全部事件。通过对用户行为序列进行分析&#xff0c;企业可以归纳出群体用户的行为特征&#xff0c;辅助产品运营和迭代。 通常&#xff0c;企业可以将用户行为序列分析应用于所有的分析场景。比如&#xff0c;当…

深入解析CFS任务的负载均衡(框架篇)

本文出现的内核代码来自Linux5.4.28&#xff0c;如果有兴趣&#xff0c;读者可以配合代码阅读本文。 一、什么是负载均衡 1、什么是CPU负载&#xff08;load&#xff09; CPU负载是一个很容易和CPU利用率&#xff08;utility&#xff09;混淆的概念。CPU利用率是CPU忙闲的比例…

全面学习Selenium和Python的Web自动化测试项目实战

目录 摘要&#xff1a; 1.安装依赖项 2.编写测试用例 3.执行测试用例 4.结论 摘要&#xff1a; 随着Web应用程序的不断发展和更新&#xff0c;保证其质量和稳定性变得越来越重要。为了实现这一目标&#xff0c;Web自动化测试已经成为了必不可少的一部分。本文将介绍一个基…

二十、线索关联市场活动(一):查询市场活动

功能需求 用户在线索明细页面,点击"关联市场活动"按钮,弹出线索关联市场活动的模态窗口; 用户在线索关联市场活动的模态窗口,输入搜索条件,每次键盘弹起,根据名称模糊查询市场活动,把所有符合条件的市场活动显示到列表中; 用户选择要关联的市场活动,点击"关联…

SparkStreaming学习——读取socket的数据和kafka生产者的消息

目录 一、Spark Streaming概述 二、添加依赖 三、配置log4j 1.依赖下载好后打开IDEA最左侧的外部库 2.找到spark-core 3.找到apache.spark目录 4.找到log4j-defaults.properties文件 5.将该文件放在资源目录下&#xff0c;并修改文件名 6.修改log4j.properties第19行的…

Vue.js学习-1

一、Vue.js环境准备 官网地址&#xff1a;Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org) Vue.js v2教程&#xff1a;Vue.js (vuejs.org) 在浏览器中安装Vue调试工具&#xff1a;Installation | Vue Devtools (vuejs.org) VSCode安装见这里&#xff1a; 下载vue.j…

每日学术速递4.26

CV - 计算机视觉 | ML - 机器学习 | RL - 强化学习 | NLP 自然语言处理 Subjects: cs.CV 1.AutoNeRF: Training Implicit Scene Representations with Autonomous Agents 标题&#xff1a;AutoNeRF&#xff1a;使用自主代理训练隐式场景表示 作者&#xff1a;Pierre Marz…

macOS 13.4Beta 3(22F5049e)发布

系统介绍 4 月 26 日消息&#xff0c;苹果今日向 Mac 电脑用户推送了 macOS 13.4 开发者预览版 Beta 3 更新&#xff08;内部版本号&#xff1a;22F5049e&#xff09;&#xff0c;本次更新距离上次发布隔了 14 天。 macOS Ventura 带来了台前调度、连续互通相机、FaceTime 通…

Go | 一分钟掌握Go | 5 - 切片

作者&#xff1a;Mars酱 声明&#xff1a;本文章由Mars酱编写&#xff0c;部分内容来源于网络&#xff0c;如有疑问请联系本人。 转载&#xff1a;欢迎转载&#xff0c;转载前先请联系我&#xff01; 说明 切片和数组有点像&#xff0c;对于我的理解就是声明了固定长度的就是数…

「 Redis 」RDB和AOF持久化全面解析

「 Redis 」RDB和AOF持久化全面解析 参考&鸣谢 【说透Redis】10分钟彻底理解Redis的持久化机制&#xff1a;RDB和AOF 程序员读书 AOF 持久化是怎么实现的&#xff1f; xiaolinCoding Redis持久化之RDB与AOF 的区别 1024下午茶 文章目录 「 Redis 」RDB和AOF持久化全面解析前…

设计模式之解释器模式(C++)

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 一、解释器模式是什么&#xff1f; 解释器模式是一种行为型的软件设计模式&#xff0c;定义了一个解释器&#xff0c;来解释给定语…

C语言函数大全-- q 开头的函数

C语言函数大全 本篇介绍C语言函数大全-- q 开头的函数 1. qsort 1.1 函数说明 函数声明函数功能void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *));用于将指定数组按指定顺序进行排序 参数&#xff1a; base &#xff1a; 指…

2023年,企业如何做好团队知识管理?

团队知识管理是一个组织管理中非常重要的组成部分。成熟的企业通常会非常注重团队知识管理的实践&#xff0c;以提高团队的协作效率和整体绩效。本文将介绍成熟企业如何做好团队知识管理&#xff0c;以提高企业的竞争力和创新能力。 一、了解团队知识管理的重要性 团队知识管…

【网络进阶】五种IO网络模型(二)

文章目录 1. 多路复用IO2. 异步IO3. 信号驱动IO 1. 多路复用IO I/O多路复用这个术语可能对一些人来说比较陌生&#xff0c;但提到select/epoll&#xff0c;就容易理解了。在某些场景下&#xff0c;这种I/O方式也被称为事件驱动I/O&#xff08;event-driven I/O&#xff09;。我…