并发编程之多线程

并发编程之多线程

一 同步锁

1 两个注意点

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

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

2 验证GIL锁的存在

	启动跟自己机器cpu相同的进程和线程数,多进程可以最大化利用cpu,多线程无法有效利用多核cpu。
from threading import Thread
from multiprocessing import Process
def task():
    while True:
        pass
if __name__ == '__main__':
    for i in range(4):  # 启动跟自己机器cpu相同的进程和线程数,验证GIL的存在
        # t = Thread(target=task)  # Pycharm进程CPU占用约35%
        t = Process(target=task)  # Pycharm进程CPU占用约90%
        t.start()

3 GIL与普通互斥锁的区别

3.1 前提:为何要使用锁

	锁的目的是保护共享的数据,同一时间只能有一个线程来修改共享的数据。保护不同的数据要使用不同的锁。

3.2 GIL与普通互斥锁的区别

	GIL锁保护解释器级别的数据安全(如垃圾回收的数据) ,不保护应用程序级别的数据;

	应用程序的数据需要开发者自行加锁保护,如进程、线程内的共享数据。	
GIL锁全套
###### GIL锁全套:
# 什么是GIL锁?
	GIL锁是全局解释器锁
    
# 为何要有GIL锁?
	为了实现python的垃圾回收机制,才有了GIL锁;
	python解释型语言,需要做垃圾回收,数据进程内共享,多线程下有的线程没有操作变量,有的有,为防止垃圾回收出错,所以作者想了一个办法,垃圾回收的时候,变量只在一个线程使用,其他线程都不要使用,然后来决定是否回收。   
    编译型语言没有这个问题,要先编译,然后再执行,可以监控到变量是否在使用,不存在GIL锁的问题。  
    
# 为何加互斥锁?GIL与普通互斥锁的区别?
	# 首先,GIL锁与互斥锁,控制级别不一样	
    GIL锁有了以后,同一时刻同一进程内,只有一个线程执行。如定义a=0 多个线程对a进行累加,并发执行,数据会乱【为何会乱?线程1拿到a=0,遇到IO或者时间片调度;切换到线程2去使用,拿到了a,进行a+1,遇到IO;线程3拿到了a,进行a+1,遇到IO,切换出来执行,最后还是a+1=0+1,最后都是1】,加互斥锁,是因为GIL锁无法控制用户级别的线程安全。

3.3 代码示例:

	不加互斥锁,多个线程同时修改同一个数据,无法保证数据的安全。
from threading import Thread, Lock
import time

mutex = Lock()
money = 100
def task():
    global money
    # mutex.acquire()
    temp = money
    time.sleep(1)
    money = temp - 1
    # mutex.release()
if __name__ == '__main__':
    ll = []
    for i in range(10):
        t = Thread(target=task,)
        t.start()
        # t.join()  # 会怎么样?变成了串行,不能这么做
        ll.append(t)
    for t in ll:
        t.join()
    print(money)

4 IO密集型与计算密集型

	其它语言中,不允许开启多进程,都是开启多线程完成并发,而使用cpython解释器的python中,由于GIL锁的存在,一个进程同时只有一个线程,所以针对IO密集操作和计算密集操作有不同的方法:	# 计算密集型:开多进程	# I/O密集型:开多线程
from threading import Threadfrom multiprocessing import Processimport time# 计算密集型def task():    count = 0    for i in range(100000000):        count += 1if __name__ == '__main__':    ctime = time.time()    ll = []    for i in range(10):        # t = Thread(target=task,)  # 开线程:55.27706050872803        t = Process(target=task, )  # 开进程:16.265764474868774        t.start()        ll.append(t)    for i in ll:        i.join()    print(time.time()-ctime)
# IO密集型def task():    time.sleep(2)if __name__ == '__main__':    ctime = time.time()    ll = []    for i in range(400):        # t = Thread(target=task,)  # 开线程:2.076205253601074        t = Process(target=task,)  # 开进程:10.390235424041748        t.start()        ll.append(t)    for t in ll:        t.join()    print(time.time()-ctime)

二 死锁现象与递归锁

1 什么是死锁现象

	指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

2 如下就是死锁:

# 死锁现象,张三拿到了A锁,等B锁,李四拿到了B锁,等A锁from threading import Thread,Lockimport timemutexA = Lock()mutexB = Lock()def eat_apple(name):    mutexA.acquire()    print('%s 获得了a锁' % name)    mutexB.acquire()    print('%s 获得了b锁' % name)    print('开始吃苹果,并且吃完了')    mutexB.release()    print('%s 释放了b锁' % name)    mutexA.release()    print('%s 释放了a锁' % name)def eat_watermelon(name):    mutexB.acquire()    print('%s 获得了b锁' % name)    time.sleep(2)    mutexA.acquire()    print('%s 获得了a锁' % name)    print('开始吃瓜,并且吃完了')    mutexA.release()    print('%s 释放了a锁' % name)    mutexB.release()    print('%s 释放了b锁' % name)if __name__ == '__main__':    ll = ['lxx', 'alex', 'youngboy']    for name in ll:        t1 = Thread(target=eat_apple,args=(name,))        t2 = Thread(target=eat_watermelon,args=(name,))        t1.start()        t2.start()

3 什么是递归锁

	为了解决死锁问题,python提供了可重入锁RLock。这个RLock内部维护者一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

4 递归锁解决死锁示例:

# 解决死锁现象,使用递归锁from threading import Thread,RLockimport timemutexA = mutexB = RLock()def eat_apple(name):    mutexA.acquire()    print('%s 获得了a锁' % name)    mutexB.acquire()    print('%s 获得了b锁' % name)    print('开始吃苹果,并且吃完了')    mutexB.release()    print('%s 释放了b锁' % name)    mutexA.release()    print('%s 释放了a锁' % name)def eat_watermelon(name):    mutexB.acquire()    print('%s 获得了b锁' % name)    time.sleep(2)    mutexA.acquire()    print('%s 获得了a锁' % name)    print('开始吃瓜,并且吃完了')    mutexA.release()    print('%s 释放了a锁' % name)    mutexB.release()    print('%s 释放了b锁' % name)if __name__ == '__main__':    ll = ['lxx', 'alex', 'youngboy']    for name in ll:        t1 = Thread(target=eat_apple,args=(name,))        t2 = Thread(target=eat_watermelon,args=(name,))        t1.start()        t2.start()

三 信号量Semaphore

1 信号量Semaphore是什么

	Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release()时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

2 信号量与进程池的区别

	信号量与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾只有者四个进程,不会产生新的,而信号量是产生一堆线程/进程。

3 代码示例:

# (同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):# Semaphore:信号量可以理解为多把锁,同时允许多个线程来更改数据from threading import Thread,Semaphoreimport timeimport randomsm = Semaphore(5)  # 数字表示可以同时有多少个线程操作def task(name):    sm.acquire()    print('%s is in wc' % name)    time.sleep(random.randint(1,5))    sm.release()if __name__ == '__main__':    for i in range(15):        t = Thread(target=task, args=('lxx %s 号'%i,))        t.start()

四 Event事件

1 什么是Event事件

	一些线程需要等到其他线程执行完成之后才能执行,类似于发射信号。比如一个线程等待另一个线程执行结束之后再继续执行。这种情况下需要用到Event事件来实现。

2 Event事件的用法

1) event.isSet(): 返回event的状态值;2) event.wait(): 如果 event.isSet()==False将阻塞线程;3) event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态,等待操作系统调度;4) event.clear(): 恢复event的状态值为False。

3 代码示例:

# 通过event事件,实现两个线程,一个线程读文件前一半,写入另一个文件,另一个线程读后一半写入文件from threading import Thread, Eventimport osevent = Event()  # 获取文件的大小size = os.path.getsize('zhuxian.txt')def read_first():    with open('zhuxian.txt', 'r', encoding='utf-8') as f:        n = size // 2  # 取文件一半,整除        data = f.read(n)        with open('aaa.txt', 'w', encoding='utf-8') as f1:            f1.write(data)        print('我读完一半了,发了个信号')        event.set()def read_last():    event.wait()    with open('zhuxian.txt', 'r', encoding='utf-8') as f:        n = size // 2  # 取文件一半,整除        f.seek(n, 0)  # 光标从文件开头开始,移动了n个字节,移动到文件一半        data = f.read()        with open('aaa.txt', mode='at', encoding='utf-8') as f1:            f1.write(data)if __name__ == '__main__':    t1 = Thread(target=read_first)    t1.start()    t2 = Thread(target=read_last)    t2.start()

五 线程Queue

1 为什么要使用线程的Queue

	线程间通信,因为共享变量会出现数据不安全问题,用线程queue通信,不需要加锁,内部自带。

2 线程的Queue用法

# 进程的Queue队列from multiprocessing import Queue# 线程的Queue队列from queue import Queue,LifoQueue,PriorityQueue
# 三种线程方法之Queue :队列,先进先出from queue import Queue, PriorityQueue, LifoQueueq = Queue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']for i in ll:    q.put(i)# q.put('abp')  # 超过队列的最大值,会卡住# q.put_nowait('snis')  # queue.Full# 取值for i in range(len(ll)):    print(q.get())# print(q.get())  # 队列没有值的时候,再取,会卡住# print(q.get_nowait())  # _queue.Empty
# 三种线程方法之LifoQueue: 堆栈,后进先出q = LifoQueue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']for i in ll:    q.put(i)for i in range(len(ll)):    print(q.get())
# 三种线程方法之PriorityQueue:优先级队列,谁小谁先出q = PriorityQueue(5)ll = ['lxx', 'alex', 'blex', 'clex', 'dlex']lxx = [-10, 100, 99, 88, 102]count = 0for i in ll:    q.put((lxx[count], i))  # 存值的时候要用元组    count += 1for i in range(len(ll)):    print(q.get())  # 先打印出优先级数字小的
# 其他用法,同线程Queue

六 线程池和进程池的shutdown

# 主线程等待所有任务完成from concurrent.futures import ThreadPoolExecutorimport timepool = ThreadPoolExecutor(3)def task(name):    print('%s 开始' % name)    time.sleep(1)    print('%s 结束' % name)if __name__ == '__main__':    for i in range(20):        pool.submit(task, '沙雕%s' % i)    pool.shutdown(wait=True)  # 放到for循环外面,等待所有任务完成,并且把池关闭    # pool.submit(task, 'qqqqq')  # 关闭进程池之后不能再提交任务了 RuntimeError: cannot schedule new futures after shutdown    print('>>>main<<<')

七 定时器了解

# 控制多长时间后执行一个任务from threading import Timerdef task(name):    print('汤姆大战杰瑞————%s' % name)if __name__ == '__main__':    # t = Timer(2, task, args=('lxx',))  # 本质是开一个线程,延迟两秒执行    t = Timer(2, task, kwargs={'name': 'lxx'})  # 本质是开一个线程,延迟两秒执行    t.start()

八 Python标准模块--concurrent.futures

1 基本使用

concurrent.futures模块提供了高度封装的异步调用接口;ThreadPoolExecutor: 线程池。提供异步调用;ProcessPoolExecutor: 进程池,提供异步调用;
# 基本方法1) submit(fn, *args, **kwargs)  # 异步提交任务2) map(func, *iterables, timeout=None, chunksize=1)  # 取代for循环submit的操作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)  # 回调函数

2 线程池

2.1 为什么要有线程池?

	不管是开进程和开线程,不能无限制开,通过池,假设池子里就有10个位子,不管怎么开,永远是这10个。

2.2 如何使用

from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutorfrom threading import Threadimport time, randompool = ThreadPoolExecutor(5)  # 数字是池的大小# pool = ProcessPoolExecutor(5)  # 数字是池的大小def task(name):    print('%s 任务开始' % name)    time.sleep(random.randint(1, 4))    print('任务结束')    return '%s 返回了' % namedef call_back(f):    print(f.result())if __name__ == '__main__':    # 基础版本    # ll = []    # for i in range(100):    #     res = pool.submit(task, '沙雕 %s 号' % i)  # 不需要再写在args中了    #     # res 是Feature对象    #     # from concurrent.futures._base import Future    #     # print(type(res))  # <class 'concurrent.futures._base.Future'>    #     # print(res.result())  # 像join,只要执行result,就会等着结果回来,就变成串行了    #     ll.append(res)    # for res in ll:    #     print(res.result())    # 终极版本    for i in range(100):        pool.submit(task,'沙雕 %s 号'%i).add_done_callback(call_back)  # 向线程池中提交一个任务,等任务执行完成,自动回到到call_back函数执行

3 进程池

# 1 如何使用from concurrent.futures import ProcessPoolExecutorpool = ProcessPoolExecutor(2)pool.submit(get_pages, url).add_done_callback(call_back)
posted @ 2021-06-25 15:12  越关山  阅读(59)  评论(0)    收藏  举报