Linux:多线程(单例模式,其他常见的锁,读者写者问题)

发布于:2025-03-10 ⋅ 阅读:(17) ⋅ 点赞:(0)

目录

单例模式

什么是设计模式

单例模式介绍

饿汉实现方式和懒汉实现方式

其他常见的各种锁

自旋锁

读者写者问题

逻辑过程

接口介绍


单例模式

什么是设计模式

设计模式就是一些大佬在编写代码的过程中,针对一些经典常见场景,给定对应解决方案,于是将其设计成一种模式,以后我们想使用就只需要套这个模式就好了。

单例模式介绍

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

在很多服务器开发场景中,经常需要让服务器加载很多数据到内存中,此时往往需要单例的类来管理这些数据。

饿汉实现方式和懒汉实现方式

static修饰的对象当类被加载到内存就被定义出来了,而不是等到类实例化对象后才定义。static修饰的成员在类中只有一份 饿汉和懒汉式通过static修饰的成员属于整个类,不管类实例化多少对象,都只有一个静态成员。

饿汉实现方式

饿汉实现方式就是:当类加载到到内存,就将成员变量定义出来了。

template<class T>
class Singleton{
    static T data;//类加载到内存直接定义
public:
    //获得data
    static T* GetInstance(){
        return &data;
    }
}

懒汉实现方式

懒汉实现方式就是,类加载时并没有将对象定义出来,而是在需要时才定义。

template<class T>
class Singleton(){
    //类加载时只是定义一个指针,
    static T* inst;
public:
    //当需要inst时,才会定义
    static T* GetInstance(){//不需要this指针,inst属于整个类。是静态成员
        if(inst == nullptr){
            inst = new T();
        }
        return inst;
    }
}

 饿汉实现方式会比懒汉实现方式启动得慢,因为饿汉实现方式在启动时就将需要的成员全部定义出来了。懒汉将定义时间挪后了。

 饿汉模式在使用时不会出现线程安全问题,但是在懒汉模式在使用时可能会出现线程安全问题。

 在多线程中,当两个线程同时调用GetInstance时,可能会创建出两个inst成员。 

懒汉模式线程安全版本

template<class T>
class Singleton(){
    //类加载时只是定义一个指针,
    volatile static T* inst;
    static Std::mutex lock;
public:
    //当需要inst时,才会定义
    static T* GetInstance(){//不需要this指针,inst属于整个类。是静态成员
        if(inst == nullptr){//双重判定,降低锁的冲突概率,提高性能
            lock.lock();    //加锁,保证只由一个线程可以进入
            if(inst == nullptr){
                inst = new T();
            }
            lock_unlock();
        } 
        return inst;
    }
}

注意事项:

  • 加锁位置,inst是临界资源,只需要对其加锁。

  • 两重判定。不一定每一个线程就来inst都为空,不至于每一个线程都加锁等待。

  • volatile,防止编译器优化。

其他常见的各种锁

悲观锁和乐观锁是两种并发控制的策略,而自旋锁、公平锁和非公平锁则属于具体实现并发控制的方式

1. 悲观锁(Pessimistic Locking)

  • 在每次对共享资源进行操作时都持有锁,认为其他线程会修改数据,因此在操作之前先加锁。

  • 主要用于保证并发环境下数据的一致性和可靠性。

  • 常见的悲观锁实现包括:互斥锁、读写锁等。

2. 乐观锁(Optimistic Locking)

  • 在操作共享资源时假设并发冲突的概率不高,因此不立即加锁,而是在更新时检查是否有其他线程修改过数据。

  • 乐观锁通常会使用版本号机制或CAS操作(Compare and Swap)来确保数据的一致性。\

  • CAS是一种乐观锁的实现方式,在更新数据时,会比较当前内存值和之前读取的值是否相等,如果相等说明数据未被修改,就可以进行更新操作,否则会失败。

  • CAS是一种原子操作,通常是一个自旋过程,即不断重试直到CAS成功或者达到重试次数。

  • 乐观锁避免了频繁加锁解锁的开销,适合读多写少的场景。

3. 自旋锁(Spin Lock)

  • 自旋锁是一种基于忙等待的锁,当线程尝试获取锁时如果锁已经被其他线程占用了,该线程会处于忙等待状态,直到锁被释放。

  • 自旋锁适用于短暂持有锁的情况,长时间持有锁会造成CPU资源的浪费。

4. 公平锁与非公平锁

  • 公平锁指的是对锁的获取按照请求的顺序进行,保证每个线程都有机会获取锁,即先到先得。

  • 非公平锁则允许锁的获取不按照请求顺序,有可能后到的线程会在先前请求而未获得锁的线程之前获取锁。

  • 非公平锁可以提高整体吞吐量,但可能导致优先级反转等问题。

自旋锁

自旋锁是一种基于忙等待的锁,当一个线程尝试获取自旋锁时,如果锁已经被其他线程占用,该线程会进行自旋操作,即不断检查锁的状态是否被释放,而不是立即被挂起等待。这种方式可以减少线程上下文切换的性能开销,适用于临界区内操作时间短暂的情况。

如何衡量临界区内操作时间:

  • 统计分析:通过在临界区内添加时间戳或者计时器,可以统计每个线程在临界区内的实际操作时间。这样可以得出平均操作时间、最大操作时间等数据。

  • 经验估计:根据对应用程序的了解和经验,估计临界区内操作的典型执行时间。这种方法可能不够精确,但可以作为初步评估。

  • 实际观察:观察程序的实际运行情况,包括临界区内操作的执行时间和频率。根据观察结果来评估操作的时间。

还是看我们的经验来选择合适,恰当的锁

1. 初始化自旋锁

int pthread_spin_init(pthread_spinlock_t* lock, int shared);
  • 功能:初始化互斥锁

  • 参数pthread_spinlock_t* lock表示需要被初始化的自旋锁的地址。int shared表示锁的是否进程间共享,0表示共享,非0表示不共享,一般都设为0。

  • 返回值:取消成功返回0,取消失败返回错误码。

2. 销毁自旋锁

int pthread_spin_destroy(pthread_spinlock_t* lock);
  • 功能:销毁互斥锁。

  • 参数pthread_spinlock_t* lock表示需要被销毁的自旋锁的地址。

  • 返回值:销毁成功返回0,失败返回-1。

3. 添加自旋锁

int pthread_spin_lock(pthread_spinlock_t* lock);
  • 功能:对lockunlock的部分代码加锁(仅允许线程串行)。

  • 参数pthread_spinlock_t* lock表示需要加锁的锁指针。

  • 返回值:加锁成功返回0,失败返回-1。

4. 释放自旋锁

int pthread_spin_unlock(pthread_spinlock_t* lock);
  • 功能:标识走出lockunlock的部分代码解锁(恢复并发)。

  • 参数pthread_spinlock_t* lock表示需要解锁的锁指针。

  • 返回值:解锁成功返回0,失败返回-1。

读者写者问题

在多线程编程中,有时候会遇到一种常见的情况,即某些共享数据的修改操作相对较少,而读取操作却非常频繁,且读取操作中可能会伴随着耗时较长的查找操作。在这种情况下,如果对整个数据结构进行加锁,那么即使是读取操作也需要等待锁的释放,这会导致程序效率降低。

为了解决这种情况,可以使用读写锁。读写锁允许多个线程同时获取读锁,只有在获取写锁时才会阻塞其他线程。这样一来,在多读少写的情况下,多个线程可以同时获得读锁,从而提高了程序的并发性能,避免了不必要的阻塞。

总结一下,读写锁适用于多读少写的场景,可以通过允许多个线程同时获取读锁来提高程序的并发性能,避免不必要的阻塞,从而提高了程序的效率。

读者写者模型是用于描述多线程对共享数据进行读写操作时的一种经典并发模型。在读者写者模型中,有两类线程:读者和写者。读者线程只对共享数据进行读操作,而写者线程则对共享数据进行写操作。读者在读操作时不会互斥,多个读者可以同时访问共享数据(不会对数据进行修改),但写者在写操作时需要互斥,同时只允许一个写者访问共享数据且不允许其他任何读者或写者访问。

读者写者模型的目标是实现对共享数据的高效访问,保证数据的一致性和并发性。为了实现这一目标,通常会使用锁和条件变量等同步机制来控制读者和写者线程的访问。

  1. 1个交易场所

  2. 2个角色:读者与写者

  3. 3种关系:写者之间的互斥、读者之间没有关系、读者与写者之间的互斥与同步

    读者和写者之间保持互斥与同步意味着在读者写者模型中,确保读者和写者之间的操作互斥(不能同时访问共享数据)并且同步(按照一定规则进行访问)。具体来说:

  • 互斥(Mutual Exclusion):读者写者模型要求在写者对共享数据进行操作时,必须排他性地拥有对该数据的访问权,即其他任何读者或写者都不可以同时访问共享数据。这样做是为了避免数据一致性问题和争用条件(Race Condition)的发生,确保在写操作时数据不会同时被其他线程读或写。

  • 同步(Synchronization):读者写者模型还要求在读者和写者之间进行协调,保证数据的访问顺序和一致性。通常情况下,写者优先的规则要求在写者请求访问共享数据时,必须等待所有正在读取数据的读者完成操作后才能进行写入;而在有写者等待访问共享数据时,所有新的读者请求必须等待,直到写者完成操作。这种同步行为保证了数据的一致性和安全性。

逻辑过程
int reader_count = 0;
pthread_rwlock_t wlock;
pthread_rwlock_t rlock;

// 读者线程
void reader() {
    lock(&rlock); // 获取读者锁
    if (reader_count == 0) {
        lock(&wlock); // 如果当前没有读者,则获取写者锁
    }
    ++reader_count; // 增加读者计数
    unlock(&rlock); // 释放读者锁

    // 这里进行读取操作

    lock(&rlock); // 重新获取读者锁
    --reader_count; // 减少读者计数
    if (reader_count == 0) {
        unlock(&wlock); // 如果已经没有读者,释放写者锁
    }
    unlock(&rlock); // 释放读者锁
}

// 写者线程
void writer() {
    lock(&wlock); // 获取写者锁

    // 这里进行写入操作

    unlock(&wlock); // 释放写者锁
}

在上述伪代码中,我们模拟了读者写者模型的加锁逻辑,主要包括了对读者和写者线程进行互斥和同步控制。下面我们简要解释一下这段伪代码的逻辑:

  • reader_count表示当前正在读取数据的读者数量。

  • pthread_mutex_t wlockpthread_mutex_t rlock分别表示写者锁和读者锁,用于读者写者线程的互斥操作。

对于读者线程:

1. 首先获取读者锁rlock,确保读者线程之间的互斥。

2. 如果当前没有其他读者在读取数据,则获取写者锁wlock,确保写者无法进入。

  • 申请成功:就接着行下进行

  • 申请失败:说明写者正在写,那就阻塞等着

3. 增加reader_count计数器,表明有一个读者正在读取数据。

4. 释放读者锁,允许其他读者进入读取数据。

5. 进行读取操作。

当没有读者在读时,我们就会释放写者锁

对于写者线程:

  • 获取写者锁wlock,确保写者线程独占对共享数据的访问。

  • 进行写操作。

  • 释放写者锁,允许其他写者或读者访问数据。

接口介绍

1. 初始化读写锁

int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr)
  • 功能:该函数用于初始化一个读写锁对象rwlock,可以指定属性attr,一般情况下可以传入NULL使用默认属性。

  • 参数rwlock:指向读写锁对象的指针,attr:读写锁的属性对象指针,可以为 NULL,表示使用默认属性。

  • 返回值:如果函数调用成功,返回值为 0;否则返回一个非零的错误码。

  • 说明:该函数用于初始化一个读写锁对象,可以指定一些属性,如锁的类型、优先级规则等。

2. 销毁读写锁

int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
  • 功能:用于销毁已经初始化的读写锁对象rwlock销毁读写锁后,该读写锁对象不可再使用,需要重新进行初始化。

  • 参数rwlock:指向读写锁对象的指针。

  • 返回值:如果函数调用成功,返回值为 0;否则返回一个非零的错误码。

  • 说明:该函数用于销毁已经初始化的读写锁对象,释放相关资源。

3. 获取读锁

int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock)
  • 功能:该函数用于获取读锁,即允许多个线程同时获取读取权限,但在写锁被获取时将会阻塞。当读线程数较多时,考虑性能可以使用读锁。

  • 参数rwlock:指向读写锁对象的指针。

  • 返回值:如果函数调用成功,返回值为 0;否则返回一个非零的错误码。

  • 说明:该函数用于获取读锁,允许多个线程同时获取读取权限,但在写锁被获取时将会阻塞。

4. 获取写锁

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock)
  • 功能:该函数用于获取写锁,即独占地写入数据。一旦有线程获取了写锁,其他线程无法获取读锁或写锁,只能等待写锁的释放。

  • 参数rwlock:指向读写锁对象的指针。

  • 返回值:如果函数调用成功,返回值为 0;否则返回一个非零的错误码。

  • 说明:该函数用于获取写锁,独占地写入数据。一旦有线程获取了写锁,其他线程无法获取读锁或写锁,只能等待写锁的释放。

5. 释放锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock)
  • 功能:用于释放读锁或写锁,让其他线程可以获取读写锁。

  • 参数rwlock:指向读写锁对象的指针。

  • 返回值:如果函数调用成功,返回值为 0;否则返回一个非零的错误码。

  • 说明:该函数用于释放读锁或写锁,让其他线程可以获取读写锁,从而读取或写入共享数据。

我们对于读者里面的加锁就直接使用pthread_rwlock_rdlock,相当于上面的全部过程了

同理:对于写者里面的加锁就直接使用pthread_rwlock_wrlock