⭐小白苦学IT的博客主页
⭐初学者必看:Linux操作系统入门
⭐代码仓库:Linux代码仓库
❤关注我一起讨论和学习Linux系统
1.什么是线程安全问题?
线程安全问题是指在多线程环境中,当多个线程同时访问共享数据时,由于操作顺序的不确定性,可能导致数据的不一致性或错误。简单来说,就是当一个线程访问的共享数据被其他线程修改时,就可能发生线程安全问题。
线程安全问题的原因主要有以下几点:
- 操作系统的线程调度方式:操作系统的线程是“抢占式执行,随机调度”,这意味着程序在多线程环境下的执行顺序存在很多变数,可能导致线程之间的操作冲突。
- 多个线程同时修改同一个变量:如果多个线程同时修改同一个变量,如执行count++操作,就有可能出现两个线程同时读取count的值,然后同时修改,再同时存入,导致count只自增了一次,而不是预期的自增两次。
- 内存可见性:一个线程读,一个线程写时,编译器可能优化成读寄存器或缓存,导致写线程做出的修改,读线程感知不到。
- 指令重排序:计算机编译器和处理器在执行程序时可能会对指令顺序进行重新排序,这也可能导致线程安全问题。
2.STL,智能指针和线程安全
STL中的容器是否是线程安全的?
C++标准模板库(STL)中的容器本身不是线程安全的。这意味着,在没有适当的外部同步机制的情况下,从多个线程同时访问同一个STL容器可能会导致数据竞争和不可预测的行为。
具体来说,当至少有一个线程在修改容器(如添加、删除元素),而其他线程正在读取或写入同一个容器时,必须使用适当的同步机制(如互斥锁)来保护对容器的访问。此外,即使STL容器本身是线程安全的,但在多线程环境下使用时,仍需要注意对迭代器的操作可能会引起容器的线程安全问题。
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
3.其他常见的各种锁
- 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
- 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
- CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
- 自旋锁,公平锁,非公平锁?
自旋锁详解
自旋锁概念
自旋锁(spinlock)是一种轻量级的锁机制,用于实现保护共享资源的目的。在多线程环境中,当多个线程尝试访问同一共享资源时,自旋锁能够确保同一时刻只有一个线程能够访问该资源,从而避免数据竞争和不一致性的问题。
自旋锁工作原理
自旋锁的工作原理基于“自旋”的概念。当一个线程尝试获取已经被另一个线程占有的锁时,该线程不会立即进入睡眠状态,而是会进入一个忙等待的循环,不断检查锁的状态,直到锁被释放。这种持续检查锁状态的行为被称为“自旋”。一旦锁被释放,等待的线程会立即获取锁并继续执行。
自旋锁运用场景
- 自旋锁适用于那些需要短暂等待的情况,因为它避免了线程的上下文切换和睡眠开销。然而,如果等待时间较长,自旋锁可能会导致CPU资源的浪费,因为等待的线程会持续占用CPU进行循环检查。
- 自旋锁在多处理器环境中特别有用,特别是在某些资源有限的情况下。由于多线程的核心是CPU的时分片,因此通过自旋锁可以实现线程之间的同步,确保对共享资源的互斥访问。
- 需要注意的是,自旋锁并不适用于所有场景。在长时间等待或高并发场景下,使用其他类型的锁(如互斥锁)可能更为合适。此外,自旋锁的使用也需要谨慎处理,以避免死锁和活锁等问题。
说白了就是
总之,自旋锁是一种有效的线程同步机制,能够在多线程环境中保护共享资源的安全访问。它通过忙等待的方式实现线程之间的互斥访问,适用于短暂等待的场景。
4.读者写者问题
其实我们生活学习过程中也存在很多的读者写者的问题,像我们写博客,写文章,出黑板报,做视频等这些都是典型的读者写者问题案例。
什么是读写者问题?
读者写者问题(Reader-Writer Problem)是计算机科学中的一个经典并发控制问题。该问题关注的是在多个读者(只读取数据,不修改数据)和多个写者(修改数据)之间共享数据资源时,如何保证数据的一致性和正确性,同时最大化并发性能。
具体来说,读者写者问题要解决以下几个方面的挑战:
互斥性(Mutual Exclusion):当一个写者在写入数据时,必须保证其他写者和读者都不能访问数据,以防止数据冲突和不一致。
读者优先或写者优先:这是两种常见的策略,用于决定当有读者和写者同时请求访问数据时,哪一方应该被优先处理。
读者优先:当至少有一个读者正在访问数据时,新到达的读者可以立即访问,而写者需要等待直到所有读者都完成访问。这可能导致写者长时间等待,特别是在高读取频率的场景下。
写者优先:当有写者请求访问数据时,系统会优先处理写者的请求,阻止新的读者访问,并等待现有的读者完成访问。这可以确保写者不会长时间等待,但可能牺牲一些读者的并发性。
读者之间的并发性:多个读者可以同时访问数据,而不会造成数据冲突,因此应该允许这种并发访问以提高效率。
饥饿问题(Starvation):必须确保系统不会陷入一种状态,使得某个或某些读者或写者永远无法访问数据资源(即避免饥饿)。
公平性(Fairness):在长时间运行的系统中,应保证读者和写者都有机会公平地访问数据资源。
为什么要有读写锁?
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
初识读写锁
读写锁(Read-Write Lock)是一种用于多线程编程的同步机制,它允许多个线程同时读取共享资源,但在有线程要对共享资源进行写操作时,要求其它线程不能执行读或写操作。这种锁机制有助于平衡读操作和写操作之间的线程同步需求,提高并发性能。
读写锁接口认识
1.初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
const pthread_rwlockattr_t *restrict attr);
rwlock
是一个指向读写锁对象的指针。attr
是一个指向读写锁属性的指针,通常可以设置为NULL以使用默认属性。
2.销毁读写锁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
3.加读锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
当多个线程持有读锁时,其他线程仍然可以获取读锁,但不能获取写锁。
4.加写锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
当写锁被持有时,其他线程(无论是读线程还是写线程)都不能获取锁。
5.解锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
6.尝试加锁
尝试加读锁:pthread_rwlock_tryrdlock
尝试加写锁:pthread_rwlock_trywrlock
这些函数尝试获取锁,如果锁已经被其他线程持有,则立即返回错误(通常是EBUSY),而不是阻塞等待。
注意:在使用读写锁时,需要确保在适当的时候释放锁,以避免死锁和资源争用问题。通常,最好在进入临界区之前获取锁,在离开临界区之后释放锁。此外,读写锁并不适用于所有场景,有时其他同步机制(如互斥锁或条件变量)可能更为合适。