多线程和GIL
多线程类似于同时执行多个不同程序,多线程运行有一下优点:
- 可以把运行时间长的任务放到后台处理
- 用户界面可以更加吸引人,比如用户点击一个按钮触发某些事件处理,可以弹出一个进度条来显示处理进度
- 程序运行可以更快
- 在一些需要等待的任务实现上,如用户输入、文件读写和网络收发数据等,线程比较有用了,在这样的情况下就可以释放一些的珍贵的资源,比如内存占用等。
Python的标准库提供了两个模块,thread和threading,thread是低级模块,threading是高级模块,对thread进行封装,大多数情况下,我们只需要使用threading这个高级模块。
1.用threading模块创建多线程
threading模块一般通过两种方式创建多线程,第一种方式把一个函数传入并创建Thread实例,然后调用start()方法开始执行, 第二种方式直接从threading.Thread继承并创建线程类,然后重写__init__方法和run()方法
第一种方法:
import random
import time, threading
# 第一种方法
def thread_run(urls):
print('Current {} is running'.format(threading.current_thread().name))
for url in urls:
print('{}---------------------->{}'.format(threading.current_thread().name, url))
time.sleep(random.random())
print('{} end.....'.format(threading.current_thread().name))
if __name__ == '__main__':
print('start run :{}'.format(threading.current_thread().name))
t1 = threading.Thread(target=thread_run, args=(('url1', 'url2', 'url3'),), name='Thread1')
t2 = threading.Thread(target=thread_run, args=(('url4', 'url5', 'url6'),), name='Thread2')
t1.start()
t2.start()
t1.join()
t2.join()
print('all {} end'.format(threading.current_thread().name))
返回:
第二种方法:
import threading, time, random
# 第二种方法
class myThread(threading.Thread):
def __init__(self, name, urls):
threading.Thread.__init__(self, name=name)
self.urls = urls
def run(self):
print('Current {} is running'.format(threading.current_thread().name))
for url in self.urls:
print('{}---------------------->{}'.format(threading.current_thread().name, url))
time.sleep(random.random())
print('{} end.....'.format(threading.current_thread().name))
if __name__ == '__main__':
print('start run :{}'.format(threading.current_thread().name))
t1 = myThread(name='Thread1', urls=['url1', 'url2', 'url3'])
t2 = myThread(name='Thread2', urls=['url4', 'url5', 'url6'])
t1.start()
t2.start()
t1.join()
t2.join()
print('all {} end'.format(threading.current_thread().name))
返回
2.线程同步
如果多线程共同对某一个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步,使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire 和release方法,对于每次只允许一个线程操作的数据,可以将其操作到acquire 和release方法之间。
对于Lock对象,如果一个线程连续两次进行acquire操作,那么由于第一次acquire之后没有release,第二次acquire将挂起线程,这会导致Lock对象永远不会release,使得线程死锁。 Rlock对象允许一个线程多次对acquire操作,因为在其内部通过一个counter变量维护着线程acquire的次数,而且每一次的acquire操作必须有一个release操作与之对应,在所有的release操作完成之后,别的线程才能申请该Rlock对象,例如:
# 线程同步
import threading
mylock = threading.RLock()
num = 0
class myThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self, name=name)
def run(self):
global num
while True:
mylock.acquire()
print('{} locked.Number:{}'.format(threading.current_thread().name, num))
if num > 4:
mylock.release()
print('release thread_name:{},num:{}'.format(threading.current_thread().name, num))
break
num += 1
print('release thread_name:{},num:{}'.format(threading.current_thread().name, num))
mylock.release()
if __name__ == '__main__':
t1 = myThread('thread1')
t2 = myThread('thread2')
t1.start()
t2.start()
返回;
3.全局解释器锁(GIL)
在Python的原始解释器cpython中存在着GIL(Global Interpreter Lock,全局解释器锁),因此在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数到达一定数目时才会释放FIL。由于全局解释器锁的存在,在进行多线程操作的时候,不能调用多个CPU内核,只能利用一个内核,所以子啊进行CPU密集型操作的时候,不推荐使用多线程,更加倾向于多进程,那么多线程适合什么样的应用场景呢?对于IO密集型操作,多线程可以明显提高效率,例如Python爬虫的开发,绝大多数时间爬虫实在等待socket返回数据,网络IO的操作延时比CPU大得多。