CAS算法
Compare & Set/Compare & Swap
,CAS
是解决多线程并行情况下使用锁造成性能损耗的一种机制。
假设内存中的原数据V,旧的预期值A,需要修改的新值B
- 比较 A 与 V 是否相等
- 如果比较相等,将 B 写入 V
- 返回操作是否成功
代码示例
bool compare_and_swap (int *accum, int *dest, int newval)
{
if ( *accum == *dest )
{
*dest = newval;
return true;
}
return false;
}
GCC的CAS,GCC4.1+版本中支持CAS的原子操作。
bool __sync_bool_compare_and_swap (type *ptr, type oldval, type newval, ...)
type __sync_val_compare_and_swap (type *ptr, type oldval, type newval, ...)
C++11中的CAS,C++11中的STL中的atomic类的函数可以让你跨平台。
template< class T > bool atomic_compare_exchange_weak( std::atomic* obj,T* expected, T desired );
template< class T > bool atomic_compare_exchange_weak( volatile std::atomic* obj,T* expected, T desired );
CAS
的ABA
问题
ABA
问题描述:
- 进程P1在共享变量中读到值为A
- P1被抢占了,进程P2执行
- P2把共享变量里的值从A改成了B,再改回到A,此时被P1抢占。
- P1回来看到共享变量里的值没有被改变,于是继续执行。
解决ABA
问题:真正要做到严谨的CAS
机制,我们在compare
阶段不仅要比较期望值A和地址V中的实际值,还要比较变量的版本号是否一致。
悲观锁和乐观锁思想
这是两种应对并发的思想
悲观锁
- 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。
- 悲观锁的实现,往往依靠底层提供的锁机制。
- 悲观锁会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。
乐观锁:
- 假设不会发生并发冲突,每次不加锁而是假设没有冲突而去完成某项操作,只在提交操作时检查是否违反数据完整性。
- 如果因为冲突失败就重试,直到成功为止。
- 乐观锁大多是基于数据版本记录机制实现。
- 为数据增加一个版本标识,比如在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。
- 此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
- 乐观锁的缺点是不能解决脏读的问题。
- 在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题。
- 如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁定的方法。
锁机制存在的问题
- 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
- 一个线程持有锁会导致其它所有需要此锁的线程挂起。
- 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。
MVCC多版本并发控制
MVCC
的乐观锁思想的一种实现方式。
当前读:它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。
快照读:像不加锁的select
操作就是快照读,即不加锁的非阻塞读,快照读的实现是基于多版本并发控制,即MVCC
,可以认为MVCC
是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。
可通过数据库执行事务时的Read View
实现,可见下方参考链接。
互斥锁和条件变量
互斥量(mutex
)本质上是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。在互斥量进行加锁以后,任何其它试图再次对互斥量加锁的线程将会阻塞直到当前线程释放该互斥锁。
pthread_mutex_t mutex;
int pthread_mutex_init(pthread_mutex_t* restrict mutex,
const pthread_mutexattr_t* restrict attr);
int pthread_mutex_destroy(pthread_mutex_t* mutex);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_trylock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread_mutex_t* mutex);
条件变量(cond
):多线程程序中用来实现“等待–>唤醒”逻辑的常用的方法。条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包括两个动作:一个线程等待“条件变量的条件成立”而挂起;另一个线程使“条件成立”而发出信号。
int pthread_cond_init(pthread_cond_t* cond, const pthread_condattr_t* sttr);
int pthread_cond_destroy(pthread_cond_t* cond);
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
int pthread_cond_timewait(pthread_cond_t* restrict cond,
pthread_mutex_t* restrict mutex,
const struct timespec* restrict abstime);
/* 一次唤醒一个线程 */
int pthread_cond_signal(pthread_cond_t* restrict cond);
/* 一次唤醒多个线程 */
int pthread_cond_broadcast(pthread_cond_t* restrict cond);
行锁和列锁
一般用于数据库中,在对行对进行读写时进行加锁。
自旋锁
线程的挂起和恢复操作都会转入内核态完成(这是很关键的性能损耗),这些操作给系统的并发性能带来很大压力。很多时候,共享数据的锁定状态只会持续很短一段时间。为了这么短的时间挂起和恢复线程不值得。如果计算机有一个以上的处理器,可以让两个线程同时并行执行,我们就会让后面那个线程稍等一下,但是不放弃处理器执行时间,看看持有锁的线程是否很快释放锁。为了让线程稍等一下,我们需要让线程执行一个忙循环(自旋),这项技术就是自旋锁。
代码示例
class spin_lock
{
private:
std::atomic_flag _atomic;
public:
spin_lock() noexcept;
void lock() noexcept;
void unlock() noexcept;
bool try_lock() noexcept;
};
spin_lock::spin_lock() noexcept :
_atomic(ATOMIC_FLAG_INIT) {}
void spin_lock::lock() noexcept
{
while (_atomic.test_and_set(std::memory_order_acquire));
}
void spin_lock::unlock() noexcept
{
_atomic.clear(std::memory_order_release);
}
bool spin_lock::try_lock() noexcept
{
return _atomic.test() ? false : (_atomic.test_and_set(std::memory_order_acquire));
}
分布式锁
Java中的锁,只能保证在同一个JVM进程内中执行。如果在分布式集群环境下呢?
分布式锁的实现有很多,比如基于数据库、memcached、Redis、系统文件、zookeeper等。它们的核心的理念跟上面的过程大致相同。
分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机之间共享了某个临界资源,往往需要互斥来防止彼此干扰,以保证一致性。
分布式锁的特性:互斥性、锁超时释放、可重入性、高可用、高性能、安全性。
互斥性: 任意时刻,只有一个客户端能持有锁。
锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
安全性:锁只能被持有的客户端删除,不能被其他客户端删除
实现方案:
- SETNX + EXPIRE
- SETNX + value值是(系统时间+过期时间)
- 使用Lua脚本(包含SETNX + EXPIRE两条指令)
- SET的扩展命令(SET EX PX NX)
- SET EX PX NX + 校验唯一随机值,再释放锁
- 开源框架~Redisson
- 多机实现的分布式锁Redlock
线程安全和可重入性
线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
解决方案:
-
synchronized
同步代码块 -
synchronized
同步方法 -
Lock
锁
可重入:常见的情况是,程序执行到某个函数foo()时,收到信号,于是暂停目前正在执行的函数,转到信号处理函数。
如果一个函数是可重入函数,那么它一定是线程安全的,但是如果一个函数是线程安全的,它不一定是可重入函数。
可重入函数需要满足什么条件呢?
- 不能使用malloc系列函数,因为malloc函数内部是通过全局链表实现的
- 不可以调用标准I/O库函数,这些库函数很多都不是可重入的
- 肯定不能有全局或者静态变量,否则连线程安全都不满足了
参考文章:
- 锁机制及CAS实现原理(C++)
- MVCC多版本并发控制原理总结(最终版)
- 多线程编程与资源同步API和示例
- C++ 实现自旋锁
- 几种常见锁的理解
- Redis实现分布式锁的7种方案,及正确使用姿势!