完善博文 共享内存一写多读无锁实现的代码逻辑部分

15 篇文章 0 订阅
13 篇文章 0 订阅

使用共享内存(内存映射)实现发布订阅模式

  • 多进程实现PubSub发布订阅模式,从而实现进程间的通信。
  • 通信方式可以是TCP/UDP,管道Pipe/消息队列,共享内存shared memory等等。其中TCP/UDP的方式是可以用作局域网以及跨平台的通信,Pipe/消息队列是进程间基于系统实现比较基础的通信,这两者有大量优秀的第三方库支持,如ZeroMQ,只要加入我们自定义数据的转换方式即可方便实现;而共享内存是实现进程间通信最快的方式,但因为共享内存的设计并不是用来做类似PubSub这种模式的实现的,并且共享内存实质上就是一段进程间共享的内存空间,使用自由度是极高的,所以也很少有第三方库来实现共享内存方式的进程间通信。
  • 因此本文的重点是如何使用共享内存shared memory来实现高效的PubSub发布订阅模式。

需求

  • 消息通过事先分配好的共享内存空间来传递
  • 需要有一定的机制来管理消息的发送(写)和接收(读)
  • 需要实现发布订阅模式,也就是一个发布者(一写)多个订阅者(多读)
  • 考虑到平台的原因,最后采用了文件映射内存的这种方式,在各种系统中都有比较通用的实现

逻辑分析

  • 显然,只要创建了一个文件并且设置好需要的大小,即可以使用mmap映射到进程的内存空间,并且在退出时可以用munmap将映射释放掉。但是空间真正的释放是要把文件删掉的,因此我们需要一个计数器来记录使用这块共享内存的进程数,类似共享指针shared_ptr的实现,在计数为零时把文件删掉。在修改这个计数的时候还需要一把进程间读写锁:
  • 对于只有单个订阅者,数据之后包含一个标志位,发布者写完后置为true,订阅者读完之后置为false,可能再加上一个信号灯的控制,来避免频繁读写;
  • 对于多个订阅者,数据中的这个标志位变成一个计数,发布者写完之后将计数器置为订阅者的数量,订阅者读完之后将计数器减1,再加上一个进程条件变量的控制,来避免频繁读写。
  • 这两种方案都有一定的弊端,最大的问题在于,订阅者还需要修改共享内存的内容,这样就发挥不出读写锁支持多读的优势了。我们需要一个更好的机制。
  • 一个简单的实现是数据中带有一个单调递增的标签,订阅者读到数据后本地保存一下这个标签的值,如果下次读到的这个值不比保存的值大,就认为读到了旧数据,忽略之。这个标签比较好的实现是用当前的系统时间而不是计数,因为发布者可能会重启清零,就算重启后可以从已经写入的数据中读取,但后面为了实现无锁队列会让这个事情变得麻烦。这样还有一个问题是,依然会频繁地去读取这个标签。因此需要加入进程条件变量的控制来减少这种频繁。接下来是2,实现消息发送(写)和接收(读)的管理。因为我们已经有了一把读写锁,很自然地想到可以用它来管理读写啊。事实上并不是这样,因为发布者写完数据之后可能会有一段时间不会占有写锁,这时候就要一种机制来限制订阅者不会重复来读这个数据。对于这个实现,已有的方案有:
  • 对于每一个订阅者都开辟一块共享内存,可以按一对一的方式同时复制多份数据;
  • 使用生产消费模式,使用循环队列来实现读写分离。
  • 第1种方案是解决了读写锁争抢的问题,但是增加了内存复制的开销,反而没有第2种方案好。但是我们要稍微修改一下传统的生产消费模式的实现,只用一个指针来指向最新的数据。之所以这样做是因为内存是事先分配好的,我们把它改造成环形的内存缓冲区,很难保证数据读取的序列性;再者就是循环的尾指针应该由订阅者自己来维护,因为每个订阅者处理的速度是不一样的。
  • 如此一来,所有数据的修改完全是由发布者来做的,也就是说对于订阅者来说,这是个无锁队列:

代码实现

#include <iostream>
#include <cstring>
#include <vector>
#include <functional>
#include <memory>
#include <sys/mman.h>
#include <atomic>
#include <thread>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <fcntl.h>

struct ShmData{
    bool written_;
    long timestamp_;
    size_t size_;
    char data_[1];

    ShmData():written_(false){}

    void Write(const char *data,const size_t len){
        written_ = false;
        memcpy(data_,data,len);
        size_ = len;
        timestamp_ = GetTimestamp();
        written_ = true;
    }

    bool Read(std::vector<char>* data,long* time = nullptr){
        if (!written_){
            return false;
        }
        if (time){
            *time = timestamp_;
        }
        data->resize(size_);
        memcpy(data->data(),data_,size_);
        return true;
    }

    static long GetTimestamp(){
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME,&ts);
        return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
    }
};

struct ShmQueue{
    size_t size_;
    int count_;
    int head_;
    char data_[1];

    ShmQueue(const size_t size,const int count)
    :size_(sizeof(ShmData) + size),count_(count),head_(0){
        new(data_)ShmData;
    }
    void Write(const char* data,const size_t len){
        const int next = (head_ + 1) % count_;
        (reinterpret_cast<ShmData *>(data_ + next * size_))->Write(data,len);
        head_ = next;
    }
    bool Read(std::vector<char>*data,long* time){
        return (reinterpret_cast<ShmData *>(data_ + head_ * size_))->Read(data,time);
    }
};

struct ShmSlice{
    int attached_;
    pthread_rwlock_t rwlock_;
    pthread_mutex_t mutex_;
    pthread_cond_t cond_;
    char data_[1];

    ShmSlice(const size_t size,const int count,const bool init = false){
        if (init){
            //init rwlock
            pthread_rwlockattr_t rwattr;
            pthread_rwlockattr_init(&rwattr);
            pthread_rwlockattr_setpshared(&rwattr,PTHREAD_PROCESS_SHARED);
            pthread_rwlock_init(&rwlock_,&rwattr);
            //init mutex
            pthread_mutexattr_t mattr;
            pthread_mutexattr_init(&mattr);
            pthread_mutexattr_setpshared(&mattr,PTHREAD_PROCESS_SHARED);
            pthread_mutex_init(&mutex_,&mattr);
            //init condition variable
            pthread_condattr_t cattr;
            pthread_condattr_init(&cattr);
            pthread_condattr_setpshared(&cattr,PTHREAD_PROCESS_SHARED);
            pthread_cond_init(&cond_,&cattr);
            //init shm queue
            new(data_)ShmQueue(size,count);
        }
        LockWrite();
        if (init){
            attached_ = 1;
        } else{
            ++attached_;
        }
        UnLockWrite();
    }
    ~ShmSlice(){
        LockWrite();
        UnLockWrite();
        if (0 == attached_){
            pthread_cond_destroy(&cond_);
            pthread_mutex_destroy(&mutex_);
            pthread_rwlock_destroy(&rwlock_);
        }
    }
    int count(){
        LockRead();
        const int count = attached_;
        UnlockRead();
        return count;
    }
    void Write(const char* data,const size_t len){
        LockWrite();
        (reinterpret_cast<ShmQueue*>(data_))->Write(data,len);
        UnLockWrite();
    }
    bool Read(std::vector<char> *data,long* time){
        return (reinterpret_cast<ShmQueue *>(data_))->Read(data,time);
    }
    void LockWrite(){
        pthread_rwlock_wrlock(&rwlock_);
    }
    void UnLockWrite(){
        pthread_rwlock_unlock(&rwlock_);
    }
    void LockRead(){
        pthread_rwlock_rdlock(&rwlock_);
    }
    void UnlockRead(){
        pthread_rwlock_unlock(&rwlock_);
    }
    void LockMutex(){
        while (EOWNERDEAD == pthread_mutex_lock(&mutex_)){
            UnlockMutex();
        }
    }
    void UnlockMutex(){
        pthread_mutex_unlock(&mutex_);
    }
    void NotifyOne(){pthread_cond_signal(&cond_);}
    void NotifyAll(){pthread_cond_broadcast(&cond_);}
    void wait(){
        LockMutex();
        pthread_cond_wait(&cond_,&mutex_);
        UnlockMutex();
    }
    bool WaitFor(struct timespec *ts,const std::function<bool()>&cond){
        if (cond && cond()){
            return true;
        }
        LockMutex();
        pthread_cond_timedwait(&cond_,&mutex_,ts);
        UnlockMutex();
        bool ret;
        if (cond){
            ret = cond();
        } else{
            struct timespec now;
            clock_gettime(CLOCK_REALTIME,&now);
            ret = now.tv_sec < ts->tv_sec || (now.tv_sec == ts->tv_sec && now.tv_nsec <= ts->tv_nsec);
        }
        return ret;
    }
};

class ShmManger{
public:
    ShmManger(std::string file_name,const int size)
    : name_(std::move(file_name)),
      size_(sizeof(ShmSlice) + sizeof(ShmQueue) + 3 * (sizeof(ShmData) + size)){
        bool init = false;
        //open file descriptor
        int fd = open(name_.c_str(),O_RDWR | O_CREAT | O_EXCL,0600);
        if(fd < 0){
            fd = open(name_.c_str(),O_RDWR,0600);
        }else{
            //set file size
            struct stat fs;
            fstat(fd,&fs);
            if (fs.st_size < 1){
                ftruncate(fd,size_);
            }
            init = true;
        }
        //mmap
        void *shmaddr = mmap(NULL,size_,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
        new (shmaddr) ShmSlice(size,3,init);
        auto deleter = [](ShmSlice *ptr){ptr->~ShmSlice();};
        slice_ = std::shared_ptr<ShmSlice>(reinterpret_cast<ShmSlice *>(shmaddr),deleter);
        close(fd);
    }
    ~ShmManger(){
        running_ = false;
        slice_->NotifyAll();
        if (read_thread_.joinable()){read_thread_.join();}
        const int count = slice_->count();
        auto ptr = slice_.get();
        slice_.reset();
        if(count > 1){
            //unmap
            munmap(ptr,size_);
        } else{
            //remove file
            remove(name_.c_str());
        }
    }
    void Publish(const std::vector<char> &data){
        slice_->Write(data.data(),data.size());
        slice_->NotifyAll();
    }
    void Subscribe(std::function<void (const std::vector<char>&)>callback){
        callback = std::move(callback);
        running_ = true;
        read_thread_ = std::thread(&ShmManger::ReadThread,this);
    }

private:
    void ReadThread(){
        long read_time = 0;
        while (running_){
            std::vector<char> data;
            long time;
            struct timespec ts;
            clock_gettime(CLOCK_REALTIME,&ts);
            ts.tv_sec += 5;
            if (!slice_->WaitFor(&ts,[&]{
                return slice_->Read(&data,&time) && time > read_time;
            })){
                continue;
            }
            read_time = time;
            //deal with data
            callback_(data);
        }
    }

    std::string name_;
    int size_;
    std::shared_ptr<ShmSlice>slice_;
    std::function<void(const std::vector<char>&)>callback_;
    std::atomic_bool running_;
    std::thread read_thread_;
};
int main() {
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

参考链接

### 回答1: 当然可以!以下是一个简单的Java多线程博文: Java多线程指的是在Java程序中创建并同时运行多个线程的过程。它可以提高程序的性能和响应性,因为它允许程序在执行某些操作时同时执行其他操作。 Java提供了两种创建线程的方式:继承Thread类和实现Runnable接口。在本文中,我们将使用实现Runnable接口的方式来创建线程。 要创建一个新的线程,我们需要首先创建一个实现了Runnable接口的类。然后,我们需要在这个类中实现run()方法,这个方法将在线程开始时被调用。 接下来,我们需要创建一个Thread对象,并将实现了Runnable接口的类的实例作为参数传递给Thread对象的构造函数。最后,我们可以调用Thread对象的start()方法来启动新线程。 以下是一个简单的Java多线程的例子: ```java public class MyRunnable implements Runnable { public void run() { System.out.println("MyRunnable running"); } public static void main(String[] args) { Thread thread = new Thread(new MyRunnable()); thread.start(); System.out.println("Main thread running"); } } ``` 在这个例子中,我们创建了一个实现了Runnable接口的类MyRunnable,并在其中实现了run()方法。然后,我们创建了一个Thread对象,并将MyRunnable类的实例传递给Thread对象的构造函数。最后,我们调用Thread对象的start()方法来启动新线程。在main()方法中,我们还打印了一条消息来表示主线程正在运行。 当你运行这个程序时,你会看到类似于以下的输出: ``` Main thread running MyRunnable running ``` 这表明主线程和新线程都在同时运行,因为我们使用了Java多线程。 这只是Java多线程的基础,Java还提供了许多其他的多线程特性,例如线程同步、线程池等等。希望这篇博文能够为你提供一个好的起点来学习Java多线程! ### 回答2: 当下,计算机领域近年来最具热度和应用广泛的概念之一便是多线程编程。多线程编程是指在一个程序中同时执行多个线程,每个线程都是独立运行的子任务,可以减少程序执行时间和提高资源利用率。而在Java中,多线程编程则能够通过Thread类和Runnable接口来实现,使用起来也相对简单。 Java多线程编程以Thread类为基础,通过继承Thread类并重写run()方法来实现多线程。在run()方法内,我们编写具体的并发执行逻辑代码。通过调用Thread类的start()方法,系统会自动创建一个新线程,并调用线程的run()方法。当线程对象调用start()方法后,线程的生命周期开始,并引发线程调度器将其放入可运行线程池中。 除了通过继承Thread类实现多线程外,Java还提供了一种更灵活的方式,即实现Runnable接口。这种方式更推荐,因为Java只支持单继承,而实现接口可以解决这个问题。实现Runnable接口后,需要创建一个Thread对象,将实现Runnable接口的类实例作为参数传入Thread的构造方法。然后,通过调用Thread的start()方法,即可开启一个新线程,并执行实现Runnable接口的类中的run()方法。 在进行多线程编程时,需要注意线程的同步和互斥问题。多个线程访问共享数据时,可能会出现竞态条件,导致结果出错。为了解决这个问题,Java提供了synchronized关键字来实现线程的同步。通过在关键代码块或方法前加上synchronized关键字,可以使得同一时间只有一个线程能够进入该代码区域,从而避免出现数据竞争的问题。 总结来说,Java的多线程编程为程序员提供了一种并发执行的能力,可以提高程序的性能和效率。但需要注意线程同步和互斥的问题,以保证线程之间的协同工作和正确性。掌握好多线程编程的相关知识和技巧,将能让开发者编写出更加高效和稳定的Java程序。 ### 回答3: Java 多线程是指在一个程序中同时运行多个线程,每个线程执行自己的任务。多线程可以提高程序的并发性和效率,使得程序能够更好地利用计算机的资源。 在Java中,可以通过继承Thread类或实现Runnable接口来创建线程。使用继承Thread类的方法,需要重写run方法,并在run方法中编写线程的逻辑。使用实现Runnable接口的方法,则需要实现run方法,并将其作为参数传递给Thread类的构造方法。 多线程的优势主要体现在以下几个方面: 1. 提高程序的响应性:当一个线程被阻塞时,其他线程可以继续工作,使得程序在处理耗时操作时能够保持响应。 2. 提高系统资源的利用率:多线程可以有效利用计算机的多核处理器,提高程序的执行效率和性能。 3. 实现复杂的并发控制:通过线程的同步与互斥,可以实现多个线程之间的协同工作,例如生产者-消费者模型、读者-写者模型等。 在编写多线程程序时,需要注意以下几点: 1. 线程安全性:多线程操作共享的数据时,需考虑线程安全,避免出现数据竞争、不一致等问题。可以使用synchronized关键字实现线程的同步和互斥。 2. 死锁问题:当多个线程相互等待对方释放资源时,可能会导致死锁。为了避免死锁,需要合理设计线程的资源申请和释放顺序。 3. 线程间的通信:多个线程之间的通信可以使用wait、notify和notifyAll等方法来实现。 总之,Java多线程是一种能够充分利用计算机多核处理器的编程模型,能够提高程序的并发性和系统资源的利用率。在多线程编程时,我们需要注意线程的安全性、死锁问题和线程间的通信,以保证程序的正确性和性能。同时,也要根据具体问题的需求,选择合适的多线程编程方式,以达到最佳的效果。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值