day40—开线程,线程方法、线程锁、多线程于多进程优缺点
今日内容:
- 开启线程的两种方式
- TCP服务端实现并发的效果
- 线程对象的join方法
- 线程对象的属性以及其他方法
- 守护线程
- 线程互斥锁
- GIL全局解释器锁
- 多进程与多线程的各自的优缺点即实际的应用场景
1、开启线程的两种方式
线程的的开启方式跟进程一样,只不过是换了了模块,而且可以不用在main下面开启,但是我们还是习惯的将启动命令写在main下。
方式一:类实例化创造对象
from threading import Thread
import time
# 方式一
def task(name):
print(f'hello {name}')
time.sleep(1)
print(f'bye {name}')
if __name__ == '__main__':
t=Thread(target=task,args=('tom',))
t.start()
print('主')
方式二:继承的继承
# 方式二:
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, name):
# 重写其他的类的方法但又不知道方法里有什么的时候直接调用父类的方法
super().__init__()
self.name = name
def run(self):
print(f'hello {self.name}')
time.sleep(1)
print(f'bye {self.name}')
if __name__ == '__main__':
t = MyThread('tom')
t.start()
print('主')
注意:我们知道开线程不像开进程那样需要申请内存空间、拷贝代码,所以创建线程的开销是非常小的,几乎是代码一执行线程就已经创建好了
ps:python中以双下滑线加开头双下滑下结尾的统一读成:“双下划xxx”如:(init)读成“双下滑init”
2、TCP服务端实现并发的效果
将之前通信循环的代码用进程或者线程来执行,多个进程或者线程的运行就实现了并发的效果,不需要使用socketserver模块。
服务端:
from multiprocessing import Process
from threading import Thread
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8087))
server.listen(5)
#将通信服务的代码封装进函数内:
def talk(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0: break
print(data.decode('utf-8'))
conn.send(data.upper())
except Exception as e:
print(e)
break
conn.close()
while True:
conn, addr = server.accept()
# p=Process(target=talk,args=(conn,))
# p.start()
t = Thread(target=talk, args=(conn,))
t.start() #循环开启多个线程或进程实现并发
服务端应该具备的三个特性:
1)要有固定的ip和port,也就是说要有固定的链接地址,这样客服端才能连上服务端。
- 永不停机,让客服端能够在任意时间访问。
3)支持并发,能够让多个客户端同时访问。
客户端:
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8087))
while True:
msg = b'hallo world'
client.send(msg)
data = client.recv(1024)
print(data.decode('utf-8'))
3、线程的join方法
跟进程一个道理,join方法就是让主线程等待子线程结束后在执行
# 线程的join方法:
from threading import Thread
import time
def task():
print('hello')
time.sleep(2)
print('bye')
if __name__ == '__main__':
t = Thread(target=task)
t.start()
t.join()
print('主')
4、线程的属性及其他方法
同一个进程中的数据,该进程下的多个线程可以同享
# 同进程下,多线程同享数据
from threading import Thread
money=2
def task():
global money
money-=1
print(money)
if __name__ == '__main__':
t1=Thread(target=task)
t2=Thread(target=task)
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
'''
1
0
0'''
线程对象属性及其他方法
from threading import Thread, current_thread, active_count
import os, time
def task(n):
print('hello world', os.getpid())
print(current_thread().name) # 当前线程的线程名
print(current_thread().getName()) # 获取当前线程的线程名
time.sleep(n)
if __name__ == '__main__':
t1 = Thread(target=task, args=(1,))
t2 = Thread(target=task, args=(2,))
t1.start()
t2.start()
t1.join()
print(active_count()) # 统计当前活跃的进程
print('主', os.getpid())
print(t1.is_alive()) # 线程是否存活
# print(current_thread().getName())
5、守护进程
# 守护进程
from threading import Thread
import time
def task():
print('running ')
time.sleep(1)
print('end')
if __name__ == '__main__':
t = Thread(target=task)
t.daemon = True # 将子线程设为主线程的守护进程,主进程结束,子进程立即结束
t.start()
print('主')
注意:主线程代码执行完后不会立马结束,会等到其它非守护进程结束后才会结束,这个过程中,如果它的守护进程的运行时间短时,等到非守护进程运行结束时,守护进程实际上也已经运行结束了。
# 守护进程
from threading import Thread
import time
def foo():
print('1111')
time.sleep(1)
print('end1111')
def func():
print('2222')
time.sleep(2)
print('end2222')
if __name__ == '__main__':
t1 = Thread(target=foo)
t2 = Thread(target=func)
t1.daemon = True # 将子线程设为主线程的守护进程,主进程结束,子进程立即结束
t1.start()
t2.start()
print('主')
# 主线程代码执行完,等待它的非守护线程t2执行完,
# t2需要时间2s+,t1守护线程只需要1s+,所有t2运行完时,t1已经运行完了
'''
1111
2222主
end1111
end2222'''
6、线程互斥锁
# 线程互斥锁
from threading import Thread,Lock
import time
metex=Lock()
money=100
def task():
global money
metex.acquire() # 操作数据的时候,加锁将并发变成串行,保证数据不会错乱
tmp=money
time.sleep(0.1)
money=tmp-1
metex.release() # 操作数据结束,释放锁
#操作数据时还可以用with上下文管理自动抢锁释放锁,
#但推荐使用上面哪一种方法
#with mutex:
# tmp = money
# time.sleep(0.1)
# money = tmp -1
if __name__ == '__main__':
t_list=[]
for i in range(100):
t=Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money)
'''
0'''
8、GIL全局解释器锁
参考博客园,https://www.cnblogs.com/xiaoyuanqujing, 密码:xiaoyuanqujing@666
官网解释:
"""
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.)
"""
解读:
python解释器有多种,用c写的CPython,java写的JPytho,甚至还有用python写的python解释器,而现在我们通常用的都是第一种CPython解释器,GIL是CPython中的概率,是CPython中的一把互斥锁,用来阻止同一个进程下的多个线程的同时执行。
GIL重点知识:
- GIL不是python的特点而是CPython解释器的特点
- GIL是保证解释器级别的数据的安全
- GIL会导致同一个进程下的多个线程的无法同时执行即无法利用
- 针对不同的数据还是需要加不同的锁处理
- 解释型语言的通病:同一个进程下多个线程无法利用多核优势
GIL于普通互斥锁的区别
抢到GIL的线程进入IO,GIL自动释放
from threading import Thread,Lock
import time
mutex = Lock()
money = 100
def task():
global money
# with mutex:
# tmp = money
# time.sleep(0.1)
# money = tmp -1
mutex.acquire()
tmp = money
time.sleep(0.1) # 线程进入io,线程抢到的GIL自动释放,但互斥锁还没有释放,也就是说其他线程抢到GIL还是不能操作数据
money = tmp - 1
mutex.release()
if __name__ == '__main__':
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(money) # 0
"""
100个线程起起来之后 要先去抢GIL
我进入io GIL自动释放 但是我手上还有一个自己的互斥锁
其他线程虽然抢到了GIL但是抢不到互斥锁
最终GIL还是回到你的手上 你去操作数据
"""
我们说解释器语言都有一个通病就是同一进程下多个线程无法利用多核的优势,但并不是进程下开多线程就没有用了。
具体的看情况:任务是IO密集型还是计算密集型
如果多核条件下,任务是计算密集型,那么开多进程效率较高,省时间;如果是IO密集型,开多进程就比开多线程的消耗多的多,耗时也较长,这种情况,开多线程较好。
代码验证:
# 计算密集型
# from multiprocessing import Process
# from threading import Thread
# import os,time
#
#
# def work():
# res = 0
# for i in range(10000000):
# res *= i
#
# if __name__ == '__main__':
# l = []
# print(os.cpu_count()) # 获取当前计算机CPU个数
# start_time = time.time()
# for i in range(12):
# p = Process(target=work) # 1.4679949283599854
# t = Thread(target=work) # 5.698534250259399
# t.start()
# # p.start()
# # l.append(p)
# l.append(t)
# for p in l:
# p.join()
# print(time.time()-start_time)
# IO密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work():
time.sleep(2)
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 获取当前计算机CPU个数
start_time = time.time()
for i in range(4000):
# p = Process(target=work) # 21.149890184402466
t = Thread(target=work) # 3.007986068725586
t.start()
# p.start()
# l.append(p)
l.append(t)
for p in l:
p.join()
print(time.time()-start_time)
总的来说两者各有优缺点,我们在写项目的时候可以才用多进程下开多线程的方式,将二者的优点都用到。