Python学习打call第三十九天:线程同步与并发
1.线程之间的几种通信方式
-
Event
:事件; -
Critical Section
:临界区; -
Semaphone
:信号量;
2.Event事件
from threading import Thread, Event
import time
def teacher(event: Event):
print('I am teacher , waiting for your homework')
event.wait()
print("I am teacher, already obtaining student's homework ")
def student(event: Event):
finished_homework = []
while len(finished_homework) < 10:
time.sleep(1)
print('I am student, finished one homework')
finished_homework.append(1)
else:
print('student finish homework')
event.set()
if __name__ == '__main__':
event = Event()
Thread(target=student, args=(event,)).start()
Thread(target=teacher, args=(event,)).start()
- Event是事件处理的机制,全局定义了一个内置标志Flag,如果Flag值为 False,那么当程序执行 event.wait方法时就会阻塞,如果Flag值为True, 那么event.wait 方法时便不再阻塞;
event实例对象的对象方法:
-
wait(self, timeout=None):timeout
为设置等待的时长,如果超过时长(返回值为False)则不再等待,直接向下执行,如果timeout没有指定则一 直等待,等待的时候是阻塞的没有返回值,; -
set()
:如果执行event.set(),将会设置flag为True,那么wait等待的线程就可以向下执行; -
clear()
:如果执行event.clear(),将会设置flag标记为Flase, 那么wait等待的线程将再次等待(阻塞); -
is_set()
:判断event的flag是否为True,如果为True的话wait等待的线程将向下执行;
3.线程锁
- Lock是Python中最底层的同步机制,直接由底层模块 thread 实现,每个lock对象只有两种状态——上锁和未上锁;
- 可以通过下面两种方式创建一个Lock对象,新创建的 Lock 对象处于未上锁的状态:
thread.allocate_lock()
threading.Lock()
-
锁是解决临界区资源的问题,保证每一个线程访问临界资源的时候有全部的权利;
-
一旦某个线程获得锁, 其它试图获取锁的线程将被阻塞;
-
locked()
:判断当前是否上锁,如果上锁,返回True,否则返回False; -
acquire(blocking=True,timeout=-1)
:加锁,默认True为加锁状态(阻塞),False为不阻塞,timeout为设置时长; -
release()
:释放锁,完成任务的时候释放锁,让其他的线程获取到临界资源,注意此时必须处于上锁的状态,如果试图 release() 一个 unlocked 的锁,将抛出异常 thread.error;
# 示例1:学生完成作业的总数最后为1009, 出现了临界资源争抢的问题
import time
from threading import Thread
homework_list = []
def student(number):
while len(homework_list) < number:
time.sleep(0.001)
homework_list.append(1)
print(len(homework_list))
if __name__ == '__main__':
for i in range(10):
Thread(target=student, args=(1000, )).start()
time.sleep(3)
print('完成作业的总数为: {}'.format(len(homework_list)))
# 示例2:对示例1的修正,使用锁机制
import time
import threading
from threading import Thread, Lock
homework_list = []
# 全局阻塞锁
lock = Lock()
def student(number):
while True:
lock.acquire() # 一定是在获取临界资源之前加锁
if len(homework_list) >= number:
break
time.sleep(0.001)
homework_list.append(1)
lock.release() # 完成任务的时候释放锁,让其他的线程获取到临界资源
print('current_thread={}, homework_list={}'.format(threading.current_thread().name, len(homework_list)))
if __name__ == '__main__':
for i in range(10):
Thread(target=student, name='student {}'.format(i), args=(1000, )).start()
time.sleep(3)
print('完成作业的总数为: {}'.format(len(homework_list)))
4.锁在with语句中的使用
- 使用with语句加锁
with lock
,会默认自动释放锁,不需要再写release()方法来释放锁了,可以避免程序中忘记释放锁;
import time
import threading
from threading import Thread, Lock
homework_list = []
# 全局阻塞锁
lock = Lock()
def student(number):
while True:
with lock:
if len(homework_list) >= number:
break
time.sleep(0.001)
homework_list.append(1)
print('current_thread={}, homework_list={}'.format(threading.current_thread().name, len(homework_list)))
if __name__ == '__main__':
for i in range(10):
Thread(target=student, name='student {}'.format(i), args=(1000, )).start()
time.sleep(3)
print('完成作业的总数为: {}'.format(len(homework_list)))
5.线程池
-
ThreadPoolExecutor
:构造实例的时候,传入max_workers参数来设置线程池中最多能同时运行的线程数目;
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch_url(url):
result = requests.get(url=url, )
return result.text
# 创建10个线程队列的线程池
pool = ThreadPoolExecutor(10)
# 获取任务返回对象
a = pool.submit(fetch_url, 'http://www.baidu.com')
# 取出返回的结果
x = a.result()
print(x) # 获得百度的源码
-
submit(self, fn, *args, **kwargs)
:提交线程需要执行的任务(函数名和参数)到线程池中,并返回该任务的句柄,用于提交单个任务; -
map(self, fn, *iterables, timeout=None, chunksize=1)
:类似高阶函数map,可以提交任务,且传递一个可迭代对象,返回任务处理迭代对象的结果;
6.全局解释器锁
-
尽管Python完全支持多线程编程, 但是解释器的C语言实现部分在完全并行执行时并不是线程安全的,实际上,解释器被一个全局解释器锁保护着 ,它确保任何时候都只有一个Python线程执行;
-
GIL最大的问题就是Python的多线程程序并不能利用多核CPU的优势, 就是因为GIL的存在,使得一个进程的多个线程在执行任务的时候,一个CPU时 间片内,只有一个线程能够调度到CPU上运行;
-
因此CPU密集型的程序一般不会使用Python实现,可以选择Java,GO等语言;
-
但是对于非CPU密集型程序,例如IO密集型程序,多数时间都是对网络IO的等待,因此Python的多线程完全可以胜任;
对于全局解释器锁的解决方案:
-
使用multiprocessing创建进程池对象,实现多进程并发,这样就能够使用多CPU计算资源;
-
使用C语言扩展,将计算密集型任务转移给C语言实现去处理,在C代码实现部分可以释放GIL;
多线程和多进程解决方案:
- 如果想要同时使用多线程和多进程,最好在程序启动时,创建任何线程之前,先创建一个单例的进程池, 然后线程使用同样的进程池来进行它们的计 算密集型工作,这样类似于线程调用了进程,完成了CPU密集型任务,进程也利用了多CPU的优势;