前言
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。
启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行。 虽然python的多线程受GIL限制,并不是真正的多线程,但是对于I/O密集型计算还是能明显提高效率,比如说爬虫。
创建的两种方式
面向过程(常用)
import time, threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,)) # args是你调用函数的参数哦
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
print(balance)
面向对象
手写一个子类,继承父类Thread,重写run方法
# -*-coding:utf-8 -*-
import threading
class MyThread(threading.Thread):
def __init__(self, func, args):
threading.Thread.__init__(self) # 调用父类的构造函数
self.args = args
self.func = func
def run(self): # 线程活动方法
self.func(self.args[0]) # 启动这个函数args是元组,取出指定元素
def count(n):
for i in range(n):
print(n)
t1 = MyThread(count,(10,))
t1.start()
t1.join()
join与start的区别
start() 方法是启动一个子线程,线程名就是我们定义的name
run() 方法并不启动一个新线程,就是在线程中调用了一个普通函数而已。
脏数据
多线程稍不注意就会把数据弄脏,为了避免这种状况,我们需要使用线程锁。
如下:
import threading
# 假定这是你的银行存款:
balance = 0
def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(2000000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
我们先加后减,按理说最后应该输出0,但是其不是。(你可以跑一下试试)
究其原因,是因为修改balance需要多条语句,而执行这几条语句时,线程可能中断,从而导致多个线程把同一个对象的内容改乱了。
如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:
balance = 0
lock = threading.Lock()
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
爬虫与多线程
本人详细说明了爬虫中如何使用多线程,以及相关模板。