多线程-生产者消费者模型

生产者消费者模型

单例模式和其他锁

生产者消费者模型

生产者消费者模型是一种并发编程模型,用于解决多线程或多进程之间的协作和数据共享问题。在该模型中,有两种角色:生产者和消费者。生产者负责生成数据,并将其放入共享缓冲区中,而消费者则负责从共享缓冲区中获取数据进行处理。

生产者消费者模型,其实就是多个线程通过一个缓冲区通信,就像进程通过管道通信
所以这个缓冲区就是共享资源,就需要保护共享资源的安全,维护线程的同步与互斥

生产者消费者模型有三种关系,生产者和生产者:互斥,消费者和消费者:互斥,生产者和消费者:同步和互斥

下面是生产者消费者模型的详细解释:

  1. 共享缓冲区:
    共享缓冲区是生产者和消费者之间的中介,它用于存储生产者生成的数据。这个缓冲区可以是一个队列、一个缓冲池或者其他适合的数据结构。生产者将数据放入缓冲区的末尾,而消费者则从缓冲区的开头获取数据进行处理。

  2. 同步机制:
    为了确保生产者和消费者之间的正确协作,需要使用适当的同步机制来处理以下情况:

    • 当缓冲区已满时,生产者需要等待,直到有空闲空间可以放入新的数据。
    • 当缓冲区为空时,消费者需要等待,直到有数据可供处理。
      这些同步机制可以使用信号量、互斥锁、条件变量或其他线程/进程同步原语来实现。
  3. 生产者:
    生产者的主要任务是生成数据并将其放入共享缓冲区。它可以是一个线程或进程,根据具体的应用场景而定。生产者的基本步骤如下:

    • 检查共享缓冲区是否已满,如果是,则等待缓冲区可用。
    • 生成数据。
    • 将数据放入共享缓冲区的末尾。
    • 通知消费者有新的数据可用。
  4. 消费者:
    消费者的主要任务是从共享缓冲区中获取数据并进行处理。它可以是一个线程或进程,与生产者并发执行。消费者的基本步骤如下:

    • 检查共享缓冲区是否为空,如果是,则等待数据可用。
    • 从共享缓冲区的开头获取数据。
    • 处理数据。
    • 通知生产者有空闲空间可用。

阻塞队列实现生产者消费者模型

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

BlockQueue.hpp

#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;

template<class T>
class BlockQueue
{
public:
    BlockQueue(const int capacity  = 5)
    :_capacity(capacity)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_consumerCond, nullptr);
        pthread_cond_init(&_productorCond, nullptr);
    }

    bool isFull()
    {
        return _q.size() == _capacity;
    }

    bool isEmpty()
    {
        return _q.empty();
    }

    void push(const T& data)
    {
        pthread_mutex_lock(&_mutex);
        while(isFull())
        {   //满了就不要加数据了,去生产者自己的条件变量等待,并释放锁
            pthread_cond_wait(&_productorCond, &_mutex);
        }

        _q.push(data);
        // 生产者加入了数据,唤醒消费者
        pthread_cond_signal(&_consumerCond);
        pthread_mutex_unlock(&_mutex);
    }

    void pop(T* data)
    {
        pthread_mutex_lock(&_mutex);
        while(isEmpty())
        {
            pthread_cond_wait(&_consumerCond, &_mutex);
        }

        *data = _q.front();
        _q.pop();
        // 消费者带走了数据,唤醒生产者
        pthread_cond_signal(&_productorCond);
        pthread_mutex_unlock(&_mutex);
    }    

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_consumerCond);
        pthread_cond_destroy(&_productorCond);
    }


private:
    queue<T> _q;
    int _capacity;
    pthread_mutex_t _mutex;//只用一把锁是因为只有一个队列
    pthread_cond_t _consumerCond;//消费者对应的条件变量
    pthread_cond_t _productorCond;//生产者对应的条件变量

};

main.cpp

#include "BlockQueue.hpp"
#include <pthread.h>
#include <ctime>
#include <unistd.h>

void* consumer(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;

    while(true)
    {
        sleep(1);//消费者慢点消费
        int data = 0;
        //1.从BlockQueue获取数据
        bq->pop(&data);
        //2.处理数据
        cout << "消费者获取的数据: " << data << endl;
    }

}

void* productor(void* args)
{
    BlockQueue<int>* bq = (BlockQueue<int>*)args;

    while(true)
    {
        //1.先获取数据
        int data = rand() % 10 + 1;
        //2.将数据写入BlockQueue
        bq->push(data);

        cout << "生产者生产的数据: " << data << endl;
    }
}


int main()
{
    //单生成单消费,后面再改成多生产多消费
    srand((uint64_t)time(nullptr) * 31);

    BlockQueue<int>* bq = new BlockQueue<int>();

    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void*)bq);
    pthread_create(&p, nullptr, productor, (void*)bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete bq;
    return 0;
}

在这里插入图片描述
这段代码写的是一个生产者和一个消费者的模型,一开始会生产者生成满,然后再消费者去消费,然后生产者生成一个消费者消费一个

这份代码也支持多线程生成者消费者模型,只需要创建多线程就行

信号量

信号量(Semaphore)是一种用于线程或进程之间同步和互斥的机制。它可以用于控制对共享资源的访问,以防止并发访问导致的竞争条件。信号量可以用于实现互斥锁、条件变量、生产者-消费者问题等。

下面是对信号量的详细解释:

  1. 概念:
    信号量是一个计数器,用于控制进程或线程对共享资源的访问。它通常被用作一种同步机制,以确保在并发环境中只有一个线程或进程可以访问共享资源。

  2. 类型:

    • 二进制信号量(Binary Semaphore):也称为互斥锁(Mutex),只能取两个值,0和1。用于实现互斥访问共享资源的机制,一次只允许一个线程或进程访问资源。
    • 计数信号量(Counting Semaphore):可以取多个非负整数值。用于控制多个线程或进程对共享资源的访问数量。
  3. 注意事项:

    • 信号量是一种底层的同步原语,需要谨慎使用,以避免死锁、活锁等并发问题。
    • 在使用信号量时,需要正确地初始化、等待和释放信号量,避免资源泄漏和竞争条件。
    • 二进制信号量可以用于实现互斥锁的功能,通常与互斥锁搭配使用,以提供更高级别的同步和互斥机制。
    • 计数信号量可以用于控制对共享资源的访问数量,例如限制并发线程数、实现生产者-消费者模式等。
    • 在多线程或多进程环境中,信号量需要进行适当的同步和互斥操作,以确保正确的并发访问和资源管理。

信号量是一种用于同步和互斥的机制,通过等待和释放操作来控制对共享资源的访问。它是并发编程中重要的工具之一,用于解决线程或进程间的同步和互斥问题。

#include <semaphore.h>
int sem_init(sem_t* sem, int pshared, unsigned int value);
int sem_destroy(sem_t* sem);
int sem_wait(sem_t* sem);//P操作
int sem_post(sem_t* sem);//V操作

sem_init

  1. 参数:

    • sem:指向sem_t类型的指针,表示要初始化的信号量。
    • pshared:指定信号量的共享方式。如果为0,则信号量只能用于同一进程内的线程间同步。如果非0,则信号量可用于多个进程间的同步。在多线程环境中,通常将其设置为0。
    • value:指定信号量的初始值。对于二进制信号量,它应该为0或1。对于计数信号量,它决定了可以同时访问共享资源的线程或进程数量。
  2. 注意事项:

    • 在使用信号量之前,必须先初始化它。否则,可能会导致未定义的行为。
    • 如果信号量是用于进程间的共享,则需要将pshared参数设置为非零的值,并确保在进程间共享信号量的机制正确设置。
    • 信号量的初始值取决于具体应用场景。对于互斥锁,二进制信号量的初始值通常为1,表示资源可用;对于计数信号量,初始值通常表示资源的数量。
    • 在多线程环境中,应谨慎使用信号量,确保正确的同步和互斥操作,避免竞争条件和死锁等问题。

sem_wait

  1. 概念:
    sem_wait函数用于等待获取信号量。如果信号量的值大于0,表示资源可用,线程或进程可以继续执行,并将信号量的值减1。如果信号量的值为0,表示资源不可用,线程或进程将进入等待状态,直到其他线程或进程释放资源。

  2. 注意事项:

    • 在使用sem_wait函数之前,必须先初始化信号量,例如使用sem_init函数。
    • 等待操作会导致线程或进程进入等待状态,直到获取到信号量。在等待期间,线程或进程可能会被阻塞,直到其他线程或进程释放资源。
    • 在多线程或多进程环境中,使用信号量时需要进行适当的同步和互斥操作,以避免竞争条件和并发问题。
    • 如果需要指定等待操作的超时时间,可以使用sem_timedwait函数来实现。

sem_post

  1. 概念:
    sem_post函数用于释放信号量并离开临界区。线程或进程在完成对共享资源的访问后,通过调用sem_post函数增加信号量的值。如果有其他线程或进程正在等待该信号量,其中一个将被唤醒并继续执行。

  2. 注意事项:

    • 在使用sem_post函数之前,必须先初始化信号量,例如使用sem_init函数。
    • 释放操作会增加信号量的值,并唤醒正在等待该信号量的线程或进程。
    • 在多线程或多进程环境中,使用信号量时需要进行适当的同步和互斥操作,以避免竞争条件和并发问题。
    • 释放操作应在完成对共享资源的访问后进行,以确保其他线程或进程可以继续执行。
环形队列实现生产者消费者模型

我们用环形队列实现生产者消费者模型,这个例子来学习信号量的使用

环形队列实际上就是用数组模拟的。

生产者消费者模型在环形队列中,实际上可以看作生产者和消费者在一个圈里追逐,生产者和消费者只能往同一个方向追,不能回头
环形队列为空表示游戏开始或者消费者追到了生产者,两者都在同一位置,这个时候要生产者先行,让消费者去追生产者
环形队列满了,表示消费者追到了生产者,两者又在同一位置,这个时候要消费者先行,进行下一轮追逐

生产者生成数据之前,要对自己的信号量进行P(空间–)操作,申请空间,申请到空间并且放入了数据后,对消费者的信号量进程V(数据++)操作,申请不到空间就阻塞

消费者拿走数据之前,要对自己的信号量进行P(数据–)操作,看空间上是否有数据可以申请,拿走数据后,对生产者的信号量进程V(空间++)操作,申请不到数据就阻塞

RingQueue.hpp

#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>

using namespace std;

template<class T>
class RingQueue
{
public:
    RingQueue(int num = 5)
    : _ring(num), _capacity(num), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
    }

    void push(const T& data)
    {
        sem_wait(&_space_sem);//生产者P

        _ring[_p_step++] = data;
        _p_step %= _capacity; //维持环形队列

        sem_post(&_data_sem); //消费者V

    }

    void pop(T* data)
    {
        sem_wait(&_data_sem); //消费者P

        *data = _ring[_c_step++];
        _c_step %= _capacity;  //维持环形队列

        sem_post(&_space_sem);//生产者V
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
    }

private:
    vector<T> _ring;
    int _capacity;   //环形队列容量
    sem_t _data_sem; //消费者
    sem_t _space_sem;//生产者
    int _c_step;     //消费者所在位置
    int _p_step;     //生产者所在位置

};

main.cpp

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <memory>

void* consumer(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        int data = 0;
        rq->pop(&data);
        cout << "拿到的数据" << data << endl;
        sleep(1);
    }

}


void* productor(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        int data = rand() % 10 + 1;
        rq->push(data);
        cout << "生产数据: " << data << endl;
    }
    
}

int main()
{
    srand(time(nullptr) * 31);
    RingQueue<int>* rq = new RingQueue<int>();
    //单生产单消费
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, (void*)rq);
    pthread_create(&p, nullptr, productor, (void*)rq);


    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    delete rq;
    return 0;
}

在这里插入图片描述
这是环形队列的单生成单消费

多生成多消费
RingQueue.hpp

#pragma once

#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

using namespace std;

template<class T>
class RingQueue
{
public:
    RingQueue(int num = 5)
    : _ring(num), _capacity(num), _c_step(0), _p_step(0)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);

        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }

    void push(const T& data)
    {   //先申请资源再申请锁
        sem_wait(&_space_sem);//生产者P
        pthread_mutex_lock(&_p_mutex);

        _ring[_p_step++] = data;
        _p_step %= _capacity; //维持环形队列

        pthread_mutex_unlock(&_p_mutex);
        sem_post(&_data_sem); //消费者V

    }

    void pop(T* data)
    {   //先申请资源再申请锁
        sem_wait(&_data_sem); //消费者P
        pthread_mutex_lock(&_c_mutex);

        *data = _ring[_c_step++];
        _c_step %= _capacity;  //维持环形队列

        pthread_mutex_unlock(&_c_mutex);
        sem_post(&_space_sem);//生产者V
    }

    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);

        pthread_mutex_destroy(&_c_mutex);
        pthread_mutex_destroy(&_p_mutex);
    }

private:
    vector<T> _ring;
    int _capacity;   //环形队列容量
    sem_t _data_sem; //消费者
    sem_t _space_sem;//生产者
    int _c_step;     //消费者所在位置
    int _p_step;     //生产者所在位置

    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

main.cc

#include "RingQueue.hpp"
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <memory>

void* consumer(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        int data = 0;
        rq->pop(&data);
        cout << "拿到的数据" << data << endl;
        sleep(1);
    }

}


void* productor(void* args)
{
    RingQueue<int>* rq = (RingQueue<int>*)args;

    while(true)
    {
        int data = rand() % 10 + 1;
        rq->push(data);
        cout << "生产数据: " << data << endl;
    }
    
}

int main()
{
    srand(time(nullptr) * 31);
    RingQueue<int>* rq = new RingQueue<int>();
    //多生产多消费
    pthread_t c[3], p[3];
    for(int i = 0; i < 3; ++i)
        pthread_create(c + i, nullptr, consumer, (void*)rq);
    for(int i = 0; i < 3; ++i)
        pthread_create(p + i, nullptr, productor, (void*)rq);

    for(int i = 0; i < 3; ++i)
        pthread_join(c[i], nullptr);
    for(int i = 0; i < 3; ++i)
        pthread_join(p[i], nullptr);

    delete rq;
    return 0;
}

实际上就是创建了多个消费者和生产者,并且给消费者和生产者在push和pop上各加了一把锁

线程池

线程池(Thread Pool)是一种用于管理和复用线程的技术,它维护了一组预先创建的线程,并在需要时分配线程来执行任务。线程池的主要目的是提高多线程应用程序的性能和资源利用率,避免频繁地创建和销毁线程带来的开销

线程池也是一种生产者消费者模型

线程池的优势:

  • 降低线程创建和销毁的开销:线程的创建和销毁需要消耗系统资源,线程池通过复用线程,避免了频繁创建和销毁线程的开销,提高了系统性能和响应速度。

  • 提高系统资源利用率:线程池可以限制同时执行的线程数量,避免过多的线程竞争系统资源,从而提高了系统资源的利用率。

  • 提供任务调度和管理:线程池可以根据任务的数量和优先级来调度执行,管理任务的执行顺序和并发度,从而提供更好的任务调度和管理机制。

  • 控制并发度和资源消耗:通过设置线程池的大小和任务队列的容量,可以控制并发执行的线程数量,从而控制系统的负载和资源消耗。

ThreadPool.hpp:

#pragma once

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

using namespace std;

template<class T>
class ThreadPool
{
public:
    ThreadPool(const int num = 5)
    :_num(num), _threads(num)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
    }

    bool isEmpty()
    {
        return _tasks.empty();
    }

    static void* thread(void* args)
    { //加static去掉this指针,否则参数多了会报错
      //args是this指针,因为用了static,所以不传this指针过来看不到类内成员
        ThreadPool<T>* tp = (ThreadPool<T>*)args;
        pthread_detach(pthread_self());

        while(true)
        {
            pthread_mutex_lock(&tp->_mutex);
            if(tp->isEmpty())
            {
                pthread_cond_wait(&tp->_cond, &tp->_mutex);
            }

            T t = tp->pop_task();
            pthread_mutex_unlock(&tp->_mutex);

            t.run();
        }
    }

    void init()
    {}

    void start()
    {
        for(int i = 0; i < _num; ++i)
        {
            pthread_create(&_threads[i], nullptr, thread, this);
        }
    }

    void push_task(const T& t)
    {
        pthread_mutex_lock(&_mutex);
        _tasks.push(t);
        pthread_cond_signal(&_cond);
        pthread_mutex_unlock(&_mutex);
    }

    T pop_task()
    {
        T t = _tasks.front();
        _tasks.pop();

        return t;
    }

    ~ThreadPool()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
private:
    vector<pthread_t> _threads;
    int _num;

    queue<T> _tasks;

    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
};

Task.hpp

#pragma once
#include <iostream>
#include <ctime>
using namespace std;


class Task
{
public:
    Task()
    {}


    void run()
    {
        srand(time(nullptr));
        const char* str = "+-*/";
        
        while(true)
        {
            int sym = rand() % 4;
            int x = (rand() * 31) % 100 + 1;
            int y = (rand() ^ 31) % 100 + 1;

            switch(str[sym])
            {
                case '+':
                    cout << x << " + " << y << " = " << x + y << endl;
                    sleep(1);
                    break;
                case '-':
                    cout << x << " - " << y << " = " << x - y << endl;
                    sleep(1);
                    break;
                case '*':
                    cout << x << " * " << y << " = " << x * y << endl;
                    sleep(1);
                    break;
                case '/':
                    cout << x << " / " << y << " = " << x / y << endl;
                    sleep(1);
                    break;
                default:
                    break;
            }
        }
    }

    ~Task()
    {}

};

main.cpp

#include "ThreadPool.hpp"
#include "Task.hpp"
#include <memory>

int main()
{
    ThreadPool<Task>* tp = new ThreadPool<Task>();
    tp->init();
    tp->start();

    while(true)
    {
        Task t;
        sleep(1);

        tp->push_task(t);
    }

    return 0;
}

单例模式和其他锁

单例模式:

饿汉模式是单例模式的一种实现方式,它在类加载时就创建并初始化了唯一的实例,因此称为"饿汉",指在一开始就"饥不可耐"地创建实例。

懒汉模式的单例模式通常指的是在需要时才创建实例的单例模式。与饿汉模式不同,懒汉模式在第一次访问实例时才创建它。

单例模式(Singleton Pattern)是一种创建型设计模式,它确保一个类只有一个实例,并提供全局访问点。单例模式常用于需要全局共享访问的对象,例如日志记录器、数据库连接池等。

实现单例模式的关键点是:

  1. 私有构造函数(Private Constructor):将类的构造函数声明为私有,防止其他类直接实例化该类。

  2. 静态方法获取实例(Static Method for Instance Retrieval):通过一个静态方法来获取类的唯一实例。该方法在首次调用时创建实例,并在后续调用时直接返回该实例。

  3. 静态变量持有实例(Static Variable to Hold the Instance):使用一个静态变量来保存类的唯一实例。

单例模式的优点:

  • 全局唯一实例:确保一个类只有一个实例存在,方便对实例进行全局访问。

  • 节省资源:由于只有一个实例存在,可以节省系统资源,特别是对于需要频繁创建和销毁的对象。

  • 实现了懒加载:在需要时才会创建实例,延迟了对象的实例化,提高了性能。

单例模式的缺点:

  • 难以扩展:由于单例模式只允许存在一个实例,因此在某些情况下可能会限制了系统的扩展性。

  • 可能引入全局状态:由于单例实例是全局访问的,可能会引入全局状态,增加了系统的复杂性。

  • 难以进行单元测试:由于依赖单例实例的类无法直接创建实例,可能会导致单元测试的困难。

悲观锁(Pessimistic Locking)是一种并发控制机制,用于保护共享资源在并发环境下的访问。悲观锁的核心思想是,在访问共享资源之前,假设会发生冲突并采取必要的措施来防止冲突的发生。

  1. 假设冲突:悲观锁的基本思想是假设在任何时刻都会发生冲突,即认为其他线程会试图修改或访问正在被保护的资源。

  2. 加锁:在悲观锁的机制下,当一个线程希望访问共享资源时,它会先尝试获取锁。如果资源已经被其他线程锁定,当前线程会被阻塞,直到锁被释放。这样可以确保同时只有一个线程能够访问资源。

  3. 保护共享资源:一旦某个线程获得了锁,它就可以安全地访问和修改共享资源,执行需要保护的操作。在执行完操作后,线程会释放锁,允许其他线程继续访问。

  4. 锁的类型:悲观锁可以通过不同的机制实现,如互斥锁(Mutex)、读写锁(ReadWrite Lock)等。互斥锁是一种独占锁,它确保同一时刻只有一个线程可以获得锁。读写锁允许多个线程同时获得读取权限,但只允许一个线程获得写入权限。

  5. 锁的粒度:在使用悲观锁时,需要考虑锁的粒度。锁的粒度应根据具体的场景和性能需求来确定,过细的粒度可能导致过多的锁竞争,而过粗的粒度可能导致并发性能下降。

需要注意的是,悲观锁在并发环境下可能会引入性能开销,因为它假设会发生冲突并采取相应的阻塞措施。因此,悲观锁适用于对共享资源的并发访问较少的情况,或者在读操作远远多于写操作的场景中。

乐观锁(Optimistic Locking)是一种并发控制机制,与悲观锁相反,它假设在大多数情况下不会发生冲突,并允许多个线程同时访问共享资源。乐观锁的核心思想是,在修改共享资源之前,不会加锁,而是在提交修改时进行冲突检测。

  1. 假设不冲突:乐观锁的基本思想是假设在大多数情况下,对共享资源的并发访问不会发生冲突,即认为其他线程不会修改正在被保护的资源。

  2. 记录版本号:在乐观锁的机制下,每个共享资源会有一个相关的版本号(或时间戳)。版本号可以是一个整数,也可以是一个时间戳,用于标识资源的状态。

  3. 读取和修改操作:当一个线程希望读取共享资源时,它会先读取资源的当前版本号,并将其保存下来。当线程完成对资源的修改后,它会将修改后的资源和保存的版本号一起提交。

  4. 冲突检测:在提交修改时,乐观锁会检查当前资源的版本号是否与保存的版本号相同。如果版本号不匹配,表示在读取和修改过程中发生了冲突。这时,当前线程可以选择放弃修改、重试操作或采取其他处理方式。

  5. 解决冲突:当检测到冲突时,乐观锁通常采用一种自动或手动的解决方案。自动解决方案可能包括回滚操作、重试操作或合并操作,以保证资源的一致性。手动解决方案可能涉及用户干预,例如提示用户冲突发生并要求用户手动解决。

  6. 版本号更新:当一个线程成功提交修改时,乐观锁会更新共享资源的版本号,以反映最新的状态。这样,其他线程在下次访问时就会获取到最新的版本号。

需要注意的是,乐观锁适用于对共享资源的并发访问较多的情况,或者在读操作远远多于写操作的场景中。它避免了加锁的开销,但需要进行冲突检测,并处理冲突的情况。

乐观锁是一种并发控制机制,假设在大多数情况下不会发生冲突,并允许多个线程同时访问共享资源。它使用版本号或时间戳来标识资源的状态,并在提交修改时进行冲突检测。乐观锁适用于对共享资源的并发访问较多的情况,避免了加锁的开销,但需要处理冲突的情况。

CAS(Compare and Swap)是一种原子操作,用于实现乐观锁和并发控制。CAS操作是基于硬件的原子指令,可以在无锁的情况下进行原子性的读取、比较和写入操作。

  1. 比较和交换:CAS操作包含两个关键步骤:比较和交换。首先,它会比较内存中的值与预期值是否相等。如果相等,则执行交换操作,将新值写入内存;如果不相等,则表示其他线程已经修改了内存的值,CAS操作失败。

  2. 原子性:CAS操作是原子的,即在执行期间不会被其他线程中断。这是通过硬件级别的原子指令来实现的,确保整个比较和交换的过程是不可分割的。

  3. 乐观锁:CAS操作通常与乐观锁机制一起使用。在乐观锁中,线程在修改共享资源之前,先读取资源的当前值,并保存下来。然后使用CAS操作进行比较和交换,将新值写入内存。如果CAS操作成功,表示没有其他线程修改过资源,当前线程的修改成功;如果CAS操作失败,表示其他线程已经修改了资源,当前线程需要根据具体情况进行重试或处理。

  4. 自旋:在CAS操作失败时,可以使用自旋(Spin)来进行重试。自旋是一种忙等待的方式,线程会反复尝试执行CAS操作,直到操作成功或达到一定的重试次数。

  5. ABA问题:CAS操作可能存在ABA问题。ABA问题指的是在CAS操作期间,值经历了从A到B再到A的变化,导致CAS操作无法察觉到值的变化。为了解决ABA问题,可以使用版本号或标记位等机制来跟踪值的变化,确保在比较和交换时能够准确判断值是否发生了变化。

自旋锁,用于在多线程编程中保护共享资源的访问。它是一种忙等待的锁,当一个线程尝试获取锁时,如果锁已被其他线程占用,该线程会一直循环检查锁的状态,直到成功获取锁为止。

  1. 自旋锁的实现通常依赖于底层硬件的原子操作,比如原子读-修改-写指令。它的基本思想是使用一个标志位来表示锁的状态,当线程尝试获取锁时,会不断地检查该标志位,直到标志位为未锁定状态时,线程将标志位设置为锁定状态,表示成功获取到了锁。当线程释放锁时,会将标志位重新设置为未锁定状态,以允许其他线程获取锁。

  2. 自旋锁的优点是在资源竞争不激烈的情况下,效率很高。由于线程不需要进行上下文切换或者睡眠等操作,而是一直循环检查锁的状态,因此减少了线程切换带来的开销。此外,自旋锁适用于在多核处理器上并行执行的场景,因为它可以确保线程在同一核心上执行,避免了在不同核心之间切换带来的缓存一致性问题。

  3. 自旋锁也存在一些缺点。首先,如果资源竞争激烈,多个线程同时自旋等待锁的释放,会导致CPU资源的浪费。其次,自旋锁不适用于长时间占用锁的情况,因为自旋等待会消耗CPU时间,对于其他需要执行的任务来说,这是一种浪费。在这种情况下,应该考虑使用其他类型的锁,比如互斥锁或读写锁。

一、 课程设计目的 在多道程序环境下,进程同步问题十分重要,通过解决“生产者-消费者”问题,可以帮助我们更好的理解进程同步的概念及实现方法。掌握线程创建和终止的方法,加深对线程和进程概念的理解,会用同步与互斥方法实现线程之间的进行操作。 在学习操作系统课程的基础上,通过实践加深对进程同步的认识,同时,可以提高运用操作系统知识解决实际问题的能力;锻炼实际的编程能力、创新能力及团队组织、协作开发软件的能力;还能提高调查研究、查阅技术文献、资料以及编写软件设计文档的能力。 二、 课程设计内容 模拟仿真“生产者-消费者”问题的解决过程及方法。 三、 系统分析与设计 1、 系统分析 在OS中引入进程后,虽然提高了资源的利用率和系统的吞吐量,但由于进程的异步性,也会给系统造成混乱,尤其是在他们争用临界资源时。为了对多个相关进程在执行次序上进行协调,以使并发执行的诸程序之间能有效地共享资源和相互合作,使程序的执行具有可再现性,所以引入了进程同步的概念。信号量机制是一种卓有成效的进程同步工具。 在生产者---消费者问题中应注意(信号量名称以多个生产者和多个消费者中的为例):首先,在每个程序中用于互斥的wait(mutex)和signal(mutex)必须成对出现;其次,对资源信号量empty和full的wait和signal操作,同样需要成对地出现,但它们分别处于不同的程序中。生产者消费者进程共享一个大小固定的缓冲区。其中,一个或多个生产者生产数据,并将生产的数据存入缓冲区,并有一个或多个消费者缓冲区中取数据。 2、 系统设计: 系统的设计必须要体现进程之间的同步关系,所以本系统采用2个生产者、2个消费者 和20个缓冲区的框架体系设计。为了更能体现该系统进程之间的同步关系,系统的生产者消费者的速度应该可控,以更好更明显的表现出结果。 为了使本系统以更加简单、直观的形式把“消费者-生产者”问题表现出来,我选择了使 用可视化界面编程。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值