线程进阶
实现线程局部变量
ThreadLocal可以实现线程数据不共享。即线程的局部变量。
先来看一个例子:
import threading
num = 0
# 创建一个全局的ThreadLocal对象
# 每个线程有独立的存储空间
# 每个线程对ThreadLocal对象都可以读写, 但是互不影响
local = threading.local()
def run(x, n):
x = x + n
x = x - n
def func(n):
# 每个线程都有local.x ,就是线程的局部变量
local.x = num
for i in range(1000000):
run(local.x, n)
print('%s--%d' % (threading.current_thread().name, local.x))
if __name__ == '__main__':
t1 = threading.Thread(target=func, args=(6,))
t2 = threading.Thread(target=func, args=(9,))
t1.start()
t2.start()
t1.join()
t2.join()
print("num=",num)
我们能从结果看到。num最后结果是0,那是因为num被设置成了线程的局部变量,每个线程操作的都是自己的局部变量,所以不会和其他线程的数据发生混乱。
使用信号量限制线程并发数
Semphore叫做信号量,可以限制线程的并发数,是一种带计数的线程同步机制,当调用release时,增加计算,当acquire时,减少计数,当计数为0时,自动阻塞,等待release被调用。
在大部分情况下,信号量用于守护有限容量的资源。
先来看一个例子:
import threading
import time
# 创建一个信号量实例,限制并行的线程数为3个
sem = threading.Semaphore(3)
def run(i):
# 获取信号量,信号量减1
sem.acquire()
print('%s--%d' % (threading.current_thread().name, i))
time.sleep(2)
# 释放信号量,信号量加1
sem.release()
if __name__ == '__main__':
# 创建5个线程
for i in range(5):
threading.Thread(target=run, args=(i,)).start()
我们能看到前三个线程会先执行,前三个线程执行完了之后才会执行后2个线程。
还有另一种信号量叫做BoundedSemaphore,比普通的Semphore更加严谨,叫做有界信号量。有界信号量会确保它当前的值不超过它的初始值。如果超过,则引发ValueError。
Barrier对象
Barrier翻译成栅栏,可以理解为线程数量不够时,会被拦住不让执行。我们来看一个例子:
import threading
import time
bar = threading.Barrier(3)
def run(i):
print('%s--%d--start' % (threading.current_thread().name, i))
time.sleep(2)
bar.wait()
print('%s--%d--end' % (threading.current_thread().name, i))
if __name__ == '__main__':
# 创建5个线程
for i in range(5):
threading.Thread(target=run, args=(i,)).start()
执行结果:
Thread-1--0--start
Thread-2--1--start
Thread-3--2--start
Thread-4--3--start
Thread-5--4--start
Thread-4--3--end
Thread-2--1--end
Thread-1--0--end
我们能看到执行完3个线程之后,程序一直停着。那是因为后面的线程不够3个,被栅栏拦住了没法继续执行。
定时执行线程
threading中的Timer可以让线程在指定时间之后执行 ,实现定时执行的效果。
import threading
import time
def run():
print('start...')
time.sleep(2)
print('end...')
if __name__ == '__main__':
timer = threading.Timer(5, run)
timer.start()
print('end main...')
最简单的线程通信机制——Event
Event(事件)是最简单的线程通信机制之一:一个线程通知事件,其他线程等待事件。Event内置了一个初始为False的标志(flag),当调用set()时设为True,调用clear()时重置为 False。wait()将阻塞线程至等待阻塞状态。
来看一个简单的例子:
import threading
import time
# 创建一个事件
event = threading.Event()
def run():
print('start...')
# 进入到等待事件的阻塞状态中
event.wait()
time.sleep(2)
print('end...')
if __name__ == '__main__':
t1 = threading.Thread(target=run)
t1.start()
time.sleep(2)
# 发送事件
print('sending event...')
event.set()
注意:clear会把内置的标志重新设为False。请写一个程序体现clear的作用。
生产者消费者模型
生产者消费者模型是多线程中常见的一种模型。先来看一个简单的例子。
import threading, queue, time, random
# 生产者
def product(id, q):
while True:
num = random.randint(0, 10000)
q.put(num)
print('生产者%d生成了%d数据放入了队列' % (id, num))
time.sleep(1)
# 告诉队列任务完成
q.task_done()
# 消费者
def consumer(id, q):
while True:
item = q.get()
if item is None:
break
print('消费者%d消费了%d数据' % (id, item))
time.sleep(1)
# 任务完成
q.task_done()
if __name__ == '__main__':
# 消息队列
q = queue.Queue()
# 启动生产者
for i in range(4):
threading.Thread(target=product, args=(i, q)).start()
# 启动消费者
for i in range(3):
threading.Thread(target=consumer, args=(i, q)).start()
生产者生成的数据存放在队列q中,消费者去队列中取数据。
线程调度
正常情况下线程的执行顺序是操作系统控制的,如果需要让我们自己来控制线程的执行顺序,需要用到Condition(条件变量)类。
尝试实现这么一个需求,有一个线程输出 0 2 4 6 8 ,另一个线程输出 1 3 5 7 9 ,这两个线程并行执行时,显示的结果是混乱无序的,要求输出结果为0 1 2 3 4 5 6 7 8 9 。
import threading, time
# 线程条件变量
cond = threading.Condition()
def run1():
for i in range(0, 10, 2):
# condition自带一把锁
# 获取锁
if cond.acquire():
print(threading.current_thread().name, i)
time.sleep(1)
# 当前线程执行完成,等待另一个线程执行,并释放锁
cond.wait()
# 通知另一个线程可以运行了。
cond.notify()
def run2():
for i in range(1, 10, 2):
# 获取锁
if cond.acquire():
print(threading.current_thread().name, i)
time.sleep(1)
# 通知上面的线程你不要等了,该走了。
cond.notify()
# 然后等待上一个线程给自己继续执行的信号。
cond.wait()
if __name__ == '__main__':
threading.Thread(target=run1).start()
threading.Thread(target=run2).start()
Condition类的方法说明:
acquire([timeout])/release(): 调用关联的锁的相应方法。
wait([timeout]): 调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
notify(): 调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
notifyAll(): 调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
condition同样可以使用with上下文管理器来自动管理锁。
多线程应用
socket编程中多线程的应用。单线程中,socket服务器在监听时,处于阻塞状态,只能同时处理一个客户端的连接,使用多线程,让服务器可以同时处理多个客户端连接。
服务器端代码:
import socket
import threading
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(('10.36.137.19',8081))
server.listen(5)
def run(ck):
data = ck.recv(1024)
print("客户端说:" + data.decode("utf-8"))
ck.send("nice to meet you too".encode("utf-8"))
print("服务器启动成功,等待客户端的链接")
while True:
clientSocket, clientAddress = server.accept()
t = threading.Thread(target=run, args=(clientSocket,))
t.start()
客户端代码:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("10.36.137.19", 8081))
while True:
data = input("请输入给服务器发送的数据")
client.send(data.encode("utf-8"))
info = client.recv(1024)
print("服务器说:", info.decode("utf-8"))