目录

全局解释器锁(GIL)

同步锁(互斥锁)

死锁

进程锁


全局解释器锁(GIL)

什么是全局解释器锁

每个CPU在同一时间只能执行一个线程,每个线程在执行之前都会拿到一把GIL锁,只有拿到这把锁才能正常执行任务,其他的线程就必须等待该线程的使用权消失后才能使用全局解释器,GIL锁只存在于cpython中,在其他解释器中不存在。

全局解释器锁的好处

  • 避免大量加锁解锁的操作
  • 使数据更加安全,解决多线程间的数据的完整行和同步性

理解记忆

  • python有GIL锁的原因,同一进程下多个线程,实际上同一时刻,只有一个线程在执行
  • 因为GIL锁的原因,所以在python上开进程的用的比较多,而在其他语言一般不开多进程,只开多线程 
  • cpython解释器开多线程不能利用多核优势,因为GIL只限制线程,所以可以开多进程才能利用多核优势,其他语言不存在这个问题
  • 如果是8核CPU的电脑,想充分利用8核优势,至少要开8个进程,同时运行这8个进程,CPU使用率才是100%
  • 但是如果不存在GIL锁,就可以一个进程下,开启8个线程,也能充分利用CPU资源,跑满CPU
  • 为了充分利用多核CPU,可以使用多进程来实现并行计算,每个进程拥有独立的解释器和GIL锁。另外,每个进程下开启的线程可以被多个CPU调度执行,从而充分利用多核CPU。
  •  cpython解释器:io密集型使用多线程,计算密集型使用多进程

 # -io密集型, 当线程遇到 I/O 操作时,它会把自己从运行队列中移除,然后让 CPU 执行其他线程,同时等待 I/O 操作完成。这个过程被称为阻塞。当 I/O 操作完成后,操作系统会将线程重新添加到运行队列中,然后让 CPU 执行它。
  
  # -计算密集型,消耗cpu,如果开了8个线程,第一个线程会一直占着cpu,而不会调度到其他线程执行,其他7个线程根本没有机会执行,所以我们可以开8个进程,每个进程有一个线程,8个进程下的线程会被8个cpu执行,从而效率高 

同步锁(互斥锁)

开多进程或者多线程虽然可以提高效率,但是会出现安全问题,多线程同时操作一个数据,容易出现数据错乱,产生并发安全问题,可以通过加锁,让原本的并发,变成串行,牺牲效率,保证安全,其次线程的队列也可以避免并发安全问题,所有队列的本质也是锁

例:以下是加锁的代码实现

使用锁的目的是让多个线程之间对共享资源的访问变成串行,避免并发访问时出现数据不一致的问题。在本示例中,由于多个线程都要修改count的值,我们使用锁来保证每次只有一个线程在修改,避免出现错误的结果。

import threading

# 定义一个全局变量counter
counter = 0
# 定义一个全局锁
lock = threading.Lock()

# 定义一个函数,该函数会在多线程中被调用
def increment_counter():
    # 获取锁
    lock.acquire()
    global counter
    # 对全局变量counter加1
    counter += 1
    # 释放锁
    lock.release()

# 创建多个线程,每个线程调用increment_counter函数
for i in range(10):
    t = threading.Thread(target=increment_counter)
    t.start()

# 等待所有线程执行完毕
for t in threading.enumerate():
    if t != threading.current_thread():
        t.join()

# 输出最终结果
print("Counter value: ", counter)

例:下面是不加锁的代码,用于演示多线程操作数据时可能发生的并发安全问题:

这个代码中,全局变量 num 初始值为 0,然后创建了两个线程分别执行自增和自减操作,每个线程执行 10000 次操作。这个操作看起来是并发的,实际上由于没有加锁,两个线程可能会同时访问并修改 num 变量,导致结果不可预测。最后输出 num 的值,预期结果应该是 0,但是实际上可能不是 0,因为线程执行的顺序和调度是不可预测的。

import threading

# 全局变量 num,初始值为 0
num = 0

# 自增函数,执行 10000 次 num += 1 操作
def increment():
    global num
    for i in range(10000):
        num += 1

# 自减函数,执行 10000 次 num -= 1 操作
def decrement():
    global num
    for i in range(10000):
        num -= 1

# 创建两个线程,一个执行自增操作,一个执行自减操作
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=decrement)

# 启动两个线程
t1.start()
t2.start()

# 等待两个线程执行完毕
t1.join()
t2.join()

# 输出 num 的值,预期值为 0,但实际上可能不是 0
print(num)

死锁

死锁指的是两个或多个线程或进程,在执行过程中,因互相持有对方所需要的资源而相互等待,导致都无法继续执行下去的状态。换句话说,就是一种程序上的无限期等待状态。

举个例子,假设线程A持有锁1,尝试获取锁2,同时线程B持有锁2,尝试获取锁1,这时候两个线程就相互等待对方释放资源,形成了死锁。

在实际编程中,死锁是需要尽量避免的,因为一旦发生死锁,程序就会陷入无限等待状态,无法继续执行下去,会影响程序的稳定性和可靠性。一些避免死锁的方法包括:

  1. 避免使用多个锁,尽量减少线程间的竞争;
  2. 对多个锁进行排序,按照相同的顺序获取锁,释放锁的顺序与获取锁的顺序相反;
  3. 使用超时机制,避免一直等待资源;
  4. 使用死锁检测工具,及时检测死锁并解决。

需要注意的是,在某些情况下,死锁是不可避免的,这时候可以采用超时等机制或者自动重试等方法来解决。

进程锁

进程锁是一种用于进程间同步的锁机制。与线程锁不同的是,进程锁可以用于不同进程间的同步。

在使用进程锁时,需要先创建一个锁对象,然后在进程中需要同步的地方加锁,执行完同步操作后再解锁,以保证同步操作的原子性和互斥性。如果在同步期间有其他进程试图获取同一个锁,那么该进程会被阻塞,直到锁被释放为止。

使用进程锁的场景一般是多个进程需要访问共享资源,例如文件或共享内存等。通过加锁操作,可以确保同一时间只有一个进程能够访问共享资源,避免了数据竞争和不一致的情况发生。

需要注意的是,进程锁的开销比线程锁要大,因为它需要使用操作系统的信号量机制,同时还需要在进程间进行通信。因此,在选择锁机制时,需要根据具体的应用场景来进行权衡和选择。