线程的操作
线程理论
线程是需要依赖进程的,事实上,进程运行时并不会工作,而是线程在工作,进程只是在内存中申请了一块空间而已,所以进程是一个资源单位,而线程是执行单位;就相当于进程创建工厂,而线程是工厂的打工人。
开设线程的消耗远远小于进程,一个进程里至少有一个线程,也可以开设多个线程,创建线程无需申请内存空间,创建消耗资源小,并且一个进程内的多个线程数据是共享的。
创建线程的两种方式
创建线程与创建进程的方法几乎一致,并且创建线程无需在__main__下面编写,但是为了统一,还是习惯在子代码中写。
方式一:Thread()
from threading import Thread
import time
def task(name):
print(f'{name} is running')
time.sleep(3)
print(f'{name} is over')
if __name__ == '__main__':
t = Thread(target=task, args=('jason', ))
t.start() # 创建线程的开销极小 几乎是一瞬间就可以创建
print('主线程')
"""
执行结果:
jason is running
主线程
jason is over
"""
从执行结果来看,代码会先输出线程的内容,可见线程创建的迅速,如果是创建进程,一般情况下都是先输出主进程的内容。
方式二:重写Thread类的run方法
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, username):
super().__init__()
self.username = username
def run(self):
print(f'{self.username} jason is running')
time.sleep(3)
print(f'{self.username} is over')
t = MyThread('jason')
t.start()
print('主线程')
"""
执行结果:
jason jason is running
主线程
jason is over
"""
线程实现TCP服务端的并发
线程实现TCP服务端的并发的方法与进程也几乎一致,唯一的区别就是线程节省资源。
服务端(Server)
import socket
from threading import Thread
def task(sock):
while True:
msg = sock.recv(1024)
print(msg.decode('utf8'))
sock.send(b'from server')
if __name__ == '__main__':
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
while True:
client, addr = server.accept()
p = Thread(target=task, args=(client, ))
p.start()
客户端(Client)
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
s = input('发送给服务端的消息:')
send_msg = 'from client: %s' % s
client.send(send_msg.encode('utf8'))
msg = client.recv(1024)
print(msg.decode('utf8'))
线程join方法
join方法作用:让主线程代码等待子线程代码运行完毕之后再往下执行。
from threading import Thread
import time
def task():
print('子线程 is running')
time.sleep(3)
print('子线程 is over')
t = Thread(target=task)
t.start()
t.join()
print('主线程')
"""
执行结果:
子线程 is running
子线程 is over
主线程
"""
PS:在没有用join()方法时,主线程为什么要等着子线程结束才会结束整个进程?因为主线程结束也就标志着整个进程的结束,要确保子线程运行过程中所需的各项资源。
线程数据共享
在同一个进程内的多个线程数据是可以实现共享的。
from threading import Thread
money = 10000
def task():
global money
money = 1
t = Thread(target=task)
t.start()
t.join() # 确保线程执行了修改money的值
print(money) # 输出:1
线程对象属性和方法
os.getpid():查看当前所在进程。
active_count():获取当前存活线程,主线程也算。
current_thread().name:获取当前线程名称
from threading import Thread, current_thread, active_count
import time
import os
def task():
time.sleep(1)
print('子线程名称:', current_thread().name)
print('子线程所在进程号:', os.getpid())
if __name__ == '__main__':
t1 = Thread(target=task)
t2 = Thread(target=task)
t1.start()
t2.start()
print('当前存活线程数:', active_count()) # 获取当前存活线程,主线程也算
print('主线程名称:', current_thread().name)
print('主线程所在进程号:', os.getpid())
执行结果:
当前存活线程数: 3
主线程名称: MainThread
主线程所在进程号: 15048
子线程名称: Thread-1
子线程名称: Thread-2
子线程所在进程号: 15048
子线程所在进程号: 15048
守护线程
线程与进程一样,给对象的daemon属性值设为True即可。
from threading import Thread
import time
def task():
print('子线程 is running')
time.sleep(3)
print('子线程 is over')
if __name__ == '__main__':
t1 = Thread(target=task)
t1.daemon = True
t1.start()
print('主线程')
执行结果:
子线程 is running
主线程
但是在有多个线程的情况下,如果有一个子线程还在执行,那么这个守护线程就跟没有设置一样,因为主线程要等待所有非守护线程结束才可以结束。
GIL全局解释器锁
官方文档
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
解释:
GIL只存在于CPython(用C语言开发的)解释器中,不是python的特征。
GIL是加在CPython解释器上面的互斥锁,用于阻止同一个进程下的多个线程同时执行,原因是因为CPython解释器中的垃圾回收机制不是线程安全的,垃圾回收机制也算是一个线程,如果线程同时执行,那么可能会出现在一个线程中刚刚创建一个变量,垃圾回收机制线程就给回收了。
所以同一个进程下的多个线程要想执行必须先抢GIL锁,同一个进程下多个线程肯定不能同时运行。
拓展
如果多个任务都是IO密集型的,那么多线程更有优势,因为创建线程消耗的资源更少,并且因为有多个IO操作,线程会经常阻塞,一阻塞就会释放锁。
如果多个任务都是计算密集型,因为GIL的存在,多线程确实没有优势,但是可以用多进程。
GIL与普通互斥锁区别
GIL本质上其实也是一个互斥锁,GIL是对线程进行上锁,线程在执行IO操作的时候会释放锁;普通互斥锁会一锁到底,不会提前释放锁;
验证GIL的存在
from threading import Thread
money = 100
def task():
global money
money -= 1
if __name__ == '__main__':
t_list = [] # 用于存储线程对象
# 创建一百个线程
for i in range(100):
t = Thread(target=task)
t_list.append(t)
t.start()
# 把线程全部join一遍,确保主线程最后执行
for t in t_list:
t.join()
print(money) # 输出:0
输出的money为0,说明其中一个线程执行时,其他线程是没有在执行的;这就是GIL的作用,在同一时刻只有一个线程在执行。
我对上述代码先进行如下修改:
from threading import Thread
import time
money = 100
def task():
global money
temp = money
# 模拟IO操作
time.sleep(0.1)
money = temp - 1
if __name__ == '__main__':
t_list = [] # 用于存储线程对象
# 创建一百个线程
for i in range(100):
t = Thread(target=task)
t_list.append(t)
t.start()
# 把线程全部join一遍,确保主线程最后执行
for t in t_list:
t.join()
print(money) # 输出:99
执行结果输出为99,说明执行中的线程在执行IO操作的时候,其它的线程就会开始去抢着执行,让所有线程里的temp都等于money后,也就是100,所以最后的money = temp - 1都变成了money = 100 - 1,最后的结果当然是99,这种的解决办法就是只有在加一个锁。
验证不同数据加不同锁
from threading import Thread, Lock
import time
money = 100
mutex = Lock()
def task():
global money
# 上锁
mutex.acquire()
temp = money
# 模拟IO操作
time.sleep(0.1)
money = temp - 1
# 解锁
mutex.release()
if __name__ == '__main__':
t_list = [] # 用于存储线程对象
# 创建一百个线程
for i in range(100):
t = Thread(target=task)
t_list.append(t)
t.start()
# 把线程全部join一遍,确保主线程最后执行
for t in t_list:
t.join()
print(money) # 输出:0
这种方法虽然解决了数据安全问题,但相应的也降低了效率。
多线程作用验证
如果多个任务都是IO密集型的,那么多线程更有优势,因为创建线程消耗的资源更少,并且因为有多个IO操作,线程会经常阻塞,一阻塞就会释放锁。
验证:
使用线程
from threading import Thread
import time
def work():
# 模拟IO操作
time.sleep(5)
if __name__ == '__main__':
start_time = time.time()
t_list = []
# 创建500个执行有IO操作的线程
for i in range(500):
t = Thread(target=work)
t_list.append(t)
t.start()
# 让线程全部执行完毕才能执行主线程
for t in t_list:
t.join()
print('运行时间:', time.time() - start_time)
# 输出:运行时间: 5.065059185028076
使用进程
from multiprocessing import Process
import time
def work():
# 模拟IO操作
time.sleep(5)
if __name__ == '__main__':
start_time = time.time()
# 存储进程对象
p_list = []
# 创建500个执行有IO操作的进程
for i in range(500):
p = Process(target=work)
p_list.append(p)
p.start()
# 让线程全部执行完毕才能向下执行
for p in p_list:
p.join()
print('运行时间:', time.time() - start_time)
# 输出:运行时间: 14.466909170150757
当有多个IO密集型的任务时,线程的优势是很明显的,运行时间远小于进程。
但是如果是有多个计算密集型任务时,进程的优势更大。
验证
使用线程
from threading import Thread
import time
def work():
res = 1
# 疯狂进行计算
for i in range(1000000):
res += i
if __name__ == '__main__':
start_time = time.time()
t_list = []
# 创建500个线程
for i in range(50):
t = Thread(target=work)
t_list.append(t)
t.start()
# 让线程全部执行完毕才能向下执行
for t in t_list:
t.join()
print('运行时间:', time.time() - start_time)
# 输出:运行时间: 2.6860923767089844
使用进程
from multiprocessing import Process
import time
def work():
res = 1
# 疯狂进行计算
for i in range(1000000):
res += i
if __name__ == '__main__':
start_time = time.time()
p_list = []
# 创建500个线程
for i in range(50):
p = Process(target=work)
p_list.append(p)
p.start()
# 让线程全部执行完毕才能向下执行
for p in p_list:
p.join()
print('运行时间:', time.time() - start_time)
# 输出:运行时间: 1.5109596252441406