python并发编程调优_Python并发编程-线程

Python中的全局解释器锁(GIL)使得多线程无法充分利用多核CPU,适合于IO密集型任务而非CPU密集型。在Python中,可以使用多进程实现并发。本文介绍了信号量、互斥锁、递归锁、条件变量、事件和队列等同步机制,并通过实例展示了如何避免死锁。线程池则提供了线程复用,降低了创建和销毁线程的开销。
摘要由CSDN通过智能技术生成

Python作为一种解释型语言,由于使用了全局解释锁(GIL)的原因,其代码不能同时在多核CPU上并发的运行。这也导致在Python中使用多线程编程并不能实现并发,我们得使用其他的方法在Python中实现并发编程。

一、全局解释锁(GIL)

Python中不能通过使用多线程实现并发编程主要是因为全局解释锁的机制,所以首先解释一下全局解释锁的概念。

首先,我们知道C++和Java是编译型语言,而Python则是一种解释型语言。对于Python程序来说,它是直接被输入到解释器中直接运行的。解释器在程序执行之前对其并不了解;它所知道的只是Python的规则,以及在执行过程中怎样去动态的应用这些规则。它也有一些优化,但是这基本上只是另一个级别的优化。由于解释器没法很好的对程序进行推导,Python的大部分优化其实是解释器自身的优化。更快的解释器自然意味着程序的运行也能“免费”的更快。也就是说,解释器优化后,Python程序不用做修改就可以享受优化后的好处。

为了利用多核系统,Python必须支持多线程运行。但作为解释型语言,Python的解释器需要做到既安全又高效。解释器要注意避免在不同的线程操作内部共享的数据,同时还要保证在管理用户线程时保证总是有最大化的计算资源。为了保证不同线程同时访问数据时的安全性,Python使用了全局解释器锁(GIL)的机制。从名字上我们很容易明白,它是一个加在解释器上的全局(从解释器的角度看)锁(从互斥或者类似角度看)。这种方式当然很安全,但它也意味着:对于任何Python程序,不管有多少的处理器,任何时候都总是只有一个线程在执行。即:只有获得了全局解释器锁的线程才能操作Python对象或者调用Python/C API函数。

所以,在Python中”不要使用多线程,请使用多进程”。具体来说,如果你的代码是IO密集型的,使用多线程或者多进程都是可以的,多进程比线程更易用,但是会消耗更多的内存;如果你的代码是CPU密集型的,多进程(multiprocessing模块)就明显是更好的选择——特别是所使用的机器是多核或多CPU的时候。

另外,Python的官方实现CPython带有GIL,但并不是所有的Python实现版本都是这样的。IronPython,Jython,还有使用.NET框架实现的Python就没有GIL。所以如果你不能忍受GIL,也可以尝试用一下其他实现版本的Python。

ae2cb5b0fa4ecb7cac39174017709423.png

如果是一个计算型的任务,GIL就会让多线程变慢。我们举个计算斐波那契数列的例子:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importtimeimportthreadingdeftext(name):defprofile(func):def wrapper(*args,**kwargs):

start=time.time()

res= func(*args,**kwargs)

end=time.time()print('{} cost:{}'.format(name,end-start))returnresreturnwrapperreturnprofiledeffib(n):if n <= 2:return 1

return fib(n-1) + fib(n-2)

@text('nothread')defnothread():

fib(35)

fib(35)

@text('hasthread')defhasthread():for i in range(2):

t= threading.Thread(target=fib,args=(35,))

t.start()

main_thread=threading.current_thread()for t inthreading.enumerate():if t ismain_thread:continuet.join()

nothread()

hasthread()##输出结果###

nothread cost:6.141353607177734hasthread cost:6.15336275100708

View Code

这种情况还不如不用多线程!

GIL是必须的,这是Python设计的问题:Python解释器是非线程安全的。这意味着当从线程内尝试安全的访问Python对象的时候将有一个全局的强制锁。 在任何时候,仅仅一个单一的线程能够获取Python对象或者C API。每100个字节的Python指令解释器将重新获取锁,这(潜在的)阻塞了I/O操作。因为锁,CPU密集型的代码使用线程库时,不会获得性能的提高。

那是不是由于GIL的存在,多线程库就是个「鸡肋」呢?当然不是。事实上我们平时会接触非常多的和网络通信或者数据输入/输出相关的程序,比如网络爬虫、文本处理等等。这时候由于网络情况和I/O的性能的限制,Python解释器会等待读写数据的函数调用返回,这个时候就可以利用多线程库提高并发效率了。

2.同步机制

A. Semaphore(信号量)

在多线程编程中,为了防止不同的线程同时对一个公用的资源(比如全部变量)进行修改,需要进行同时访问的数量(通常是1)的限制。信号量同步基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1.当计数器为0时,acquire()调用被阻塞。

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importtimefrom random importrandomfrom threading importThread,Semaphore,current_thread,enumerate

sema= Semaphore(3)deffoo(tid):

with sema:print('{} acquire sema'.format(tid))

wt= random() * 2time.sleep(wt)print('{} release sema'.format(tid))for i in range(5):

t= Thread(target=foo,args=(i,))

t.start()

main_thread=current_thread()for t inenumerate():if t ismain_thread:continuet.join()####输出结果#####

0 acquire sema1acquire sema2acquire sema

0 release sema3acquire sema1release sema4acquire sema2release sema3release sema4 release sema

View Code

B. Lock(互斥锁)

Lock也可以叫做互斥锁,其实相当于信号量为1。我们先看一个不加锁的例子:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importtimeimportthreading

value=0defgetlock():globalvalue

new= value + 1time.sleep(0.001) #让线程有机会切换

value =newfor i in range(100):

t= threading.Thread(target=getlock)

t.start()

main_thread=threading.current_thread()for t inthreading.enumerate():if t ==main_thread:continuet.join()print(value)####输出结果#####

不确定(刷新值会发生改变)

View Code

现在,我们来看看加锁之后的情况:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importtimeimportthreading

value=0

lock=threading.Lock()defgetlock():globalvalue

with lock:

new= value + 1time.sleep(0.001) #让线程有机会切换

value =newfor i in range(100):

t= threading.Thread(target=getlock)

t.start()

main_thread=threading.current_thread()for t inthreading.enumerate():if t ==main_thread:continuet.join()print(value)####输出结果为#############

100

View Code

我们对value的自增加了锁,就可以保证了结果了。

3. RLock(递归锁)

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

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importthreadingimporttime

mutexA=threading.Lock()

mutexB=threading.Lock()classMyThread(threading.Thread):def __init__(self):

threading.Thread.__init__(self)defrun(self):

self.fun1()

self.fun2()deffun1(self):

mutexA.acquire()#如果锁被占用,则阻塞在这里,等待锁的释放

print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time()))

mutexB.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time()))

mutexB.release()

mutexA.release()deffun2(self):

mutexB.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time()))

time.sleep(0.2)

mutexA.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time()))

mutexA.release()

mutexB.release()if __name__ == "__main__":print("start---------------------------%s"%time.time())for i in range(0, 10):

my_thread=MyThread()

my_thread.start()

产生死锁

解决方案:

8f900a89c6347c561fdf2122f13be562.png

961ddebeb323a10fe0623af514929fc1.png

importthreadingimporttime

mutex=threading.RLock()classMyThread(threading.Thread):def __init__(self):

threading.Thread.__init__(self)defrun(self):

self.fun1()

self.fun2()deffun1(self):

mutex.acquire()#如果锁被占用,则阻塞在这里,等待锁的释放

print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time()))

mutex.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time()))

mutex.release()

mutex.release()deffun2(self):

mutex.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time()))

time.sleep(0.2)

mutex.acquire()print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time()))

mutex.release()

mutex.release()if __name__ == "__main__":print("start---------------------------%s"%time.time())for i in range(0, 10):

my_thread=MyThread()

my_thread.start()

View Code

递归锁内部维护了一个计数器,当有线程拿到了Lock以后,这个计数器会自动加1,只要这计数器的值大于0,那么其他线程就不能抢到改锁,这就保证了,在同一时刻,仅有一个线程使用该锁,从而避免了死锁的方法。关于递归锁内部实现,有兴趣的可以看看源码。

4. Condition(条件)

一个线程等待特定条件,而另一个线程发出特定条件满足的信号。最好说明的例子就是「生产者/消费者」模型:

importtimeimportthreadingdefconsumer(cond):

t=threading.current_thread()

with cond:

cond.wait()#创建了一个锁,等待producer解锁

print('{}: Resource is available to consumer'.format(t.name))defproducer(cond):

t=threading.current_thread()

with cond:print('{}:Making resource available'.format(t.name))

cond.notifyAll()#释放锁,唤醒消费者

condition=threading.Condition()

c1= threading.Thread(name='c1',target=consumer,args=(condition,))

p= threading.Thread(name='p',target=producer,args=(condition,))

c2= threading.Thread(name='c2',target=consumer,args=(condition,))

c1.start()

time.sleep(1)

c2.start()

time.sleep(1)

p.start()

5. Event

一个线程发送/传递事件,另外的线程等待事件的触发。我们同样的用「生产者/消费者」模型的例子:

importtimeimportthreadingfrom random importrandint

TIMEOUT= 2

defconsumer(event, l):

t=threading.currentThread()while 1:

event_is_set=event.wait(TIMEOUT)ifevent_is_set:try:

integer=l.pop()print('{} popped from list by {}'.format(integer,t.name))

event.clear()#重置状态

exceptIndexError:pass

defproducer(event, l):

t=threading.currentThread()while 1:

integer= randint(10,100)

l.append(integer)print('{} append to list by {}'.format(integer, t.name))

event.set()

time.sleep(1)

event=threading.Event()

l=[]

threads=[]

p= threading.Thread(name='producer1', target=producer, args=(event, l))

p.start()

threads.append(p)for name in ('consumer1','consumer2'):

t= threading.Thread(target=consumer, name=name, args=(event, l))

t.start()

threads.append(t)for t inthreads:

t.join()print('ending')

可以看到事件被2个消费者比较平均的接收并处理了。如果使用了wait方法,线程就会等待我们设置事件,这也有助于保证任务的完成。

6. Queue

队列在并发开发中最常用的。我们借助「生产者/消费者」模式来理解:生产者把生产的「消息」放入队列,消费者从这个队列中对去对应的消息执行。

大家主要关心如下4个方法就好了:

put: 向队列中添加一个消息。

get: 从队列中删除并返回一个消息。

task_done: 当某一项任务完成时调用。

join: 阻塞直到所有的项目都被处理完。

importtimeimportthreadingimportrandomimportqueue

q=queue.Queue()defdouble(n):return n*2

defproducer():while 1:

wt= random.randint(1,10)

time.sleep(random.random())

q.put((double, wt))defconsumer():while 1:

task, arg=q.get()print(arg, task(arg))

q.task_done()for target in(producer, consumer):

t= threading.Thread(target=target)

t.start()

Queue模块还自带了PriorityQueue(带有优先级)和LifoQueue(先进先出)2种特殊队列。我们这里展示下线程安全的优先级队列的用法,

PriorityQueue要求我们put的数据的格式是(priority_number, data),我们看看下面的例子:

importtimeimportthreadingfrom random importrandintimportqueue

q=queue.PriorityQueue()defdouble(n):return n * 2

defproducer():

count=0while 1:if count > 5:breakprit= randint(0,100)print("put :{}".format(prit))

q.put((prit, double, prit))#(优先级,函数,参数)

count += 1

defconsumer():while 1:ifq.empty():breakpri,task,arg=q.get()print('[PRI:{}] {} * 2 = {}'.format(pri,arg,task(arg)))

q.task_done()

time.sleep(0.1)

t= threading.Thread(target=producer)

t.start()

time.sleep(1)

t= threading.Thread(target=consumer)

t.start()

7.线程池

面向对象开发中,大家知道创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。无节制的创建和销毁线程是一种极大的浪费。那我们可不可以把执行完任务的线程不销毁而重复利用呢?仿佛就是把这些线程放进一个池子,一方面我们可以控制同时工作的线程数量,一方面也避免了创建和销毁产生的开销。

importtimeimportthreadingfrom random importrandomimportqueuedefdouble(n):return n * 2

classWorker(threading.Thread):def __init__(self, queue):

super(Worker, self).__init__()

self._q=queue

self.daemon=True

self.start()defrun(self):while 1:

f, args, kwargs=self._q.get()try:print('USE:{}'.format(self.name))print(f(*args, **kwargs))exceptException as e:print(e)

self._q.task_done()classThreadPool(object):def __init__(self, max_num=5):

self._q=queue.Queue(max_num)for _ inrange(max_num):

Worker(self._q)#create worker thread

def add_task(self, f, *args, **kwargs):

self._q.put((f, args, kwargs))defwait_compelete(self):

self._q.join()

pool=ThreadPool()for _ in range(8):

wt=random()

pool.add_task(double, wt)

time.sleep(wt)

pool.wait_compelete()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值