并发编程 之 线程 之 Thread 模块(数据共享, 守护线程), 锁Lock(递归锁), 信号量, 事件, 条件, 定时器 (二)
Thread 模块 from threading import Thread:
与进程的效率差别:
import time from threading import Thread from multiprocessing import Process # 效率差别 def func(a): a = a + 1 if __name__ == '__main__': start = time.time() t_l = [] for i in range(50): t = Thread(target=func,args=(i,)) t.start() t_l.append(t) # t_l 50个线程对象 for t in t_l : t.join() print('主线程') print(time.time() - start) start = time.time() t_l = [] for i in range(50): t = Process(target=func, args=(i,)) t.start() t_l.append(t) # t_l 50个线程对象 for t in t_l: t.join() print('主进程') print(time.time() - start) ''' 跑一下后, 所显示的 时间差, 会非常明显的显出来 之所以会这样, 是因为 对于多进程来说: 需要打开多个进程, 每个进程和进程之间都是独立的空间互不干扰, 所以在打开和关闭的时候 会消耗大量的时间, 对于 多线程来说: 相当于只打开了一个进程, 在一个进程中实现了更多的操作, 这样可以减少 打开和销毁 进程的一部分时间, 不过, Cpython解释器里面的线程实现不了 并行, 对于抢占CPU的利用率来说 是一个很low的东西 ''' 效率差别
数据共享:
在进程中不存在数据共享, 如果想要共享数据,需要引用一些其他模块
在线程中, 数据是共享的
守护线程:
会等待主线程结束之后才结束, 主线程会等待所有子线程结束而结束
主线程结束, 整个进程结束, 所以 主线程需要等待所有线程结束后才能结束
setDaemon(True) 开启守护线程.
import time from threading import Thread def thread1(): while True: print(True) time.sleep(0.5) def thread2(): print('in t2 start') time.sleep(3) print('in t2 end') if __name__ == '__main__': t1 = Thread(target=thread1) t1.setDaemon(True) t1.start() t2 = Thread(target=thread2) t2.start() time.sleep(1) print('主线程') # 主线程如果结束了 那么整个进程就结束 # 守护线程 会等待主线程结束之后才结束. # 主进程 等待 守护进程 子进程 # 守护进程 只守护主进程的代码就可以了 # 守护线程不行 主线程如果结束了 那么整个进程就结束 所有的线程就都结束 守护线程
用类开启线程:
继承Thread类, 在里面重写run()
from threading import Thread,get_ident # threading.get_ident() 此方法可以用来查看线程 id class MyThread(Thread): def run(self): print('in my thread : ',get_ident(),self.args) print('main',get_ident()) t = MyThread('wahaha') t.start() 用类开启线程 (不含参数)
若 方法中含有参数, 只需要在自己写的类中,重写 __init__() 构造方法, 同时继承父类的__init__() 方法:
from threading import Thread,get_ident # 开启线程的第二种方式和查看线程id class MyThread(Thread): def __init__(self,args): super().__init__() # Thread类的init,在这个方法中做了很多对self的赋值操作,都是给创建线程或者使用线程的时候用的 self.args = args def run(self): print('in my thread : ',get_ident(),self.args) print('main',get_ident()) t = MyThread('wahaha') t.start() 用类开启线程 (含参数)
线程中的其他方法:
is_alive() 判断是否活着
enumerate() 将正在进行的线程组成一个列表进行返回
activeCount() 计算正在运行的线程的数量
锁Lock, 递归锁PLack:
在进程和线程中, 同时访问一个数据的时候就会产生数据不安全的现象
GIL 全局解释器锁:
此锁是用来锁住解释器往CPU中传输需要计算的内容.
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制。Python在设计之初就考虑到要在主循环中,同时只有一个线程在执行。虽然 Python 解释器中可以“运行”多个线程,但在任意时刻只有一个线程在解释器中运行。 对Python虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。 在多线程环境中,Python 虚拟机按以下方式执行: a、设置 GIL; b、切换到一个线程去运行; c、运行指定数量的字节码指令或者线程主动让出控制(可以调用 time.sleep(0)); d、把线程设置为睡眠状态; e、解锁 GIL; d、再次重复以上所有步骤。 在调用外部代码(如 C/C++扩展函数)的时候,GIL将会被锁定,直到这个函数结束为止(由于在这期间没有Python的字节码被运行,所以不会做线程切换)编写扩展的程序员可以主动解锁GIL。 GIL详解
互斥锁: Lock
此锁是用来对内存进行锁定, 避免出现多线程 同时对某一数据进行增删改查,
用法和进程中的锁大概也一样, 只不过在进程中的 数据本身是不被共享的, 而这里的数据本身就是共享的, 所以往往在线程中出现的概率更大.
# 尽量不要设置全局变量 # 只要在多线程/进程之间用到全局变量 就加上锁 from threading import Lock,Thread lock = Lock() lock.acquire() lock.acquire() noodle = 100 def func(name,lock): global noodle lock.acquire() noodle -= 1 lock.release() print('%s吃到面了'%name) if __name__ == '__main__': lock = Lock() # 线程锁 互斥锁 t_lst = [] for i in range(10): t = Thread(target=func,args=(i,lock)) t.start() t_lst.append(t) for t in t_lst: t.join() print(noodle) Lock
递归锁: PLock
此锁是用来解决, 在使用互斥锁的时候出现的死锁的状况
死锁现象: 本质上还是代码的逻辑出现了问题,
所以 递归锁只是用来进行修补的强力胶, 而不是一个正常代码中的逻辑方法
ps:
死锁:
多把锁同时应用在多个线程中
互斥锁和递归锁哪个好:
递归锁 快速恢复服务
死锁问题的出现 是程序的设计或者逻辑的问题
还应该进一步的排除和重构逻辑来保证使用互斥锁也不会发生死锁
互斥锁和递归锁的区别:
互斥锁 就是在一个线程中不能连续多次ACQUIRE
递归锁 可以在同一个线程中acquire任意次,注意acquire多少次就需要release多少次
信号量 Semphore:
池与信号量的区别:
池 效率高:
池子里有几个进程/线程 一共就几个进程/线程
不管有多少任务, 池子的个数都是固定的
开启进程和关闭进程这些事都是需要固定的时间开销
不产生额外的时间开销
且进程池中的进程数控制的好, 那么操作系统的压力越小
信号量:
有多少个任务就开多少 进程/线程
可以帮助你进啊少操作系统切换的负担
但是并不能帮助你减少 进程/线程的开启和关闭的时间,
所以 信号量的效率略低
事件 Even: 同进程的事件整体来说是一样的.
方法:
wait() 等待,
set()
clear()
is_set()
条件:Condition
方法:
acquire()
release()
wait() 阻塞
notify() 让wait接触阻塞
ps: wait() 和 notify() 必须在执行这两个方法的前后加上 acquire()和release()
定时器 Timer:
定时器,指定n秒后执行某个操作
from threading import Timer def hello(): print("hello, world") t = Timer(1, hello) t.start() # after 1 seconds, "hello, world" will be printed