主要介绍使用 threading
模块创建线程的 3 种方式,分别为:
- 创建
Thread
实例函数 - 创建
Thread
实例可调用的类对象 - 使用
Thread
派生子类的方式
多线程是提高效率的一种有效方式,但是由于 CPython
解释器中存在 GIL
锁,因此 CPython
中的多线程只能使用单核。也就是说 Python
的多线程是宏观的多线程,而微观上实际依旧是单线程。
线程和进程之间有很多相似的地方,它们都是一个独立的任务。但是相比进程,线程要小的多。我们运行线程需要在进程中进行,而且线程和线程之间是共享内存的。相比进程的数据隔离,线程的安全性要更差一些。
1. Thread 实例函数
使用 threading
模块创建一个 Thread
的实例,传递给它一个函数。
import threading
import time
loops = [4, 2]
def loop(nloop, nsec):
print 'start loop', nloop, 'at:', time.ctime()
time.sleep(nsec)
print 'end loop', nloop, 'at:', time.ctime()
def main():
print 'start main at:', time.ctime()
threads = []
nloops = range(len(loops))
# 实例化Thread即调用Thread()与调用start_new_thread()最大区别是:新的线程不会立即开始
for i in nloops:
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
# t.daemon = True python主程序只有在没有非守护线程的时候才会退出,设置# 线程是否随主线程退出而退出,默认为False
# 所有线程创建之后,一起调用start()函数启动,而不是创建一个调用一个
for i in nloops:
threads[i].start()
# join()会等到线程结束,或者在给了timeout参数的时候,等到超时为止
# join() 的作用是让主线程等待直到该线程执行完
for i in nloops:
threads[i].join()
print 'end main at:', time.ctime()
if __name__ == "__main__":
main()
代码输出如下:
'''
start main at: Sat Jul 21 22:27:35 2018
start loop 0 at: Sat Jul 21 22:27:35 2018
start loop 1 at: Sat Jul 21 22:27:35 2018
end loop 1 at: Sat Jul 21 22:27:37 2018
end loop 0 at: Sat Jul 21 22:27:39 2018
end main at: Sat Jul 21 22:27:39 2018
'''
常用线程方法:
# 如上所述,创建一个线程
t=Thread(target=func)
# 启动子线程
t.start()
# 阻塞子线程,待子线程结束后,再往下执行
t.join()
# 判断线程是否在执行状态,在执行返回True,否则返回False
t.is_alive()
t.isAlive()
# 设置线程是否随主线程退出而退出,默认为False
t.daemon = True
t.daemon = False
# 设置线程名
t.name = "My-Thread"
2. Thread 实例可调用的类对象
创建一个 Thread
的实例,传递给它一个可调用的类对象
import threading
import time
loops = [4, 2]
class ThreadFun(object):
def __init__(self, func, args, name=''):
self.name = name
self.func = func
self.args = args
def __call__(self):
apply(self.func, self.args)
def loop(nloop, nsec):
print 'start loop', nloop, 'at:', time.ctime()
time.sleep(nsec)
print 'end loop', nloop, 'at:', time.ctime()
def main():
print 'main is start at:', time.ctime()
threads = []
nloops = range(len(loops))
for i in nloops:
t = threading.Thread(target=ThreadFun(loop, (i, loops[i]), loop.__name__))
# 该类在调用函数方面更加通用,并不局限于loop()函数
threads.append(t)
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
print 'main is end at:', time.ctime()
if __name__ == "__main__":
main()
3. Thread 派生子类
除了用函数的方式,我们还可以用面向对象的方式来创建线程。这就需要我们手动继承 Thread
类,而且还需要实现其中的 run
方法,代码如下:
import time
from threading import Thread
class MyThread(Thread):
def __init__(self):
super().__init__()
def run(self):
time.sleep(1)
print("我在运行")
t = MyThread()
t.start()
print("我是主线程")
从 Thread
派生出一个子类,创建一个这个子类的实例
import threading
import time
loops = [4, 2]
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
# super().__init__(name=name) # # 线程的名字
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
# run 等同于之前 target 指定的函数
def run(self):
apply(self.func, self.args)
def test(self):
print("this is test")
def loop(nloop, nsec):
print 'start loop', nloop, 'at:', time.ctime()
time.sleep(nsec)
print 'end loop', nloop, 'at:', time.ctime()
def main():
print 'main is start at:', time.ctime()
threads = []
nloops = range(len(loops))
for i in nloops:
# 类似于 Thread(target=函数名) , 只会创建出一个线程
t = MyThread(loop, [i, loops[i]], loop.__name__)
threads.append(t)
# MyThread 类中没有 start 方法,继承的父类 调用 start 方法,会自动调用 run 方法
for i in nloops:
threads[i].start()
for i in nloops:
threads[i].join()
print 'main is end at:', time.ctime()
t.test() # 这种方式不是多线程的方式!!!要在 run 方法里面调用 test 方法,才是多任务的方式
if __name__ == "__main__":
main()
大家在用面向对象的方式,要注意类中除了 run
方法外,其他的方法,通过类的实例化去调用并不是多线程的方式。
4. 使用线程锁来解决资源竞争
import threading
lock = threading.Lock()
some_var = 0
class IncrementThread(threading.Thread):
def run(self):
global some_var
lock.acquire() #
read_value = some_var
print "some_var in %s is %d" % (self.name, read_value)
some_var = read_value + 1
print "some_var in %s after increment is %d" % (self.name, some_var)
lock.release()
def use_increment_thread():
threads = []
for i in range(50):
t = IncrementThread()
threads.append(t)
t.start()
for t in threads:
t.join()
print "After 50 modifications, some_var should have become 50"
print "After 50 modifications, some_var is %d" % (some_var,)
if __name__ == "__main__":
use_increment_thread()
这里需要注意一点,我们两个函数/进程使用的是同一把锁,如果我们使用不同的锁还是会出现数据不安全的问题。
Python
提供的锁机制,是解决上面问题的方法之一。
某段代码只能单线程执行时,加上锁,其他线程等待,直到被释放后,其他线程再争锁,竞争到锁的线程执行代码,再释放锁,重复此过程,直到所有线程都走过一遍竞争到锁和释放锁的过程。
但是,再仔细想想,这已经是单线程顺序执行。就本案例而言,已经失去多线程的价值。并且,还带来了因为线程创建开销,浪费时间的副作用。除此之外,还有一个很大风险。
当程序中只有一把锁,通过 try...finally
还能确保不发生死锁。但是,当程序中启用多把锁,很容易发生死锁。
5. 线程池
池是用来保证计算机硬件安全的情况下,最大限度地利用计算机,它降低了程序的运行效率,但是保证了计算机硬件的安全,从而让你写的程序能够正常运行。
- 同步:提交任务之后原地等待任务的返回结果,期间不做任何事
- 异步:提交任务之后不等待任务的返回结果,执行继续往下执行
ThreadPoolExecutor
让线程的使用更加简单方便,减小了线程创建/销毁的资源损耗,无需考虑线程间的复杂同步,方便主线程与子线程的交互。
from concurrent.futures import ThreadPoolExecutor
import time
def get_html(times):
time.sleep(times)
print("get page {} success".format(times))
return times
executor = ThreadPoolExecutor(max_workers=2)
task1 = executor.submit(get_html,(2))
task2 = executor.submit(get_html,(3))
#done方法用来判断某个人物是否完成
print(task1.done())
time.sleep(5)
print(task2.done())
print(task1.cancel()
#result方法可以获取task返回值
print(task1.result())
线程池是从 Python 3.2 才被加入标准库中的 concurrent.futures
模块,相比 threading
模块,该模块通过 submit
返回的是一个 future
对象,通过它可以获取某一个线程的任务执行状态或返回值,另外 futures
可以让多线程和多进程的编码接口一致,
from concurrent.futures import ThreadPoolExecutor
import time
# 括号内可以传数字 不传的话默认会开设当前计算机 cpu 个数进程
pool = ThreadPoolExecutor(5) # 池子里面固定只有五个线程
"""
池子造出来之后 里面会固定存在五个线程
这个五个线程不会出现重复创建和销毁的过程
"""
def task(n):
print(n)
time.sleep(2)
return n**n
# pool.submit(task, 1) # 朝池子中提交任务 异步提交
# print("主")
def call_back(n): # 回调处理数据的函数
print('call_back>>>:',n.result()) # obj.result() 拿到的就是异步提交的任务的返回结果
t_list = []
for i in range(10):
res = pool.submit(task, i)
# print(res.result()) # result 方法 同步提交
# res = pool.submit(task, i).add_done_callback(call_back)
# 将 res 返回的结果 <Future at 0x100f97b38 state=running>,交给回电函数 call_back 处理
# 即 res 做实参传给 call_back 函数
t_list.append(res)
# 等待线程池中所有的任务执行完毕之后再继续往下执行
pool.shutdown() # 关闭线程池 等待线程池中所有的任务运行完毕
for t in t_list:
print(">>>", t.result())
因为开启线程需要消耗一些时间,所以有时候我们会使用线程池来减少开启线程花费的时间。线程池的操作定义在 concurrent.futures.ThreadPoolExecutor
类中,下面我们来看看线程池如何使用:
import time
import threading
from concurrent.futures import ThreadPoolExecutor
def func1():
print(threading.current_thread().name, 'is running')
def func2():
for i in range(3):
time.sleep(1)
print(threading.current_thread().name, 'is running')
pool = ThreadPoolExecutor(max_workers=2)
t1 = pool.submit(func2)
t2 = pool.submit(func1)
在代码中我们创建了一个容量为 2 的线程池,我们调用 pool.submit
函数就能使用线程池中的线程了。
总结
- 池子一旦造出来后,固定了线程或进程。
- 线程不会再变更,所有的任务都是这些线程处理。 这些线程不会再出现重复创建和销毁的过程。
- 任务的提交是异步的,异步提交任务的返回结果,应该通过回调机制来获取。
- 回调机制就相当于,把任务交给一个员工完成,它完成后主动找你汇报完成结果。
6. 查看线程数量
查看线程数量是通过 threading.enumerate()
方法来查看的。
import threading
import time
def test1():
for i in range(5):
print("--test1--%d"%i)
time.sleep(1)
def test2():
for i in range(5):
print("--test2--%d"%i)
time.sleep(1)
def main():
t1 = threading.Thread(target=test1, name="t1")
t2 = threading.Thread(target=test2)
t1.start()
t2.start()
# 获取当前程序所有的线程
print(threading.enumerate())
if __name__ == "__main__":
main()
输出结果:
--test1--0
--test2--0
[<_MainThread(MainThread, started 140076707002112)>, <Thread(t1, started 140076670510848)>, <Thread(Thread-1, started 140076662118144)>]
--test1--1
--test2--1
--test1--2
--test2--2
--test2--3
--test1--3
--test2--4
--test1--4
如果多次运行,会发现打印的顺序并不是一致的。因为线程的运行时没有先后顺序的,谁先抢到资源就先执行谁。
7. 线程其它方法
import os
import threading
from threading import active_count, current_thread
import time
def task():
print("hello")
print(os.getpid())
print(current_thread().name)
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=task, name="t1")
t2 = threading.Thread(target=task, name="t2")
t1.start()
t1.join() # 等待线程执行结果后,主线程继续执行
t2.start()
print(os.getpid()) # 进程 ID
print(current_thread().name) # 获取线程名字
print(active_count()) # 统计当前正在活跃的线程数量
-
join()
:等待线程执行结果后,主线程继续执行 -
os.getpid()
:进程 ID -
current_thread().name
:获取线程名字 -
active_count()
:统计当前正在活跃的线程数量
8. 多个线程同时修改全局变量
import threading
import time
num = 0
def test1(nums):
global num
for i in range(nums):
num += 1
print("test1----num=%d" % num)
def test2(nums):
global num
for i in range(nums):
num += 1
print("test2----num=%d" % num)
def main():
t1 = threading.Thread(target=test1, args=(1000000,))
t2 = threading.Thread(target=test2, args=(1000000,))
t1.start()
t2.start()
time.sleep(5)
print("main-----num=%d" % num)
if __name__ == "__main__":
main()
输出结果如下,当参数 args 变小时不会出现下面这种问题。
test1----num=1177810
test2----num=1476426
main-----num=1476426
当我们的线程 1 到 CPU 中执行代码 num+=1
的时候,其实这一句代码要被拆分为 3 个步骤来执行:
- 第一步:获取 num 的值
- 第二步:把获取的值 +1 操作
- 第三步:把第二步获取的值存储到 num 中
我们在 CPU
中执行这三步的时候,并不能保证这三部一定会执行结束,再去执行线程 2 中的代码。
因为这是多线程的,所以 CPU
在处理两个线程的时候,是采用雨露均沾的方式,可能在线程一刚刚将 num
值 +1
还没来得及将新值赋给 num
时,就开始处理线程二了,因此当线程二执行完全部的 num+=1
的操作后,可能又会开始对线程一的未完成的操作,而此时的操作停留在了完成运算未赋值的那一步,因此在完成对 num
的赋值后,就会覆盖掉之前线程二对 num
的 +1
操作。
那我们应该怎么解决这个问题?这就要用到我们接下来的知识——锁。
9. 互斥锁
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态——锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能改变,直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
基本使用:
import threading
# 生成锁对象,全局唯一
lock = threading.Lock()
# 获取锁。未获取到会阻塞程序,直到获取到锁才会往下执行
lock.acquire()
# 释放锁,归还锁,其他人可以拿去用了
lock.release()
需要注意的是,lock.acquire()
和 lock.release()
必须成对出现。否则就有可能造成死锁。很多时候,我们虽然知道,他们必须成对出现,但是还是难免会有忘记的时候。
为了,规避这个问题。我推荐使用使用上下文管理器来加锁。
import threading
lock = threading.Lock()
with lock:
# 这里写自己的代码
pass
with
语句会在这个代码块执行前自动获取锁,在执行结束后自动释放锁。
互斥锁解决资源竞争
import threading
import time
num = 0
# 创建一个互斥锁,默认是没有上锁的
mutex = threading.Lock()
def test1(nums):
global num
mutex.acquire()
for i in range(nums):
num += 1
mutex.release()
print("test1----num=%d"%num)
def test2(nums):
global num
mutex.acquire()
for i in range(nums):
num += 1
mutex.release()
print("test1----num=%d" % num)
def main():
t1 = threading.Thread(target=test1,args=(1000000,))
t2 = threading.Thread(target=test2,args=(1000000,))
t1.start()
t2.start()
time.sleep(2)
print("main-----num=%d" % num)
if __name__ == "__main__":
main()
此时输出的结果是没有问题的。互斥锁也会引发一个问题,就是死锁。
10. 死锁
当多个线程几乎同一 时间的去修改某个共享数据的时候就需要我们进行同步控制,线程同步能够保证多个线程安全的访问竞争资源,我们最简单的就是引入互斥锁 Lock
、递归锁 RLock
。这两种类型的锁有一点细微的区别,
像下面这种情况,就容易出现死锁。互相锁住了对方,又在等对方释放资源。
import threading
#Lock对象
lock = threading.Lock()
#A 线程
lock.acquire(a)
lock.acquire(b)
#B 线程
lock.acquire(b)
lock.acquire(a)
当线程调用 lock
对象的 acquire()
方法时,lock
就会进入锁住状态,如果此时另一个线程想要获得这个锁,该线程就会变为阻塞状态,因为每次只能有一个线程能够获得锁,直到拥有锁的线程调用 lock
的 release()
方法释放锁之后,线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行状态。
这种情况比较容易被发现,还有一种情况不太容易被发现,调用其他加锁函数,也可能造成死锁。
def add(lock):
global total
for i in range(100000):
lock.acquire()
task()
total += 1
lock.release()
def task():
lock.acquire()
# do something
lock.release()
避免死锁:
- 程序设计上尽量避免
- 添加超时时间
import threading
#RLock对象
rLock = threading.RLock()
rLock.acquire()
#在同一线程内,程序不会堵塞。
rLock.acquire()
rLock.release()
rLock.release()
RLock
允许在同一线程中被多次 acquire
,如果出现 Rlock
,那么 acquire
和 release
必须成对出现,即调用了 i
次 acquire
,必须调用 i
次的 release
才能真正释放所占用的锁。
需要注意的是,可重入锁( RLock
),只在同一线程里放松对锁(通行证)的获取,意思是,只要在同一线程里,程序就当你是同一个人,这个锁就可以复用,其他的话与 Lock
并无区别。
11. 线程同步
11.1 condition 条件变量
condition
(条件变量):condition
有两把锁,一把底层锁会在线程底层调用 wait
后释放。我们每次调用 wait
时候回分配一把锁放到 condition
的等待队列中等待 notify
方法的唤醒。
import threading
class factory(threading.Thread):
def __init__(self,cond):
super(factory,self).__init__(name="口罩生产厂家")
self.cond = cond
def run(self):
with self.cond:
self.cond.wait()
print("{}:生产了10万个口罩,快来拿".format(self.name))
self.cond.notify()
self.cond.wait()
print("{}:又生产了100万个口罩发往武汉".format(self.name))
self.cond.notify()
self.cond.wait()
print("{}:加油,武汉!".format(self.name))
self.cond.notify()
class wuhan(threading.Thread):
def __init__(self,cond):
super(wuhan,self).__init__(name="武汉志愿者")
self.cond = cond
def run(self):
with self.cond:
print("{}:能帮我们生产一批口罩吗?".format(self.name))
self.cond.notify()
self.cond.wait()
print("{}:谢谢你们".format(self.name))
self.cond.notify()
self.cond.wait()
print("{}:一起加油".format(self.name))
self.cond.notify()
self.cond.wait()
if __name__=="__main__":
lock = threading.Condition()
factory = factory(lock)
wuhan = wuhan(lock)
factory.start()
wuhan.start()
上面的代码,大家看到我用到 with
语句,这是因为 Condition
源码中实现了 __enter__
和 __exit__
,类中实现了这两个方法,就可以用 with
语句。而且 __enter__
调用了 acquire() 方法,在 __exit__
方法中调用了 release()
方法。
def __enter__(self):
return self._lock.__enter__()
def __exit__(self, *args):
return self._lock.__exit__(*args)
11.2 semaphore 信号对象
semaphore
(信号对象):用于控制进入数量的锁,Semaphore
对象管理着一个计数器,当我们每次调用 acquire()
方法的时候会进行递减,而每个 release()
方法调用递增,计数器永远不会低于零,当 acquire()
发现计数器为零的时候线程阻塞等待其他线程调用 release()
,具体如一下示例:
import threading
import time
class HtmlSpider(threading.Thread):
def __init__(self, url, sem):
super().__init__()
self.url = url
self.sem = sem
def run(self):
time.sleep(2)
print("got html text success")
self.sem.release()
class UrlProducer(threading.Thread):
def __init__(self, sem):
super().__init__()
self.sem = sem
def run(self):
for i in range(20):
self.sem.acquire()
html_thread = HtmlSpider("https://baidu.com/{}".format(i), self.sem)
html_thread.start()
if __name__ == "__main__":
sem = threading.Semaphore(3)
url_producer = UrlProducer(sem)
url_producer.start()
12. 线程间通信
Python
的 Queue
模块中提供了以下几种队列类:
-
FIFO
(先入先出) 队列Queue
-
LIFO
(后入先出)队列LifoQueue
- 优先级队列
Priority Queue
一般我们可以使用队列来实现线程同步,在开发中 FIFO
队列我们使用的比较多,下面我将用一个例子说明:
from threading import Thread
from time import sleep
from queue import Queue
#生产者
def Producer():
count =0
while True:
if queue.qsize()<1000:
for i in range(100):
count +=1
msg = "生产商品"+str(count)
queue.put(msg)
print(msg)
sleep(0.5)
#消费者
def Consumer():
while True:
if queue.qsize()>100:
for i in range(3):
msg = "消费者消费了"+queue.get()
print(msg)
sleep(1)
if __name__=="__main__":
#定义一个队列
queue = Queue();
#初始化商品
for i in range(500):
queue.put("初始商品"+str(i))
#生产商品
for i in range(4):
p = Thread(target=Producer)
p.start()
#消费商品
for i in range(10):
c = Thread(target=Consumer)
c.start()
队列对象(Queue、LifoQueue 或者 PriorityQueue)提供下列描述的公共方法。
Queue.qsize()
返回队列的大致大小。注意,qsize()> 0 不保证后续的 get() 不被阻塞,qsize() < maxsize 也不保证 put() 不被阻塞。Queue.empty()
如果队列为空,返回 True,否则返回 False。如果 empty() 返回 True,不保证后续调用的 put() 不被阻塞。类似的,如果 empty() 返回 False,也不保证后续调用的 get() 不被阻塞。Queue.full()
如果队列是满的返回 True,否则返回 False。如果 full() 返回 True 不保证后续调用的 get() 不被阻塞。类似的,如果 full() 返回 False 也不保证后续调用的 put() 不被阻塞。
Queue.put(item, block=True, timeout=None)
将 item 放入队列。如果可选参数 block 是 true 并且 timeout 是 None(默认),则在必要时阻塞至有空闲插槽可用。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间没有可用的空闲插槽,将引发 Full 异常。反之(block 是 false),如果空闲插槽立即可用,则把 item 放入队列,否则引发 Full 异常(在这种情况下,timeout 将被忽略)。Queue.put_nowait (item)
相当于 put(item, False)。Queue.get(block=True, timeout=None)
从队列中移除并返回一个项目。如果可选参数 block 是 true 并且 timeout 是 None(默认值),则在必要时阻塞至项目可得到。如果 timeout 是个正数,将最多阻塞 timeout 秒,如果在这段时间内项目不能得到,将引发 Empty 异常。反之(block 是 false),如果一个项目立即可得到,则返回一个项目,否则引发 Empty 异常(这种情况下,timeout 将被忽略)。
POSIX 系统 3.0 之前,以及所有版本的 Windows 系统中,如果 block 是 true 并且 timeout 是 None,这个操作将进入基础锁的不间断等待。这意味着,没有异常能发生,尤其是 SIGINT 将不会触发 KeyboardInterrupt 异常。
Queue.get_nowait()
相当于 get(False)。提供了两个方法,用于支持跟踪排队的任务是否被守护的消费者线程完整的处理。Queue.task_done()
表示前面排队的任务已经被完成。被队列的消费者线程使用。每个 get() 被用于获取一个任务,后续调用 task_done() 告诉队列,该任务的处理已经完成。
如果 join() 当前正在阻塞,在所有条目都被处理后,将解除阻塞(意味着每个 put() 进队列的条目的 task_done() 都被收到)。 如果被调用的次数多于放入队列中的项目数量,将引发 ValueError 异常。
Queue.join()
阻塞至队列中所有的元素都被接收和处理完毕。
在多线程通信中,Queue 扮演者重要的角色,一般添加数据到队列使用 put() 方法,在队列中取数据使用 get() 方法,后面针对 Queue 还会做进一步的讲解
其它参考一篇带你熟练使用多线程与原理