Linux操作系统多线程

Posted Ricky_0528

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统多线程相关的知识,希望对你有一定的参考价值。

文章目录

4. 线程池

介绍

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

应用场景

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

示例

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

Task.hpp

#pragma

#include <iostream>

namespace ns_task

    class Task
    
    private:
        int _x;
        int _y;
        char _op;

    public:
        Task()
        
        
        Task(int x, int y, char op) : _x(x), _y(y), _op(op)
        
        
        ~Task()
        
        

    public:
        int run()
        
            int res = 0;
            switch (this->_op)
            
            case '+':
                res = this->_x + this->_y;
                break;
            case '-':
                res = this->_x - this->_y;
                break;
            case '*':
                res = this->_x * this->_y;
                break;
            case '/':
                res = this->_x / this->_y;
                break;
            case '%':
                res = this->_x % this->_y;
                break;
            default:
                std::cout << "Calculation error..." << std::endl;
                break;
            
            std::cout << "The calculation result is " << res << std::endl;
            return res;
        
    ;

ThreadPool.hpp

#pragma once

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

namespace ns_threadpool

    const int default_num = 5;

    template <class T>
    class ThreadPool
    
    private:
        int _num;
        std::queue<T> _task_queue;
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;

    public:
        ThreadPool(int num = default_num) : _num(num)
        
            pthread_mutex_init(&this->_mtx, nullptr);
            pthread_cond_init(&this->_cond, nullptr);
        
        ~ThreadPool()
        
            pthread_mutex_destroy(&this->_mtx);
            pthread_cond_destroy(&this->_cond);
        
        void lock()
        
            pthread_mutex_lock(&this->_mtx);
        
        void unlock()
        
            pthread_mutex_unlock(&this->_mtx);
        
        bool isEmpty()
        
            return this->_task_queue.empty();
        
        void wait()
        
            pthread_cond_wait(&this->_cond, &this->_mtx);
        
        void wake()
        
            pthread_cond_signal(&this->_cond);
        

    public:
        // 线程无法直接执行类内的方法,因为类中的方法参数列表会隐含一个this指针,需要定义为静态成员函数
        static void *runtime(void *args)
        
            pthread_detach(pthread_self());
            ThreadPool<T> *_this = (ThreadPool<T> *)args;

            while (true)
            
                _this->lock();
                while (_this->isEmpty())
                
                    _this->wait();
                

                T *task = new T();
                _this->popTask(task);
                _this->unlock();
                task->run();
            
        
        void initThreadPool()
        
            pthread_t tid;
            for (int i = 0; i < this->_num; i++)
            
                pthread_create(&tid, nullptr, runtime, (void *)this);
            
        
        void pushTask(const T &in)
        
            lock();
            this->_task_queue.push(in);
            unlock();
            this->wake();
        
        void popTask(T *out)
        
            *out = this->_task_queue.front();
            this->_task_queue.pop();
        
    ;

Main.cc

#include "Task.hpp"
#include "ThreadPool.hpp"

#include <ctime>

using namespace ns_threadpool;
using namespace ns_task;

int main()

    srand((long long)time(nullptr));

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

    while (true)
    
        Task t(rand() % 20 + 1, rand() % 10 + 1, "+-*/%"[rand() % 5]);
        tp->pushTask(t);

        sleep(1);
    

    return 0;

5. 单例模式

某些类,只应该具有一个对象(实例),就称之为单例

  • 语义上只有一个
  • 该对象内部存在大量的空间,保存了大量的数据,如果允许该对象存在多份,或者允许发生各种拷贝,内存中会存在很多冗余数据

5.1 饿汉模式

在加载对象时候,对象就会创建实例

template <typename T>
class Singleton

	static T data;
public:
    static T* GetInstance()
    
    	return &data;
	
;

5.2 懒汉模式

懒汉方式最核心的思想是"延时加载",从而能够优化服务器的启动速度

template <typename T>
class Singleton

	static T* inst;
public:
    static T* GetInstance()
    
        if (inst == NULL)
        
        	inst = new T();
        
        return inst;
    
;

但这样的懒汉模式存在一个严重的问题——线程不安全,第一次调用GetInstance的时候,如果多个线程同时调用,可能会创建出多份T对象的实例,这里第一次调用时的T对象也是临界资源,如果后续再次调用,就没有问题

线程安全的懒汉方式实现的线程池

ThreadPool.hpp

#pragma once

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

namespace ns_threadpool

    const int default_num = 5;

    template <class T>
    class ThreadPool
    
    private:
        int _num;
        std::queue<T> _task_queue;
        pthread_mutex_t _mtx;
        pthread_cond_t _cond;
        static ThreadPool<T> *instance;

    private:
        ThreadPool(int num = default_num) : _num(num)
        
            pthread_mutex_init(&this->_mtx, nullptr);
            pthread_cond_init(&this->_cond, nullptr);
        
        ThreadPool(const ThreadPool<T> &tp) = delete;
        ThreadPool<T> &operator=(ThreadPool<T> &tp) = delete;

    public:
        static ThreadPool<T> *getInstance()
        
            pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
            if (instance == nullptr)
            
                pthread_mutex_lock(&mtx);
                if (instance == nullptr)
                
                    instance = new ThreadPool<T>();
                    instance->initThreadPool();
                
                pthread_mutex_unlock(&mtx);
            
            return instance;
        
        ~ThreadPool()
        
            pthread_mutex_destroy(&this->_mtx);
            pthread_cond_destroy(&this->_cond);
        
        void lock()
        
            pthread_mutex_lock(&this->_mtx);
        
        void unlock()
        
            pthread_mutex_unlock(&this->_mtx);
        
        bool isEmpty()
        
            return this->_task_queue.empty();
        
        void wait()
        
            pthread_cond_wait(&this->_cond, &this->_mtx);
        
        void wake()
        
            pthread_cond_signal(&this->_cond);
        

    public:
        // 线程无法直接执行类内的方法,因为类中的方法参数列表会隐含一个this指针,需要定义为静态成员函数
        static void *runtime(void *args)
        
            pthread_detach(pthread_self());
            ThreadPool<T> *_this = (ThreadPool<T> *)args;

            while (true)
            
                _this->lock();
                while (_this->isEmpty())
                
                    _this->wait();
                

                T *task = new T();
                _this->popTask(task);
                _this->unlock();
                task->run();
            
        
        void initThreadPool()
        
            pthread_t tid;
            for (int i = 0; i < this->_num; i++)
            
                pthread_create(&tid, nullptr, runtime, (void *)this);
            
        
        void pushTask(const T &in)
        
            lock();
            this->_task_queue.push(in);
            unlock();
            this->wake();
        
        void popTask(T *out)
        
            *out = this->_task_queue.front();
            this->_task_queue.pop();
        
    ;

    template <class T>
    ThreadPool<T> *ThreadPool<T>::instance = nullptr;

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

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

不是

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

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

对于unique_ptr,由于只是在当前代码块范围内生效,因此不涉及线程安全问题

对于shared_ptr,多个对象需要共用一个引用计数变量,所以会存在线程安全问题,但是标准库实现的时候考虑到了这个问题,基于原子操作(CAS)的方式保证shared_ptr能够高效、原子的操作引用计数

6.3 其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁、写锁、行锁等),当其他线程想要访问数据时,被阻塞挂起
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作
    • 版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功
    • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等,如果相等则用新值更新,若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
  • 自旋锁:线程会反复检查锁变量是否可用,由于线程在这一过程中保持执行,因此是一种忙等待,一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁
    • 公平锁:多个线程按照申请锁的顺序来获取锁
    • 非公平锁:多个线程获取的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁在高并发的情况下,有可能会造成优先级后传或者饥饿想象

考虑线程访问临界资源的时长问题,因为将线程挂起等待是有成本的

  • 如果花费的时间非常短,就比较适合自旋锁
  • 如果花费的时间比较长,就比较适合挂起等待锁

自旋锁

  • 初始化

    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
    
  • 销毁

    int pthread_spin_destroy(pthread_spinlock_t *lock);
    
  • 加锁与解锁

    int pthread_spin_lock(pthread_spinlock_t *lock);
    
    int pthread_spin_trylock(pthread_spinlock_t *lock);
    

7. 读者写者模型

7.1 基本概念

读者写者模型

  • 对数据,大部分的操作是读取,少量的操作是写入
  • 判断依据是,进行数据读取(消费)的一端,是否会将数据取走,如果不取走,就可以考虑读者写者模型

321原则

  • 三种关系

    • 读者和读者:没有关系

      生产者消费者模型 vs 读者写者模型

      不一样的原因:读者不会取走资源,而消费者会拿走数据

    • 写者和写者:互斥、同步

    • 读者和写者:互斥关系

  • 两种角色:读者和写者,由线程承担

  • 一个交易场所:一段缓冲区(自己申请的,或者STL容器)

7.2 读写锁

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

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

7.3 基本操作

  • 初始化

    int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
    *restrict attr);
    
  • 销毁

    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
    
  • 加锁和解锁

    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
    
  • 设置读写优先

    int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
    
    • pref有三种选择
      • PTHREAD_RWLOCK_PREFER_READER_NP:默认设置,读者优先,可能会导致饥饿情况
      • PTHREAD_RWLOCK_PREFER_WRITER_NP:写者优先,目前有 BUG,导致表现行为和
      • PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP:写者优先,但写者不能递归加锁

7.4 优先级

读者优先:读者和写者同时到来的时候,让读者先进入访问

写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有读者,都不要再进入临界区访问了,等临界区中没有读者的时候,让写者先写入

以上是关于Linux操作系统多线程的主要内容,如果未能解决你的问题,请参考以下文章

java 多线程怎么深入?

Java多线程-静态条件与临界区

多线程环境,线程安全知识点Violatile和synchronized

Linux 多线程:线程安全之同步与互斥的理解

Linux下各种锁的理解和使用及总结解决epoll惊群问题(面试常考)

Linux并发与同步专题