29.并发编制【六】守护线程与锁

【一】守护线程

守护线程是在后台运行并依赖于主线程或非守护线程的存在

1)主线程死亡,子线程存活

  • 主线程结束后不会立马结束,而是等待其他子线程结束之后结束
from threading import Thread
import time

def work(name):
    print(f'{name}开始')
    time.sleep(2)
    print(f'{name}结束')

if __name__ == '__main__':
    print(f'主线程开始')
    t = Thread(target=work, args=('ST1',))
    t.start()
    print(f'主线程结束')
# 主线程开始
# ST1开始
# 主线程结束
# ST1结束

2)主线程死亡,子线程也死亡

  • 给子线程添加守护,便可在主进程结束后直接结束,无需等待子线程
from threading import Thread
import time

def work(name):
    print(f'{name}开始')
    time.sleep(2)
    print(f'{name}结束')

if __name__ == '__main__':
    print(f'主进程开始')
    t = Thread(target=work, args=('ST1',))
    t.daemon = True
    t.start()
    print(f'主进程结束')
# 主进程开始
# ST1开始
# 主进程结束

3)示例

  • work_1被守护,当主线程结束后,程序会等待work_2结束后直接关闭
  • 若work_1在work_2之后完成,程序将等不到work_1的输出便会结束
  • 若work_2在work_1之后完成,程序便能正常接收到work_1的输出
from threading import Thread
import time

def work_1():
    print('work_1 开始')
    time.sleep(2)
    print('work_1 结束')

def work_2():
    print('work_2 开始')
    time.sleep(1)
    print('work_2 结束')

if __name__ == '__main__':
    print('主线程开始')
    t1 = Thread(target=work_1)
    t1.daemon = True
    t2 = Thread(target=work_2)
    t1.start()
    t2.start()
    print('主线程结束')
# 主线程开始
# work_1 开始
# work_2 开始
# 主线程结束
# work_2 结束

【二】线程的互斥锁

1)无互斥锁

  • 所有子进程都会进行阻塞,导致最后的结果只改变了一次
  • 由于延迟的存在,每次赋值时原值都未来得及改变,才使得结果只改变了一次
from threading import Thread
import time
number = 0

def work():
    global number
    num = number
    time.sleep(0.03)
    number = num + 1

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
# 1

2)有互斥锁

  • 在数据发送变化前加锁,让上一个进程结束后在执行下一个进程
from threading import Thread, Lock
import time
number = 0
# 生成锁
lock = Lock()

def work():
    global number
    # 数据改变前加锁
    lock.acquire()
    num = number
    time.sleep(0.03)
    number = num + 1
    # 数据改变后解锁
    lock.release()

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
# 100

【三】GIL全局解释器锁

1)介绍

在Cpython中,全局解释器锁(GIL)是一个互斥锁,它可以防止多个本地线程同时执行Python字节码。这把锁主要是必要的
因为Cpython的内存管理不是线程安全的。(然而,由于GIL存在,其他特性已经发展到依赖于它所实施的保证。)

2)区别

from threading import Thread, Lock
import time

lock = Lock()
number = 0

# 所有线程都去抢那把 GIL 锁住的数据,当所有子线程都抢到后再去修改数据就变成了 99
def work_1():
    global number
    num = number
    time.sleep(0.1)
    number = num + 1

# 谁先抢到谁就先处理数据
def work_2():
    global number
    num = number
    number = num + 1

# 自动加锁并解锁
# 子线程启动,后先去抢GIL锁,进入IO自动释放GIL锁,但是自己加的锁还没解开,其他线程资源能抢到GIL锁,但是抢不到互斥锁,最终GIL回到互斥锁的那个进程上,处理数据
def work_3():
    global number
    with lock:
        num = number
        time.sleep(0.01)
        number = num + 1

if __name__ == '__main__':
    task_list = []
    for i in range(100):
        t = Thread(target=work_)
        task_list.append(t)
    [t.start() for t in task_list]
    [t.join() for t in task_list]
    print(number)
#work_1:1
#work_2:100
#work_3:100

3)GIL导致多线程无法利用多核优势

1.Cpython解释器中GIL

  • 在Cpython解释器中GIL是一把互斥锁,用来阻止同一个进程下的多个线程的同时进行

2.Python的多线程

  • 多线程是否有用看情况
    • 单核
      • 四个任务(IO密集型/计算密集型)
    • 多核
      • 四个任务(IO密集型/计算密集型)
计算密集型

一种处在计算运行中

  • 每个任务都需要 10s

    • 单核
      • 多进程:额外消耗资源
      • 多线程:减少开销
    • 多核
      • 多进程:总耗时 10s
      • 多线程:总耗时 40s+
from multiprocessing import Process
from threading import Thread
import time, os

def work():
    res = 0
    for i in range(1, 100000000):
        res *= i

def main_t():
    t_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        t = Thread(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(f'总耗时:>>>{time.time() - start_time}')
    
def main_p():
    p_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print(f'总耗时:>>>{time.time() - start_time}')

if __name__ == '__main__':
    main_t()  # 十二个核一致在跑 12 个进程就需要开设 12块内存空间 ---> 耗费资源和时间
    # 12
    # 总耗时:>>>5.368584394454956
    main_p()  # 只需要开设一个进程 在进程内部开设 12个线程就可以了
    # 12
    # 总耗时:>>>27.101927280426025
    
IO密集型

存在多个 IO 阻塞切换操作

  • 每个任务都需要 10s

    • 多核
      • 多进程:相对浪费资源
      • 多线程:更加节省资源
from multiprocessing import Process
from threading import Thread
import time, os

def work():
    res = 0
    for i in range(1, 100000000):
        res *= i

def main_t():
    t_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        t = Thread(target=work)
        t.start()
        t_list.append(t)
    for t in t_list:
        t.join()
    print(f'总耗时:>>>{time.time() - start_time}')

def main_p():
    p_list = []
    # 获取当前CPU运行的个数
    print(os.cpu_count())
    start_time = time.time()
    for i in range(12):
        p = Process(target=work)
        p.start()
        p_list.append(p)
    for p in p_list:
        p.join()
    print(f'总耗时:>>>{time.time() - start_time}')

if __name__ == '__main__':
    main_t()  # 十二个核一致在跑 12 个进程就需要开设 12块内存空间 ---> 耗费资源和时间
    # 12
    # 总耗时:>>>27.46139693260193
    main_p()  # 只需要开设一个进程 在进程内部开设 12个线程就可以了
    # 12
    # 总耗时:>>>5.451528549194336

3.小结

  • 计算是消耗cpu的:

    • 代码执行,算术,for都是计算
  • io不消耗cpu:

    • 打开文件,写入文件,网络操作都是io
    • 如果遇到io,该线程会释放cpu的执行权限,cpu转而去执行别的线程
  • 计算密集型任务使用多进程可以充分利用多核CPU的优势,

  • IO密集型任务使用多线程能够更好地处理IO操作,避免频繁的进程切换开销。

4.GIL特点总结

  • GIL 不是python的特点而是Cpython解释器的特点
  • GIL 保证解释器级别的数据的安全
  • GIL会导致同一个进程下的多个线程的无法同时进行即无法利用多核优势
  • 针对不同的数据还是需要加不同的锁处理
  • 解释型语言的通病:同一个进程下的多个线程无法利用多核优势

【四】死锁

1)介绍

  • 指两个或多个进行,在执行过程中因争夺资源而造成了互相等待的一种现象

  • 即两个或多个进程持有各自的锁并试图获取对方持有的锁,从而导致阻塞,不能向前执行,最终形成僵局。

2)决绝方案

  • 锁不要有多个,一个足够
  • 如果真的发生了死锁的问题,必须迫使一方先交出锁

3)示例

from threading import Thread, Lock
import time

lock_1 = Lock()
lock_2 = Lock()

class MyThread(Thread):
    def run(self):
        self.work1()
        self.work2()
    def work1(self):
        lock_1.acquire()
        print(f'{self.name}抢到了A锁')
        lock_2.acquire()
        print(f'{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()
    def work2(self):
        lock_2.acquire()
        print(f'{self.name}抢到了A锁')
        time.sleep(1)
        lock_1.acquire()
        print(f'{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()

if __name__ == '__main__':
    for i in range(5):
        t = MyThread()
        t.start()
# Thread-1抢到了A锁
# Thread-1抢到了B锁
# Thread-1抢到了A锁
# Thread-2抢到了A锁
# 进程卡死,无法继续

【五】递归锁

1)介绍

  • 一种特殊的锁,允许一个线程多次请求同一个锁
  • 在该线程释放锁之前,会对锁计数器进行累加操作,线程每成功获得一次锁时,都要进行相应的解锁操作,直到锁计数器清零才能完全释放
  • 其能保证同一线程在持有锁时能够再次获取该所,而不是被自己所持有的锁阻塞
  • 但过多的获取锁会导致性能下降

3)示例

from threading import Thread, Lock, RLock
import time

lock_1 = lock_2 = RLock()

class MyThread(Thread):
    def run(self):
        self.work1()
        self.work2()
    def work1(self):
        lock_1.acquire()
        print(f'1-{self.name}抢到了A锁')
        lock_2.acquire()
        print(f'1-{self.name}抢到了B锁')
        lock_2.release()
        lock_1.release()
    def work2(self):
        lock_2.acquire()
        print(f'2-{self.name}抢到了A锁')
        time.sleep(1)
        lock_1.acquire()
        print(f'2-{self.name}抢到了B锁')
        lock_1.release()
        lock_2.release()

if __name__ == '__main__':
    for i in range(3):
        t = MyThread()
        t.start()
# 1-Thread-1抢到了A锁
# 1-Thread-1抢到了B锁
# 2-Thread-1抢到了A锁
# 2-Thread-1抢到了B锁
# 1-Thread-2抢到了A锁
# 1-Thread-2抢到了B锁
# 2-Thread-2抢到了A锁
# 2-Thread-2抢到了B锁
# 1-Thread-3抢到了A锁
# 1-Thread-3抢到了B锁
# 2-Thread-3抢到了A锁
# 2-Thread-3抢到了B锁
posted on 2024-05-28 20:17  晓雾-Mist  阅读(7)  评论(0编辑  收藏  举报