python D34 线程池、GIL锁、定时器和条件

一、条件Condition

使得线程等待,只有满足某条件时,才释放n个线程

条件(condition)
con.acquire() # 相当于锁
con.wait()   # 等待notify传过来的钥匙
con.release() # 释放锁(不会换回钥匙,用完就没了)


con = Condition() # 制造条件
con.acquire() # 相当于锁
con.notify(num) #相当于造钥匙(num:钥匙的数量)
con.release() # 释放锁(不会换回钥匙,用完就没了)

from threading import Thread, Condition

def func(con, i):
    con.acquire() # 相当于锁
    con.wait() # 等待notify传过来的钥匙
    print('这是第%s进程' %i)
    con.release()  # 释放锁(不会换回钥匙,用完就没了)


con = Condition()
for i in range(10):
    t = Thread(target=func, args=(con, i))
    t.start()


while 1:
    try:
        num = int(input('请输入一个数字'))
        con.acquire()
        con.notify(num)  #相当于造钥匙(num:钥匙的数量)
        con.release()
    except:
        print('你输入的格式有误')
Condition

二、定时器

定时器,指定n秒后执行某个操作,这个做定时任务的时候可能会用到。

# from threading import Timer
# import threading
#
# def hello():
#     print('hello,world')
#
# t = Timer(2, hello) # 过两秒执行hello这个函数
# t.start()
定时器

三、线程队列

# 线程中的三种队列:
# 1、正常队列:
# import queue
# q = queue.Queue
#
# 2、先进后出队列
# import queue
# q = queue.LifoQueue
#
# 3、优先级队列(数据以元组的形式比较),按大小,按ASCII码排序.要保证比较的数据类型一致性
# import queue
# q = queue.PriorityQueue
先进后出队列
import queue
q = queue.LifoQueue()  # 先进先出队列
q.put('first')
q.put('second')
q.put('thrid')
# q.put_nowait()

print(q.get())
print(q.get())
print(q.get())
print(q.get_nowait())

import queue
q = queue.PriorityQueue()  # 优先级队列(数据以元组的形式比较),按大小,按ASCII码排序.要保证比较的数据类型一致性
q.put((-1, 'a'))
q.put((-5, 'a'))
q.put((-1, 'b'))
q.put(('a', 'bb'))

print(q.get())
print(q.get())
print(q.get())
print(q.get())
队列实例

四、线程池的回调函数

from concurrent.futures import ThreadPoolExecutor

def func(n):
    print('我是%s' %n)
    return n**2

def call_back(res):
    res = res.result() # 需要result获取返回值(进程池则不用)
    print('我是返回值%s' %res)  
tpool = ThreadPoolExecutor(max_workers=5)
for i in range(10):
    tpool.submit(func, i).add_done_callback(call_back)
回调函数

五、线程池

到这里就差我们的线程池没有讲了,我们用一个新的模块给大家讲,早期的时候我们没有线程池,现在python提供了一个新的标准或者说内置的模块,这个模块里面提供了新的线程池和进程池,之前我们说的进程池是在multiprocessing里面的,现在这个在这个新的模块里面,他俩用法上是一样的。

为什么要将进程池和线程池放到一起呢,是为了统一使用方式,使用threadPollExecutor和ProcessPollExecutor的方式一样,而且只要通过这个concurrent.futures导入就可以直接用他们两个了

concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
ProcessPoolExecutor: 进程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
# 线程池的方法:
# # 1、submit(fn,*args,**kwargs) 异步提交任务
#
# # 2、map(func,*iterables,timeout=None,chunksize=1) 启动线程池无返回值
#
# 3、shutdown(wait=True)
# 相当于进程池的pool.close()+pool.join()操作
# wait=True,等待池内所有任务执行完毕回收完资源后才继续
# wait=False,立即返回,并不会等待池内的任务执行完毕
# 但不管wait参数为何值,整个程序都会等到所有任务执行完毕
# submit和map必须在shutdown之前
#
# 4、取得结果:#result(timeout=None)
#
# 5、回调函数: add_done_callback(fn)

# map方法调用线程池
# from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
#
# def func(n):
# print('我是%s哇' %n)
# return n**2
#
# tpool = ThreadPoolExecutor(max_workers = 5)
# res = tpool.map(func, range(10))
# print(res.result()) #没办法拿到返回值
# tpool.shutdown()
# # tpool.map(func, range(10))
# # tpool.shutdown() # 相当于close()+join()
# print('主线程结束')


# submit方法调用线程池
# from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
#
# def func(n):
# print('我是%s哇' %n)
# return n**2
#
# # tpool = ThreadPoolExecutor(max_workers = 5)
# for i in range(10):
# tpool.submit(func,i) # 异步提交
# tpool.shutdown() # (可加可不加)
# print('主线程结束')
# # 为什么这里的'主线程结束' 打印出来后续程序还能执行,因为这句话打印主线程并没有真正的结束,真正的主线程会等着所有线程结束之后再结束


# submit方法调用线程池取返回值的第一种方法
# import time
# from concurrent.futures import ThreadPoolExecutor
#
# def func(n):
# time.sleep(1)
# print('我是%s哇' %n)
# return n**2
#
# tpool = ThreadPoolExecutor(max_workers = 5)
# for i in range(10):
# res = tpool.submit(func,i)
# print('wishing返回值哇', res.result()) # 这里打印返回值会导致主线程for循环这里产生阻塞一个打印出来下一个才会执行
# tpool.shutdown()
# print('主线程结束')



# submit方法调用线程池取返回值的第二种方法
# import time
# from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor
#
# def func(n):
# time.sleep(1)
# print('我是%s哇' %n)
# return n**2
#
# lst_res = []
# tpool = ThreadPoolExecutor(max_workers=5)
#
# for i in range(10):
# res = tpool.submit(func,i)
# lst_res.append(res)
# tpool.shutdown()
# print('主线程结束')
# for r in lst_res: print(r.result()) #一起打印出返回值

六、GIL锁

1.GIL锁(Global Interpreter Lock)

  首先,一些语言(java、c++、c)是支持同一个进程中的多个线程是可以应用多核CPU的,也就是我们会听到的现在4核8核这种多核CPU技术的牛逼之处。那么我们之前说过应用多进程的时候如果有共享数据是不是会出现数据不安全的问题啊,就是多个进程同时一个文件中去抢这个数据,大家都把这个数据改了,但是还没来得及去更新到原来的文件中,就被其他进程也计算了,导致数据不安全的问题啊,所以我们是不是通过加锁可以解决啊,多线程大家想一下是不是一样的,并发执行就是有这个问题。但是python最早期的时候对于多线程也加锁,但是python比较极端的(在当时电脑cpu确实只有1核)加了一个GIL全局解释锁,是解释器级别的,锁的是整个线程,而不是线程里面的某些数据操作,每次只能有一个线程使用cpu,也就说多线程用不了多核,但是他不是python语言的问题,是CPython解释器的特性,如果用Jpython解释器是没有这个问题的,Cpython是默认的,因为速度快,Jpython是java开发的,在Cpython里面就是没办法用多核,这是python的弊病,历史问题,虽然众多python团队的大神在致力于改变这个情况,但是暂没有解决。(这和解释型语言(python,php)和编译型语言有关系吗???待定!,编译型语言一般在编译的过程中就帮你分配好了,解释型要边解释边执行,所以为了防止出现数据不安全的情况加上了这个锁,这是所有解释型语言的弊端??)

  

    但是有了这个锁我们就不能并发了吗?当我们的程序是偏计算的,也就是cpu占用率很高的程序(cpu一直在计算),就不行了,但是如果你的程序是I/O型的(一般你的程序都是这个)(input、访问网址网络延迟、打开/关闭文件读写),在什么情况下用的到高并发呢(金融计算会用到,人工智能(阿尔法狗),但是一般的业务场景用不到,爬网页,多用户网站、聊天软件、处理文件),I/O型的操作很少占用CPU,那么多线程还是可以并发的,因为cpu只是快速的调度线程,而线程里面并没有什么计算,就像一堆的网络请求,我cpu非常快速的一个一个的将你的多线程调度出去,你的线程就去执行I/O操作了

三个需要注意的点:
#1.线程抢的是GIL锁,GIL锁相当于执行权限,拿到执行权限后才能拿到互斥锁Lock,其他线程也可以抢到GIL,但如果发现Lock仍然没有被释放则阻塞,即便是拿到执行权限GIL也要立刻交出来

#2.join是等待所有,即整体串行,而锁只是锁住修改共享数据的部分,即部分串行,要想保证数据安全的根本原理在于让并发变成串行,join与互斥锁都可以实现,毫无疑问,互斥锁的部分串行效率要更高

#3. 一定要看本小节最后的GIL与互斥锁的经典分析
复制代码
复制代码
GIL VS Lock

    机智的同学可能会问到这个问题,就是既然你之前说过了,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock? 

 首先我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

    然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

 最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

过程分析:所有线程抢的是GIL锁,或者说所有线程抢的是执行权限

  线程1抢到GIL锁,拿到执行权限,开始执行,然后加了一把Lock,还没有执行完毕,即线程1还未释放Lock,有可能线程2抢到GIL锁,开始执行,执行过程中发现Lock还没有被线程1释放,于是线程2进入阻塞,被夺走执行权限,有可能线程1拿到GIL,然后正常执行到释放Lock。。。这就导致了串行运行的效果

  既然是串行,那我们执行

  t1.start()

  t1.join

  t2.start()

  t2.join()

  这也是串行执行啊,为何还要加Lock呢,需知join是等待t1所有的代码执行完,相当于锁住了t1的所有代码,而Lock只是锁住一部分操作共享数据的代码。
复制代码

详解:

因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,
此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,
可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了,为了解决类似的问题,python解释器简单粗暴的加了锁,
即当一个线程运行时,其它人都不能动,这样就解决了上述的问题, 这可以说是Python早期版本的遗留问题。

锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:

复制代码
import threading

R=threading.Lock()

R.acquire() #
#R.acquire()如果这里还有一个acquire,你会发现,程序就阻塞在这里了,因为上面的锁已经被拿到了并且还没有释放的情况下,再去拿就阻塞住了 ''' 对公共数据的操作 ''' R.release()
复制代码

 通过上面的代码示例1,我们看到多个线程抢占资源的情况,可以通过加锁来解决,看代码:

 同步锁的引用

 看上面代码的图形解释:

 

分析:
    #1.100个线程去抢GIL锁,即抢执行权限
    #2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
    #3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
    #4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程 
原文地址:https://www.cnblogs.com/z520h123/p/10060329.html