线程互斥与同步--Linux

文章目录

  • 线程互斥的概念与意义
    • 互斥的原理--原子性
    • 关于售票模拟的互斥应用
    • 死锁问题
  • 线程同步的概念与意义
    • 条件变量实现同步
  • 生产者消费者模型--互斥与同步
    • 基于阻塞队列的生产者消费者模型
    • 基于环形队列的生产者消费者模型
      • POSIX信号量
  • 线程池
  • 线程安全下的单例模式
  • 总结

线程互斥的概念与意义

线程作为执行流和基本的调度单位,在一个进程中被调度和执行时,一个全局变量很可能会被多个线程同时访问到,一旦涉及写入操作,并且某个线程A在写入时又被切走了,线程B切入,线程B写入完成后又切回线程A,此时就很有可能会出现问题。具体可能出什么问题,我们以++操作来进行阐述:

线程互斥与同步--Linux_第1张图片

可以观察出,在++的过程中,多线程还是可能会出问题的,线程被切走的可能性还是有的,有可能时间片到了,也有可能是被信号暂停了……可能性虽然小,但是必须要预防,一旦出现线程安全问题,在涉及到金钱交易时,更是可能会导致经济损失。为了解决这种类似的问题,互斥的概念以及应用就应运而生。

首先,类似上面的全局变量gval被多个线程同时看到并被使用,被称为临界资源。每个线程内部,访问临界资源的代码,就叫做临界区。而任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。因此线程互斥的意义在于保证多个线程能够安全地使用共享资源。

互斥的原理–原子性

实现互斥,就得保证临界区在同一时间只能有一个线程进入,一旦有线程进入,就给临界区上锁,使得其他线程一旦准备进入该区域,就因为没有进入权限而被挂起,阻塞的等待在临界区前。上面给临界区上锁的过程称为加锁。问题在于,既然所有的线程都可能竞争到锁,那么这个锁也可以被认为是临界资源,这就会造成概念上的死循环:使用临界资源来保护临界资源。打破这个循环的编程者在设计锁的时候,使得申请锁的过程为原子操作。原子操作的解决问题的根源在于:内存与寄存器的数据交换是不可分割的,要不是交换完成,要不就是交换失败,根本不可能存在中间态从而给其他线程制造切入的机会。原子性的特性就很好理解了:不可分割与拆分。最终使得申请锁的过程中,即使发生了线程切换,也不会在发生在数据交换的时候,这是汇编语言保证的。下面给出图解:

线程互斥与同步--Linux_第2张图片

关于售票模拟的互斥应用

接下来我们演示一下多线程非互斥与互斥的情景,具体应用一下。

抢票模拟:创建若干线程,使得所有的线程对一个全局变量进行–操作,以实现购票的操作。

​ 非互斥抢票:

#include 
#include 
#include 
#include 
using namespace std;

int tickets=10000;//总票数
void* buyTickets(void* arg)
{
    char* name=static_cast<char*>(arg);
    while(true)
    {
        if(tickets>0)//票数大于0才进行抢票
        {
            usleep(1000);//制造被切走的机会
            cout<<name<<" 抢到了票: "<<tickets<<endl;
            tickets--;
        }
        else break;
    }
    return nullptr;
}
int main()
{
    pthread_t t1,t2,t3,t4;//创建若干个线程抢票
    pthread_create(&t1,nullptr,buyTickets,(void*)"thread 1");
    pthread_create(&t2,nullptr,buyTickets,(void*)"thread 2");
    pthread_create(&t3,nullptr,buyTickets,(void*)"thread 3");
    pthread_create(&t4,nullptr,buyTickets,(void*)"thread 4");

    //回收线程资源
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);
    pthread_join(t4,nullptr);

    return 0;
}

线程互斥与同步--Linux_第3张图片

运行的结果中居然在票数为0和-1的时候也会抢票成功,证明多线程不加锁确实会出问题。

在模拟互斥抢票之前,我们得先认识锁的数据类型和几个函数来帮助我们进行加锁操作。

pthread_mutex_t : 锁的数据类型

线程互斥与同步--Linux_第4张图片

pthread_mutex_init:

作用:初始化锁

参数:

mutex:要初始化的锁的地址

attr:要设置的锁的属性,默认传nullptr

返回值:成功返回0,失败返回错误码

pthread_mutex_destroy:

作用:销毁锁,防止内存泄漏(与pthread_mutex_init搭配使用)

参数:

mutex:要销毁的锁的地址

返回值:成功返回0,失败返回错误码

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER 语句可以直接设置一个可以使用的锁,且不需要进行手动销毁。

线程互斥与同步--Linux_第5张图片

pthread_mutex_lock:

作用:加锁

参数:

mutex:使用哪个锁进行加锁

返回值:成功返回0,失败返回错误码

pthread_mutex_unlock:

作用:解锁

参数:

mutex:使用哪个锁进行解锁

返回值:成功返回0,失败返回错误码

​ 互斥抢票模拟:

#include 
#include 
#include 
#include 
using namespace std;

pthread_mutex_t mutex; // 互斥锁
int tickets = 10000;   // 总票数
void *buyTickets(void *arg)
{
    char *name = static_cast<char *>(arg);
    while (true)
    {
        pthread_mutex_lock(&mutex); // 加锁

        if (tickets > 0) // 票数大于0才进行抢票
        {
            usleep(1000); // 制造被切走的机会
            cout << name << " 抢到了票: " << tickets << endl;
            tickets--;
            pthread_mutex_unlock(&mutex); // 解锁
            usleep(123);                  // 抢完票暂时沉寂一下,给其他线程抢的机会
        }
        else 
        {
            pthread_mutex_unlock(&mutex); // 解锁,在这里多放一个解锁语句,防止条件不满足时有一个线程无法完成解锁
            usleep(123);                  // 抢完票暂时沉寂一下,给其他线程抢的机会
            break;
        }
    }
    return nullptr;
}
int main()
{
    pthread_mutex_init(&mutex, nullptr); // 初始化锁
    pthread_t t1, t2, t3, t4;            // 创建若干个线程抢票
    pthread_create(&t1, nullptr, buyTickets, (void *)"thread 1");
    pthread_create(&t2, nullptr, buyTickets, (void *)"thread 2");
    pthread_create(&t3, nullptr, buyTickets, (void *)"thread 3");
    pthread_create(&t4, nullptr, buyTickets, (void *)"thread 4");

    // 回收线程资源
    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    pthread_mutex_destroy(&mutex); // 销毁锁
    cout<<"抢票完成"<<endl;
    return 0;
}

线程互斥与同步--Linux_第6张图片

加完锁之后,多个线程之间明显没有冲突了。有兴趣的老铁可以试一下代码,明显加完锁的代码运行时间更长。

死锁问题

在使用锁的过程中,很可能会因为使用不当造成死锁现象,导致整个进程都被阻塞。

死锁四个必要条件

互斥条件:一个资源每次只能被一个执行流使用

请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放

不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺

循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

上面前三种条件是使用锁时的默认条件,最后一个条件一般是程序员的代码造成的。

要想避免死锁,就得规范使用锁,首先要加锁顺序一致,避免未释放锁,资源一次性分配等等。

下面我们模拟一个由于加锁顺序不一致而造成的死锁的场景:

#include 
#include 
#include 
#include 
using namespace std;

pthread_mutex_t mutexA,mutexB;//两个互斥锁
void* startRoutineA(void* arg)
{
    pthread_mutex_lock(&mutexA);
    sleep(1);
    pthread_mutex_lock(&mutexB);
    while(true)
    {
        cout<<pthread_self()<<" is running……"<<endl;
        sleep(1);
    }
    pthread_mutex_unlock(&mutexA);
    pthread_mutex_unlock(&mutexB);
}
void* startRoutineB(void* arg)
{
    pthread_mutex_lock(&mutexB);
    sleep(1);
    pthread_mutex_lock(&mutexA);
    while(true)
    {
        cout<<pthread_self()<<" is running……"<<endl;
        sleep(1);

    }
    pthread_mutex_unlock(&mutexB);
    pthread_mutex_unlock(&mutexA);
}
int main()
{
    pthread_mutex_init(&mutexA, nullptr); 
    pthread_mutex_init(&mutexB, nullptr);
    pthread_t t1, t2;            
    pthread_create(&t1, nullptr, startRoutineA, nullptr);
    pthread_create(&t2, nullptr, startRoutineB, nullptr);

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_mutex_destroy(&mutexA);
    pthread_mutex_destroy(&mutexB);
    return 0;
}

运行的结果就是线程都阻塞住,主要原因就是两个线程形成一种头尾相接的循环等待资源的关系,都各自拿了一个锁,去申请对方手中的锁。

线程同步的概念与意义

线程同步是在线程互斥的基础之上进行优化的产物,乍一看互斥与同步在概念上应该是冲突的,但实际上这里的同步不是指所有的线程同时访问临界区,而是有序的访问临界区。由于加锁与解锁的操作相比于唤醒一个线程的成本是要低很多的,因此很可能会出现一个线程释放锁之后,在其他线程醒来之前马上就抢到锁了,这会导致其他线程被闲置,浪费资源。并且,如果因某些资源暂时不能满足某些线程的运行条件,完全可以设定一些运行顺序使得该线程等待,避免饥饿问题。因此线程同步在有些场景下是的非常有必要的。

条件变量实现同步

条件变量的使用和锁有点类似,都是使用一个变量来控制所有依赖该变量的线程,进行挂起或唤醒某些线程的操作。

线程互斥与同步--Linux_第7张图片

pthread_cond_t:条件变量的数据类型

pthread_cond_init:

作用:初始化条件变量

参数:

cond:要初始化的条件变量

attr:条件变量的属性,默认传空指针

返回值:成功返回0,失败返回错误码

pthread_cond_destroy:

作用:销毁条件变量,一般与pthread_cond_init搭配使用

参数:

cond:要销毁的条件变量

返回值:成功返回0,失败返回错误码

pthread_cond_t cond=PTHREAD_COND_INITIALIZER 语句可以直接设置一个可以使用的条件变量,且不需要进行手动销毁。

pthread_cond_wait:

作用:使一个依赖某个条件变量的线程挂起等待

参数:

cond:线程所依赖的条件变量

mutex:互斥锁。一般线程在使用条件变量时,可能在访问临界区,因此可能会在加锁的情况下使用的cond_wait,而等待会使得整个线程被挂起,但是不能拿着锁被挂起,否则会使得其他线程申请不到锁而被阻塞,因此在挂起该线程之前要释放锁,因此传入锁来实现解锁挂起,唤醒之后自动加锁的操作。之所以在一个函数中完成解锁和挂起等待的操作,是为了保证整个过程是原子的。一旦设计成两个独立的操作,就会导致在解锁与挂起等待之间线程被切走,一旦切走就可能会导致释放的锁再也拿不回来了,存在死锁的隐患。

返回值:成功返回0,失败返回错误码

pthread_cond_signal:

作用:给依赖某个条件变量的线程发信号,唤醒该线程(按序循环唤醒)

参数:

cond:线程所依赖的条件变量

返回值:成功返回0,失败返回错误码

pthread_cond_broadcast:

作用:使得所有依赖于某个条件变量的线程被唤醒

参数:

cond:线程所依赖的条件变量

返回值:成功返回0,失败返回错误码

生产者消费者模型–互斥与同步

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

线程互斥与同步--Linux_第8张图片

优点:解耦、支持并发、支持忙闲不均

基于阻塞队列的生产者消费者模型

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进行操作时会被阻塞)

线程互斥与同步--Linux_第9张图片

#include 
#include 
#include 
#include 
#include 
#include
using namespace std;
#define NUM 5

template <class T>
class BlockQueue
{
public:
    BlockQueue(const int cap = NUM) // 在创建阻塞队列时,完成对锁和条件变量的初始化
        : _cap(cap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_proCond, nullptr);
        pthread_cond_init(&_comCond, nullptr);
    }
    ~BlockQueue() // 退出时完成资源的释放
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_proCond);
        pthread_cond_destroy(&_comCond);
    }

public:
    bool isFull()
    {
        return _q.size() == _cap;
    }
    bool isEmpty()
    {
        return _q.size() == 0;
    }
    void lockQueue()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlockQueue()
    {
        pthread_mutex_unlock(&_mutex);
    }
    void productorWait()
    {
        pthread_cond_wait(&_proCond, &_mutex);
    }
    void consumerWait()
    {
        pthread_cond_wait(&_comCond, &_mutex);
    }
    void productorWake()
    {
        pthread_cond_signal(&_proCond);
    }
    void consumerWake()
    {
        pthread_cond_signal(&_comCond);
    }
    void put(const T &data)
    {
        lockQueue();
        while (isFull())
        {
            consumerWake();
            productorWait();
        }
        _q.push(data);
        consumerWake();
        unlockQueue();
    }
    T take()
    {
        lockQueue();
        while (isEmpty())
        {
            productorWake();
            consumerWait();
        }
        productorWake();
        T tmp = _q.front();
        _q.pop();
        unlockQueue();
        return tmp;
    }
private:
    queue<T> _q;             // 队列
    int _cap;                // 队列的大小
    pthread_mutex_t _mutex;  // 生产者与消费者共用一个互斥锁
    pthread_cond_t _proCond; // 生产者所使用的条件变量
    pthread_cond_t _comCond; // 消费者所使用的条件变量
};

static const string str="+-*/%";
class Task//任务类
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {}
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {}
    int operator() ()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                cout << "div zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }
        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                cout << "mod zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            cout << "非法操作: " << operator_ << endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }
private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};


void *productor(void *arg)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(arg);
    while(true)
    {
        int one=rand()%100;
        int two=rand()%100;
        char op=str[rand()%(str.size())];
        cout<<pthread_self()<< "于 "<<(unsigned int)time(nullptr)<<" 生产任务: "<<one<<op<<two<<"=?"<<endl;
        Task t(one,two,op);
        bq->put(t);
        sleep(1);
    }
    return nullptr;
}

void *consumer(void *arg)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(arg);
    while(true)
    {
        Task t=bq->take();
        int one,two;
        char op;
        t.get(&one,&two,&op);
        cout<<pthread_self()<<"于 "<<(unsigned int)time(nullptr)<<" 处理任务: "<<one<<op<<two<<"="<<t.run()<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t c1, c2, c3, p1, p2, p3;
    BlockQueue<Task> bq;
    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self()); // 随机生成数字
    pthread_create(&p1, nullptr, productor, (void *)&bq);
    pthread_create(&p2, nullptr, productor, (void *)&bq);
    pthread_create(&p3, nullptr, productor, (void *)&bq);
    pthread_create(&c1, nullptr, consumer, (void *)&bq);
    pthread_create(&c2, nullptr, consumer, (void *)&bq);
    pthread_create(&c3, nullptr, consumer, (void *)&bq);

    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);

    return 0;
}

线程互斥与同步--Linux_第10张图片

基于环形队列的生产者消费者模型

关于环形队列,这里我就不多赘述了,环形队列的优势在于,如果队列不为空或者不为满,那么不会对同一个数据进行写入数据和拿走数据的操作,这种情况下生产者与消费者不会出现互斥的现象,也就可以实现并发,即生产者和消费者同时工作。

POSIX信号量

环形队列的难点在于队列为空或者为满时的状态是一样的,不好区分。但是如果我们使用两个计数器来分别记录剩余空间和生成数据的个数,就很容易实现线程间的并发了。并且比较特殊的是,信号量的++和–操作都是原子性的,因此在使用的时候并不用担心会出现线程安全问题。

线程互斥与同步--Linux_第11张图片

sem_t:信号量的数据类型

sem_init:

作用:初始化信号量

参数:

sem:要初始化的信号量

pshared :0表示线程间共享,非零表示进程间共享

value:信号量初始值

返回值:成功返回0;失败返回-1,并设置errno

sem_destroy

作用:销毁信号量

参数:

sem:要销毁的信号量

返回值:成功返回0;失败返回-1,并设置errno

sem_wait:

作用:使得信号量在大于0时自减1(原子性),等于0时使得该线程被挂起,直到信号量大于0才会被唤醒。

参数:

sem:要–的信号量

返回值:成功返回0;失败返回-1,并设置errno

sem_post:

作用:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。(原子性)

参数:

sem:要++的信号量

返回值:成功返回0;失败返回-1,并设置errno

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define NUM 5

static const string str="+-*/%";
class Task//任务类
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {
    }
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {
    }
    int operator() ()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                cout << "div zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }
        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                cout << "mod zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            cout << "非法操作: " << operator_ << endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }
private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

template<class T>
class RingQueue
{
public:
    RingQueue(const int cap=NUM):ringqueue_(cap),pIndex_(0), cIndex_(0)
    {
        pthread_mutex_init(&pmutex_,nullptr);
        pthread_mutex_init(&cmutex_,nullptr);
        sem_init(&roomSem_,0,cap);
        sem_init(&dataSem_,0,0);
    }
    ~RingQueue()
    {
        pthread_mutex_destroy(&pmutex_);
        pthread_mutex_destroy(&cmutex_);
        sem_destroy(&roomSem_);
        sem_destroy(&dataSem_);
    }
    void put(const T& data)
    {
        sem_wait(&roomSem_);
        pthread_mutex_lock(&pmutex_);
        ringqueue_[pIndex_]=data;
        pIndex_++;
        pIndex_%=ringqueue_.size();
        pthread_mutex_unlock(&pmutex_);
        sem_post(&dataSem_);
    }
    T take()
    {
        sem_wait(&dataSem_);
        pthread_mutex_lock(&cmutex_);
        T tmp=ringqueue_[cIndex_];
        cIndex_++;
        cIndex_%=ringqueue_.size();
        pthread_mutex_unlock(&cmutex_);
        sem_post(&roomSem_);
        return tmp;
    }
private:
    vector<T> ringqueue_; // 数组模拟环形队列
    sem_t roomSem_;       // 衡量空间计数器,productor
    sem_t dataSem_;       // 衡量数据计数器,consumer
    uint32_t pIndex_;     // 当前生产者写入的位置, 如果是多线程,pIndex_也是临界资源
    uint32_t cIndex_;     // 当前消费者读取的位置,如果是多线程,cIndex_也是临界资源

    pthread_mutex_t pmutex_;// 生产者使用的互斥锁
    pthread_mutex_t cmutex_;// 消费者使用的互斥锁
};

void *productor(void *arg)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(arg);
    while(true)
    {
        int one=rand()%100;
        int two=rand()%100;
        char op=str[rand()%(str.size())];
        cout<<pthread_self()<< "于 "<<(unsigned int)time(nullptr)<<" 生产任务: "<<one<<op<<two<<"=?"<<endl;
        Task t(one,two,op);
        rq->put(t);
        sleep(1);
    }
    return nullptr;
}

void *consumer(void *arg)
{
    RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(arg);
    while(true)
    {
        Task t=rq->take();
        int one,two;
        char op;
        t.get(&one,&two,&op);
        cout<<pthread_self()<<"于 "<<(unsigned int)time(nullptr)<<" 处理任务: "<<one<<op<<two<<"="<<t.run()<<endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t c1, c2, c3, p1, p2, p3;
    RingQueue<Task> rq;
    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self()); // 随机生成数字
    pthread_create(&p1, nullptr, productor, (void *)&rq);
    pthread_create(&p2, nullptr, productor, (void *)&rq);
    pthread_create(&p3, nullptr, productor, (void *)&rq);
    pthread_create(&c1, nullptr, consumer, (void *)&rq);
    pthread_create(&c2, nullptr, consumer, (void *)&rq);
    pthread_create(&c3, nullptr, consumer, (void *)&rq);

    pthread_join(p1, nullptr);
    pthread_join(p2, nullptr);
    pthread_join(p3, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);

    return 0;
}

线程互斥与同步--Linux_第12张图片

线程池

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

线程池的应用场景:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。

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

  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

线程池示例:

  1. 创建固定数量线程池,循环从任务队列中获取任务对象,

  2. 获取到任务对象后,执行任务对象中的任务接口

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define NUM 5
static const string str = "+-*/%";
class Task // 任务类
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {
    }
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {
    }
    int operator()()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                cout << "div zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }
        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                cout << "mod zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            cout << "非法操作: " << operator_ << endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }

private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

template <class T>
class ThreadPool
{
public:
    ThreadPool(const int& threadNum=NUM):threadNum_(threadNum),isStart_(false)
    {
        assert(threadNum_>0);
        pthread_mutex_init(&mutex_,nullptr);
        pthread_cond_init(&cond_,nullptr);
    }
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    static void* threadpool(void* arg)
    {
        pthread_detach(pthread_self());
        ThreadPool<T>* tp=static_cast<ThreadPool<T>*>(arg);
        while(true)
        {
            tp->lockQueue();
            while(!tp->haveTask())
            {
                tp->waitForTask();
            }
            T tmp=tp->pop();
            tp->unlockQueue();


            int one,two;
            char op;
            tmp.get(&one,&two,&op);
            cout<<pthread_self()<<"于 "<<(unsigned int)time(nullptr)<<" 处理任务: "<<one<<op<<two<<"="<<tmp.run()<<endl;
        }
        return nullptr;
    }
    void start()
    {
        assert(!isStart_);
        for(int i=0;i<threadNum_;++i)
        {
            pthread_t tmp;
            pthread_create(&tmp,nullptr,threadpool,this);
        }
    }
    void put(const T &data)
    {
        lockQueue();
        taskQueue_.push(data);
        unlockQueue();
        choiceThreadForHandler();
    }
private:
    void lockQueue() { pthread_mutex_lock(&mutex_); }
    void unlockQueue() { pthread_mutex_unlock(&mutex_); }
    bool haveTask() { return !taskQueue_.empty(); }
    void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
    void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
    T pop()
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }

private:
    bool isStart_;
    int threadNum_;
    queue<T> taskQueue_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
};
int main()//主线程给线程池发布任务
{
    ThreadPool<Task> tp;
    tp.start();
    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self()); // 随机生成数字
    while(true)
    {
        int one,two;
        char op;
        one=rand()%100,two=rand()%100;
        op=str[rand()%str.size()];
        Task t(one,two,op);
        tp.put(t);
        cout<<pthread_self()<< "于 "<<(unsigned int)time(nullptr)<<" 生产任务: "<<one<<op<<two<<"=?"<<endl;
        sleep(1);
    }
    return 0;
}

线程互斥与同步--Linux_第13张图片

线程安全下的单例模式

当时在学习特殊类[传送门]的时候,有一种类是只会有一个实体,只能构造一个对象,具体的代码片段如下:

//懒汉模式
class Singleton
{
public:
	static Singleton* CallObj()
	{
		if (_slt == nullptr)   //为空的话再决定开辟空间
		{
			_slt = new Singleton;
		}
		return _slt;
	}
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	void Write()
	{
		cin >> _str;
	}
	void Read()
	{
		cout << _str << endl;
	}
private:
	Singleton()
	{}
	static Singleton* _slt;
	string _str;
};
Singleton* Singleton::_slt = nullptr;  //只初始化,并不开辟空间

在单线程之下这样跑是没问题的,但是多线程之下会在判断空指针那里出现问题:

线程互斥与同步--Linux_第14张图片

如此就会造成开辟两次(多次)空间来构造对象,这与单例预期相违背,因此必须得在开辟空间那里加锁。

接下来我们尝试线程池进行单例模式的实现:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
#define NUM 5
static const string str = "+-*/%";
class Task // 任务类
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {
    }
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {
    }
    int operator()()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                cout << "div zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }
        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                cout << "mod zero, abort" << endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            cout << "非法操作: " << operator_ << endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }

private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

template <class T>
class ThreadPool
{
private:
    ThreadPool(const int &threadNum = NUM) : threadNum_(threadNum), isStart_(false)
    {
        assert(threadNum_ > 0);
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&cond_, nullptr);
    }
    ThreadPool(const ThreadPool<T> &) = delete;     // 删除拷贝构造
    void operator=(const ThreadPool<T> &) = delete; // 删除赋值构造
public:
    ~ThreadPool()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&cond_);
    }
    static ThreadPool<T> *getInstance() // 申请类的对象
    {
        pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
        if (instance == nullptr)
        {
            pthread_mutex_lock(&mutex);
            if (instance == nullptr)
            {
                instance = new ThreadPool<T>();
            }
            pthread_mutex_unlock(&mutex);
        }
        return instance;
    }
    static void *threadpool(void *arg)
    {
        pthread_detach(pthread_self());
        ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(arg);
        while (true)
        {
            tp->lockQueue();
            while (!tp->haveTask())
            {
                tp->waitForTask();
            }
            T tmp = tp->pop();
            tp->unlockQueue();

            int one, two;
            char op;
            tmp.get(&one, &two, &op);
            cout << pthread_self() << "于 " << (unsigned int)time(nullptr) << " 处理任务: " << one << op << two << "=" << tmp.run() << endl;
        }
        return nullptr;
    }
    void start()
    {
        assert(!isStart_);
        for (int i = 0; i < threadNum_; ++i)
        {
            pthread_t tmp;
            pthread_create(&tmp, nullptr, threadpool, this);
        }
    }
    void put(const T &data)
    {
        lockQueue();
        taskQueue_.push(data);
        unlockQueue();
        choiceThreadForHandler();
    }

private:
    void lockQueue() { pthread_mutex_lock(&mutex_); }
    void unlockQueue() { pthread_mutex_unlock(&mutex_); }
    bool haveTask() { return !taskQueue_.empty(); }
    void waitForTask() { pthread_cond_wait(&cond_, &mutex_); }
    void choiceThreadForHandler() { pthread_cond_signal(&cond_); }
    T pop()
    {
        T temp = taskQueue_.front();
        taskQueue_.pop();
        return temp;
    }

private:
    bool isStart_;
    int threadNum_;
    queue<T> taskQueue_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    static ThreadPool<T> *instance;
};
template <class T>
ThreadPool<T> *ThreadPool<T>::instance = nullptr; // 懒汉模式

int main()
{
    ThreadPool<Task>* tp=ThreadPool<Task>::getInstance();//申请对象
    tp->start();
    srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self()); // 随机生成数字
    while (true)
    {
        int one, two;
        char op;
        one = rand() % 100, two = rand() % 100;
        op = str[rand() % str.size()];
        Task t(one, two, op);
        tp->put(t);
        cout << pthread_self() << "于 " << (unsigned int)time(nullptr) << " 生产任务: " << one << op << two << "=?" << endl;
        sleep(1);
    }
    free(tp);//释放空间
    return 0;
}

线程互斥与同步--Linux_第15张图片

总结

线程的互斥保证线程安全,线程同步则可以有效避免多线程的饥饿问题,这两者在生产者与消费者模型中体现的非常明显,线程的互斥与同步对线程的应用至关重要,是使用多线程的基本功。对于生产者消费者模型,个人认为理解不同角色之间的关系是最重要的,其中关于并发的理解也必须认识到位:生产任务和消费任务是可以同时进行的!

你可能感兴趣的:(Linux,开发语言,linux)