在Linux系统中,同步机制是操作系统中非常重要的一部分,以下是一些基本要点:
互斥锁
互斥锁是一种「独占锁」,比如当线程 A 加锁成功后,此时互斥锁已经被线程 A 独占了,只要线程 A 没有释放手中的锁,线程 B 加锁就会失败,失败的线程B于是就会释放 CPU 让给其他线程,既然线程 B 释放掉了 CPU,自然线程 B 加锁的代码就会被阻塞。
对于互斥锁加锁失败而阻塞的现象,是由操作系统内核实现的。当加锁失败时,内核会将线程置为「睡眠」状态,等到锁被释放后,内核会在合适的时机唤醒线程,当这个线程成功获取到锁后,于是就可以继续执行。如下图:
互斥锁加锁失败,就会从用户态陷入内核态,内核帮我们切换线程,这简化了互斥锁使用的难度,但也存在性能开销。
那这个开销成本是什么呢?会有两次线程上下文切换的成本:
- 当线程加锁失败时,内核会把线程的状态从「运行」状态设置为「睡眠」状态,然后把 CPU 切换给其他线程运行;
- 接着,当锁被释放时,之前「睡眠」状态的线程会变为「就绪」状态,然后内核会在合适的时间,把 CPU 切换给该线程运行。
线程的上下文切换的是什么?当两个线程是属于同一个进程,因为虚拟内存是共享的,所以在切换时,虚拟内存这些资源就保持不动,只需要切换线程的私有数据、寄存器等不共享的数据。
上下文切换需要几十纳秒到几微秒之间,如果锁住的代码执行时间极短(常见情况),那花在两次上下文切换的时间就会远多于锁住代码的执行时长。而且,线程的私有数据已经在CPU的cache上都预热好了,这一出一进,数据可能就凉透了,之后反复的cache miss那可就真的酸爽。所以,锁住的代码执行只需要几纳秒的话,为啥不持有CPU继续自旋等待呢?
自旋锁
自旋锁是最比较简单的一种锁,一直自旋,利用 CPU 周期,直到锁可用。需要注意,在单核 CPU 上,需要抢占式的调度器(即通过时钟中断一个线程,运行其他线程)。否则,自旋锁在单 CPU 上无法使用,因为一个自旋的线程永远不会放弃 CPU。
自旋锁开销少,在多核系统下一般不会主动产生线程切换,适合异步、协程等在用户态切换请求的编程方式,但如果被锁住的代码执行时间过长,自旋的线程会长时间占用 CPU 资源,所以自旋的时间和被锁住的代码执行的时间是成「正比」的关系,我们需要清楚的知道这一点。
自旋锁与互斥锁使用层面比较相似,但实现层面上完全不同:当加锁失败时,互斥锁用「线程切换」来应对,自旋锁则用「忙等待」来应对。这里的忙等待,可以用「while」循环实现,但最好不要这么干!!CPU提供了「PAUSE」指令来实现忙等待。
总结
互斥锁和自旋锁没有优略之分,工程中使用哪种锁,主要还是看使用场景(洗地操作)。
一般情况使用互斥锁。如果我们明确知道被锁住的代码的执行时间很短(这样的场景最普遍,就算不普遍也要改代码让这种场景普遍),那我们应该选择开销比较小的自旋锁,因为自旋锁加锁失败时,并不会主动产生线程切换,而是一直忙等待,直到获取到锁,那么如果被锁住的代码执行时间很短,那这个忙等待的时间相对应也很短。
不管使用的哪种锁,我们的加锁的代码范围应该尽可能的小,也就是加锁的粒度要小,这样执行速度会比较快。
-
什么是同步机制?同步机制是一种操作系统提供的机制,用于协调多个进程或线程之间的访问共享资源,防止出现竞态条件和死锁等问题。
-
Linux中常用的同步机制有哪些?Linux中常用的同步机制包括互斥锁、读写锁、自旋锁、信号量、条件变量等。
-
读写锁和互斥锁的区别是什么?互斥锁加锁失败后,线程释放CPU,给其他线程;自旋锁加锁失败后,线程会忙等待,直到它拿到锁;读写锁和互斥锁都是用于保护共享资源的机制,但是它们的实现方式和使用场景不同。互斥锁通常用于保护临界区,在临界区中只允许一个进程或线程操作共享资源;读写锁用于保护读写共享资源,允许多个进程或线程同时读取共享资源,但是只允许一个进程或线程写入共享资源。
-
读写锁和互斥锁的使用场景:当被锁的代码消耗时间很小时推荐使用自旋锁,防止线程切换,因为线程切换时间会长一点。当被锁的代码消耗时间比较长时,推荐使用互斥锁。
-
信号量和条件变量的区别是什么?信号量和条件变量都是用于同步进程或线程的机制,但是它们的实现方式和使用场景不同。信号量通常用于限制共享资源的访问数量,多个进程或线程可以同时访问共享资源,但是访问数量受到信号量的限制;条件变量用于线程间的通信,允许线程在特定条件下等待和唤醒。
-
什么是死锁?死锁是指两个或多个进程或线程互相等待对方持有的资源,从而导致进程或线程无法继续执行的一种情况。死锁是多线程编程中常见的问题,需要通过合理的同步机制和设计来避免。
-
Linux中如何防止死锁?Linux中提供了多种机制来防止死锁,包括资源的有序分配、避免持有多个锁、使用超时机制等。在编写多线程程序时,应该尽可能地避免使用复杂的锁嵌套,合理地设计同步机制,避免出现死锁。
-
什么是内核抢占?内核抢占是指内核在某些情况下可以抢占正在运行的进程或线程,以保证内核的响应能力和稳定性。内核抢占通常发生在中断处理程序中,当中断处理程序需要执行一些紧急的操作时,可以抢占正在运行的进程或线程,以保证中断处理程序的及时响应。
-
Linux中如何实现内核抢占?Linux中提供了可抢占内核和完全抢占内核两种模式来实现内核抢占。可抢占内核是指在内核代码中插入一些抢占点,以允许内核在某些情况下抢占正在运行的进程或线程;完全抢占内核是指内核中所有的临界区都支持抢占,可以在任何时候抢占正在运行的进程或线程。可抢占内核和完全抢占内核都可以提高内核的响应能力和稳定性,但是完全抢占内核的开销更大一些。
-
什么是原子操作?原子操作是一种不可分割的操作,要么全部执行成功,要么全部执行失败,不会出现部分执行成功的情况。在多线程编程中,原子操作可以保证数据的一致性和线程安全性。Linux中提供了多种原子操作函数,如atomic_t、atomic_read、atomic_set等,可以用于实现同步机制和保证数据的一致性。
-
Linux中如何实现进程间通信?Linux中提供了多种进程间通信的机制,包括管道、消息队列、共享内存、信号量、套接字等。这些机制可以用于不同进程之间的通信和数据交换。
-
什么是线程?线程是进程中的一个执行单元,可以与其他线程共享进程的资源。线程的使用可以提高程序的并发性和响应能力,但是需要注意线程安全性和同步机制的设计。
-
Linux中如何实现线程?Linux中提供了多种线程的实现方式,包括用户级线程和内核级线程。用户级线程是指由用户空间的线程库实现的线程,可以提高程序的并发性和响应能力;内核级线程是指由内核实现的线程,可以充分利用多核处理器的性能优势。Linux中使用pthread库来实现线程,可以使用pthread_create、pthread_join、pthread_mutex_lock等函数来创建、等待和同步线程的执行。
-
什么是多线程编程中的竞态条件?竞态条件是指多个线程同时访问共享资源,从而导致数据的不可预测性和不一致性的一种情况。在多线程编程中,需要通过同步机制来避免竞态条件的出现。
-
什么是线程安全性?线程安全性是指在多线程环境下,程序能够正确地处理共享资源,不会出现数据的不一致性和竞态条件等问题。在多线程编程中,需要考虑线程安全性,使用合适的同步机制和设计,来保证程序的正确性和可靠性。