主要介绍使用 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()
:进程 IDcurrent_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 还会做进一步的讲解
其它参考
https://segmentfault.com/a/1190000014306740
一篇带你熟练使用多线程与原理