线程,有时被称为轻量进程,是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程不拥有私有的系统资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。Python多线程用于I/O操作密集型的任务,如SocketServer网络并发,网络爬虫。
现代处理器都是多核的,几核处理器只能同时处理几个线程,多线程执行程序看起来是同时进行,实际上是CPU在多个线程之间快速切换执行,这中间就涉及到上下问切换,所谓的上下文切换就是指一个线程Thread被分配的时间片用完了之后,线程的信息被保存起来,CPU执行另外的线程,再到CPU读取线程Thread的信息并继续执行Thread的过程。
线程模块
Python的标准库提供了两个模块:_thread和threading。_thread 提供了低级别的、原始的线程以及一个简单的互斥锁,它相比于 threading 模块的功能还是比较有限的。Threading模块是_thread模块的替代,在实际的开发中,绝大多数情况下还是使用高级模块threading,因此本书着重介绍threading高级模块的使用。
Python创建Thread对象语法如下:
import threading
threading.Thread(target=None, name=None, args=())
主要参数说明:
-
target 是函数名字,需要调用的函数。
-
name 设置线程名字。
-
args 函数需要的参数,以元祖( tuple)的形式传入
Thread 对象主要方法说明:
-
run(): 用以表示线程活动的方法。
-
start():启动线程活动。
-
join(): 等待至线程中止。
-
isAlive(): 返回线程是否活动的。
-
getName(): 返回线程名。
-
setName(): 设置线程名。
Python中实现多线程有两种方式:函数式创建线程和创建线程类。
第一种创建线程方式:函数式创建线程
创建线程的时候,只需要传入一个执行函数和函数的参数即可完成threading.Thread实例的创建。下面的例子使用Thread类来产生2个子线程,然后启动2个子线程并等待其结束,
import threading
import time,random,math
# idx 循环次数
def printNum(idx):
for num in range(idx ):
#打印当前运行的线程名字
print("{0}\tnum={1}".format(threading.current_thread().getName(), num) )
delay = math.ceil(random.random() * 2)
time.sleep(delay)
if __name__ == '__main__':
th1 = threading.Thread(target=printNum, args=(2,),name="thread1" )
th2 = threading.Thread(target=printNum, args=(3,),name="thread2" )
#启动2个线程
th1.start()
th2.start()
#等待至线程中止
th1.join()
th2.join()
print("{0} 线程结束".format(threading.current_thread().getName()))
运行脚本得到以下结果。
thread1 num=0
thread2 num=0
thread1 num=1
thread2 num=1
thread2 num=2
MainThread 线程结束
运行脚本默认会启动一个线程,把该线程称为主线程,主线程有可以启动新的线程,Python的threading模块有个current_thread()函数,它将返回当前线程的示例。从当前线程的示例可以获得前运行线程名字,核心代码如下。
threading.current_thread().getName()
启动一个线程就是把一个函数和参数传入并创建Thread实例,然后调用start()开始执行
th1 = threading.Thread(target=printNum, args=(2,),name="thread1" )
th1.start()
从返回结果可以看出主线程示例的名字叫MainThread,子线程的名字在创建时指定,本例创建了2个子线程,名字叫thread1和thread2。如果没有给线程起名字,Python就自动给线程命名为Thread-1,Thread-2…等等。在本例中定义了线程函数printNum(),打印idx次记录后退出,每次打印使用time.sleep()让程序休眠一段时间。
第二种创建线程方式:创建线程类
直接创建threading.Thread的子类来创建一个线程对象,实现多线程。通过继承Thread类,并重写Thread类的run()方法,在run()方法中定义具体要执行的任务。在Thread类中,提供了一个start()方法用于启动新进程,线程启动后会自动调用run()方法。
import threading
import time,random,math
class MutliThread(threading.Thread):
def __init__(self, threadName,num):
threading.Thread.__init__(self)
self.name = threadName
self.num = num
def run(self):
for i in range(self.num):
print("{0} i={1}".format(threading.current_thread().getName(), i))
delay = math.ceil(random.random() * 2)
time.sleep(delay)
if __name__ == '__main__':
thr1 = MutliThread("thread1",3)
thr2 = MutliThread("thread2",2)
# 启动线程
thr1.start()
thr2.start()
# 等待至线程中止
thr1.join()
thr2.join()
print("{0} 线程结束".format(threading.current_thread().getName()))
运行脚本得到以下结果。
thread1 i=0
thread2 i=0
thread1 i=1
thread2 i=1
thread1 i=2
MainThread 线程结束
从返回结果可以看出,通过创建Thread类来产生2个线程对象thr1和thr2,重写Thread类的run()函数,把业务逻辑放入其中,通过调用线程对象的start()方法启动线程。通过调用线程对象的join()函数,等待该线程完成,在继续下面的操作。
在本例中,主线程MainThread等待子线程thread1和thread2线程运行结束后才输出” MainThread 线程结束”。如果子线程thread1和thread2不调用join()函数,那么主线程MainThread和2个子线程是并行执行任务的,2个子线程加上join()函数后,程序就变成顺序执行了。所以子线程用到join()的时候,通常都是主线程等到其他多个子线程执行完毕后再继续执行,其他的多个子线程并不需要互相等待。
守护线程
在线程模块中,使用子线程对象用到join()函数,主线程需要依赖子线程执行完毕后才继续执行代码。如果子线程不使用join()函数,主线程和子线程是并行运行的,没有依赖关系,主线程执行了,子线程也在执行。
在多线程开发中,如果子线程设定为了守护线程,守护线程会等待主线程运行完毕后被销毁。一个主线程可以设置多个守护线程,守护线程运行的前提是,主线程必须存在,如果主线程不存在了,守护线程会被销毁。
在本例中创建1个主线程3个子线程,让主线程和子线程并行执行。内容如下。
import threading, time
def run(taskName):
print("任务:", taskName)
time.sleep(2)
print("{0} 任务执行完毕".format(taskName)) # 查看每个子线程
if __name__ == '__main__':
start_time = time.time()
for i in range(3):
thr = threading.Thread(target=run, args=("task-{0}".format(i),))
thr.start()
# 查看主线程和当前活动的所有线程数
print("{0}线程结束,当线程数量={1}".format( threading.current_thread().getName(), threading.active_count()))
print("消耗时间:", time.time() - start_time)
运行脚本得到以下结果:
任务: task-0
任务: task-1
任务: task-2
MainThread线程结束,当线程数量=4
消耗时间: 0.0009751319885253906
task-2 任务执行完毕
task-0 任务执行完毕
task-1 任务执行完毕
从返回结果可以看出,当前的线程个数是4,线程个数=主线程数 + 子线程数,在本例中有1个主线程和3个子线程。主线程执行完毕后,等待子线程执行完毕,程序才会退出。
在本例的基础上,把所有的子线程都设置为守护线程。子线程变成守护线程后,只要主线程执行完毕,程序不管子线程有没有执行完毕,程序都会退出。使用线程对象的setDaemon(True)函数来设置守护线程。
import threading, time
def run(taskName):
print("任务:", taskName)
time.sleep(2)
print("{0} 任务执行完毕".format(taskName))
if __name__ == '__main__':
start_time = time.time()
for i in range(3):
thr = threading.Thread(target=run, args=("task-{0}".format(i),))
# 把子线程设置为守护线程,在启动线程前设置
thr.setDaemon(True)
thr.start()
# 查看主线程和当前活动的所有线程数
thrName = threading.current_thread().getName()
thrCount = threading.active_count()
print("{0}线程结束,当线程数量={1}".format(thrName, thrCount))
print("消耗时间:", time.time() - start_time)
运行脚本得到以下结果。
任务: task-0
任务: task-1
任务: task-2
MainThread线程结束,当线程数量=4
消耗时间: 0.0010023117065429688
从本例的返回结果可以看出,主线程执行完毕后,程序不会等待守护线程执行完毕后就退出了。设置线程对象为守护线程,一定要在线程对象调用start()函数前设置。
多线程的锁机制
多线程编程访问共享变量时会出现问题,但是多进程编程访问共享变量不会出现问题。因为多进程中,同一个变量各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享。
多个进程之间对内存中的变量不会产生冲突,一个进程由多个线程组成,多线程对内存中的变量进行共享时会产生影响,所以就产生了死锁问题,怎么解决死锁问题是本节主要介绍的内容。
1、变量的作用域
一般在函数体外定义的变量称为全局变量,在函数内部定义的变量称为局部变量。全局变量所有作用域都可读,局部变量只能在本函数可读。函数在读取变量时,优先读取函数本身自有的局部变量,再去读全局变量。 内容如下。
# 全局变量
balance = 1
def change():
# 定义全局变量
global balance
balance = 100
# 定义局部变量
num = 20
print("change() balance={0}".format(balance) )
if __name__ == "__main__" :
change()
print("修改后的 balance={0}".format(balance) )
运行脚本得到以下结果。
change() balance=100
修改后的 balance=100
如果注释掉change()函数里的 global
v1,那么得到的返回值是。
change() balance=100
修改后的 balance=1
在本例中在change()函数外定义的变量balance是全局变量,在change()函数内定义的变量num是局部变量,全局变量默认是可读的,可以在任何函数中使用,如果需要改变全局变量的值,需要在函数内部使用global定义全局变量,本例中在change()函数内部使用global定义全局变量balance,在函数里就可以改变全局变量了。
在函数里可以使用全局变量,但是在函数里不能改变全局变量。想实现多个线程共享变量,需要使用全局变量。在方法里加上全局关键字 global定义全局变量,多线程才可以修改全局变量来共享变量。
2、多线程中的锁
多线程同时修改全局变量时会出现数据安全问题,线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。在本例中我们生成2个线程同时修改change()函数里的全局变量balance时,会出现数据不一致问题。
本案例文件名为PythonFullStack\Chapter03\threadDemo03.py,内容如下。
import threading
balance = 100
def change(num, counter):
global balance
for i in range(counter):
balance += num
balance -= num
if balance != 100:
# 如果输出这句话,说明线程不安全
print("balance=%d" % balance)
break
if __name__ == "__main__":
thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
thr1.start()
thr2.start()
thr1.join()
thr2.join()
print("{0} 线程结束".format(threading.current_thread().getName()))
运行以上脚本,当2个线程运行次数达到500000次时,会出现以下结果。
balance=200
MainThread 线程结束
在本例中定义了一个全局变量balance,初始值为100,当启动2个线程后,先加后减,理论上balance应该为100。线程的调度是由操作系统决定的,当线程t1和t2交替执行时,只要循环次数足够多,balance结果就不一定是100了。从结果可以看出,在本例中线程t1和t2同时修改全局变量balance时,会出现数据不一致问题。
注意
在多线程情况下,所有的全局变量有所有线程共享。所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
在多线程情况下,使用全局变量并不会共享数据,会出现线程安全问题。线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致
在单线程运行时没有代码安全问题。写多线程程序时,生成一个线程并不代表多线程。在多线程情况下,才会出现安全问题。
针对线程安全问题,需要使用”互斥锁”,就像数据库里操纵数据一样,也需要使用锁机制。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
互斥锁的核心代码如下:
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
如果要确保balance计算正确,使用threading.Lock()来创建锁对象lock,把 lock.acquire()和lock.release()加在同步代码块里,本例的同步代码块就是对全局变量balance进行先加后减操作。
当某个线程执行change()函数时,通过lock.acquire()获取锁,那么其他线程就不能执行同步代码块了,只能等待知道锁被释放了,获得锁才能执行同步代码块。由于锁只有一个,无论多少线程,同一个时刻最多只有一个线程持有该锁,所以修改全局变量balance不会产生冲突。改良后的代码内容如下。
import threading
balance = 100
lock = threading.Lock()
def change(num, counter):
global balance
for i in range(counter):
# 先要获取锁
lock.acquire()
balance += num
balance -= num
# 释放锁
lock.release()
if balance != 100:
# 如果输出这句话,说明线程不安全
print("balance=%d" % balance)
break
if __name__ == "__main__":
thr1 = threading.Thread(target=change,args=(100,500000),name='t1')
thr2 = threading.Thread(target=change,args=(100,500000),name='t2')
thr1.start()
thr2.start()
thr1.join()
thr2.join()
print("{0} 线程结束".format(threading.current_thread().getName()))
在本例中2个线程同时运行lock.acquire()时,只有一个线程能成功的获取锁,然后执行代码,其他线程就继续等待直到获得锁位置。获得锁的线程用完后一定要释放锁,否则其他线程就会一直等待下去,成为死线程。
在运行上面脚本就不会产生输出信息,证明代码是安全的。把 lock.acquire()和lock.release()加在同步代码块里,还要注意锁的力度不要加的太大了。第一个线程只有运行完了,第二个线程才能运行,所以锁要在需要同步代码里加上。
范例代码
其实现了一个读写锁,并可以指定读优先还是写优先,且读状态相互不影响,写状态不允许读状态。
class RWLock(object):
def __init__(self):
self.lock = threading.Lock()
self.rcond = threading.Condition(self.lock)
self.wcond = threading.Condition(self.lock)
self.read_waiter = 0 # 等待获取读锁的线程数
self.write_waiter = 0 # 等待获取写锁的线程数
self.state = 0 # 正数:表示正在读操作的线程数 负数:表示正在写操作的线程数(最多-1)
self.owners = [] # 正在操作的线程id集合
self.write_first = True # 默认写优先,False表示读优先
def write_acquire(self, blocking=True):
# 获取写锁只有当
me = threading.get_ident()
with self.lock:
while not self._write_acquire(me):
if not blocking:
return False
self.write_waiter += 1
self.wcond.wait()
self.write_waiter -= 1
return True
def _write_acquire(self, me):
# 获取写锁只有当锁没人占用,或者当前线程已经占用
if self.state == 0 or (self.state < 0 and me in self.owners):
self.state -= 1
self.owners.append(me)
return True
if self.state > 0 and me in self.owners:
raise RuntimeError('cannot recursively wrlock a rdlocked lock')
return False
def read_acquire(self, blocking=True):
me = threading.get_ident()
with self.lock:
while not self._read_acquire(me):
if not blocking:
return False
self.read_waiter += 1
self.rcond.wait()
self.read_waiter -= 1
return True
def _read_acquire(self, me):
if self.state < 0:
# 如果锁被写锁占用
return False
if not self.write_waiter:
ok = True
else:
ok = me in self.owners
if ok or not self.write_first:
self.state += 1
self.owners.append(me)
return True
return False
def unlock(self):
me = threading.get_ident()
with self.lock:
try:
self.owners.remove(me)
except ValueError:
raise RuntimeError('cannot release un-acquired lock')
if self.state > 0:
self.state -= 1
else:
self.state += 1
if not self.state:
if self.write_waiter and self.write_first: # 如果有写操作在等待(默认写优先)
self.wcond.notify()
elif self.read_waiter:
self.rcond.notify_all()
elif self.write_waiter:
self.wcond.notify()
read_release = unlock
write_release = unlock
线程间通信
cookbook 介绍:https://python3-cookbook.readthedocs.io/zh_CN/latest/c12/p03_communicating_between_threads.html
Queue 对象:从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue
库中的队列了。创建一个被多个线程共享的 Queue
对象。
from queue import Queue
from threading import Thread
# Object that signals shutdown
_sentinel = object()
# A thread that produces data
def producer(out_q):
while running:
# Produce some data
...
out_q.put(data)
# Put the sentinel on the queue to indicate completion
out_q.put(_sentinel)
# A thread that consumes data
def consumer(in_q):
while True:
# Get some data
data = in_q.get()
# Check for termination
if data is _sentinel:
in_q.put(_sentinel)
break
# Process the data
...
Condition 变量: 尽管队列是最常见的线程间通信机制,但是仍然可以自己通过创建自己的数据结构并添加所需的锁和同步机制来实现线程间通信。最常见的方法是使用 Condition
变量来包装你的数据结构。
import heapq
import threading
class PriorityQueue:
def __init__(self):
self._queue = []
self._count = 0
self._cv = threading.Condition()
def put(self, item, priority):
with self._cv:
heapq.heappush(self._queue, (-priority, self._count, item))
self._count += 1
self._cv.notify()
def get(self):
with self._cv:
while len(self._queue) == 0:
self._cv.wait()
return heapq.heappop(self._queue)[-1]
线程状态传递(是否可以结束):
- Queue 的 task_done() 和 join() 函数
from queue import Queue
from threading import Thread
# A thread that produces data
def producer(out_q):
while running:
# Produce some data
...
out_q.put(data)
# A thread that consumes data
def consumer(in_q):
while True:
# Get some data
data = in_q.get()
# Process the data
...
# Indicate completion
in_q.task_done()
# Create the shared queue and launch both threads
q = Queue()
t1 = Thread(target=consumer, args=(q,))
t2 = Thread(target=producer, args=(q,))
t1.start()
t2.start()
# Wait for all produced items to be consumed
q.join()
- 添加 Event 对象
from queue import Queue
from threading import Thread, Event
# A thread that produces data
def producer(out_q):
while running:
# Produce some data
...
# Make an (data, event) pair and hand it to the consumer
evt = Event()
out_q.put((data, evt))
...
# Wait for the consumer to process the item
evt.wait()
# A thread that consumes data
def consumer(in_q):
while True:
# Get some data
data, evt = in_q.get()
# Process the data
...
# Indicate completion
evt.set()
流量控制:对于“生产者”与“消费者”速度有差异的情况,为队列中的元素数量添加上限是有意义的。比如,一个“生产者”产生项目的速度比“消费者” “消费”的速度快,那么使用固定大小的队列就可以在队列已满的时候阻塞队列,以免未预期的连锁效应扩散整个程序造成死锁或者程序运行失常。在通信的线程之间进行“流量控制”是一个看起来容易实现起来困难的问题。如果你发现自己曾经试图通过摆弄队列大小来解决一个问题,这也许就标志着你的程序可能存在脆弱设计或者固有的可伸缩问题。get()
和 put()
方法都支持非阻塞方式和设定超时,例如:
import queue
q = queue.Queue()
try:
data = q.get(block=False)
except queue.Empty:
...
try:
q.put(item, block=False)
except queue.Full:
log.warning('queued item %r discarded!', item)
...
try:
data = q.get(timeout=5.0)
except queue.Empty:
...
最后,有 q.qsize()
, q.full()
, q.empty()
等实用方法可以获取一个队列的当前大小和状态。但要注意,这些方法都不是线程安全的。可能你对一个队列使用 empty()
判断出这个队列为空,但同时另外一个线程可能已经向这个队列中插入一个数据项。所以,你最好不要在你的代码中使用这些方法。