一、threading模块
二、开启线程的两种方式
- 导入threading包,直接创建多线程
from threading import Thread
import time
def say(name):
time.sleep(2)
print('%s say hello' % name)
if __name__ == '__main__':
t = Thread(target=say, args=('egon',))
t.start()
print('主线程')
- 继承threding.Thread,重写run方法,需要初始化init时,要super父类。
from threading import Thread
import time
class Sayhi(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
time.sleep(2)
print("%s say hello " % self.name)
if __name__ == '__main__':
t = Sayhi('egon')
t.start()
print('主线程')
三、一个进程下开启多个线程与在一个进程下开启多个子进程的区别
运行速度
- 一个进程开启多个线程
from threading import Thread
def work():
print('hello')
if __name__ == '__main__':
# 在主进程下开启线程
t = Thread(target=work)
t.start()
print('主线程/主进程')
"""
hello
主进程/主进程
"""
- 一个进程开启多个子进程
from multiprocessing import Process
import os
def work():
print('hello')
if __name__ == '__main__':
# 在主进程下开启线程
t = Thread(target=work)
t.start()
print('主线程/主进程')
"""
hello
主进程/主进程
"""
可以看出,多线程运行速度要比多进程更快,多进程需要等待子进程执行才会继续执行主进程。多线程是并行执行,主线程和子线程可以自由执行。
PID
- 多线程
from multiprocessing import Process
import os
def work():
print('hello', os.getpid())
if __name__ == '__main__':
# 在主进程下开启多个线程,每个线程跟主进程的pid一样
t1 = Thread(target=work)
t2 = Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid ', os.getpid())
- 多进程
from threading import Thread
from multiprocessing import Process
import os
def work():
print('hello', os.getpid())
if __name__ == '__main__':
# 主进程开启多个子进程,每个子进程都有不同的pid
p1 = Process(target=work)
p2 = Process(target=work)
p1.start()
p2.start()
print('主线程/主进程pid ', os.getpid())
多线程中每个子线程的pid与主进程是一样的,多进程中子进程的pid各不相同。
共享主进程数据
- 多线程
from threading import Thread
def work():
global n
n = 0
print('子', n)
if __name__ == '__main__':
n = 1
t = Thread(target=work)
t.start()
t.join()
print('主', n)
多线程中,主进程的数据在各个子线程是共享的,大家共用一个数据源,子线程可以改变主进程的数据。
- 多进程
from multiprocessing import Process
def work():
global n
n = 0
print('子', n)
if __name__ == '__main__':
n = 100
p = Process(target=work)
p.start()
p.join()
print('主', n)
子进程可以利用主进程的数据,进行任意更改,但是不会影响主进程主进程的数据。说白了就是,可以复制然后随意使用,但是原话仍然保留不变。
四、练习
练习1
- 多线程并发socke服务器
import threading
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 8080))
s.listen(5)
def action(conn):
while True:
data = conn.recv(1024)
print(data)
conn.send(data.upper())
if __name__ == '__main__':
while True:
conn, addr = s.accept()
p = threading.Thread(target=action, args=(conn, ))
p.start()
- 客户端
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 8080))
while True:
msg = input('>>: ').strip()
if not msg:continue
s.send(msg.encode('utf-8'))
data = s.recv(1024)
print(data)
练习2:三个线程,一个接受用户输入,一个将输入内容格式化,一个将格式化后的内容存入文件中
from threading import Thread
msg_1 = []
format_1 = []
def talk():
while True:
msg = input('>>: ').strip()
if not msg:continue
msg_1.append(msg)
def format_msg():
while True:
if msg_1:
res = msg_1.pop()
format_1.append(res.upper())
def save():
while True:
if format_1:
with open('db.txt', 'a', encoding='utf-8') as f:
res = format_1.pop()
f.write('%s\n' % res)
print('写入完成')
if __name__ == '__main__':
t1 = Thread(target=talk)
t2 = Thread(target=format_msg)
t3 = Thread(target=save)
t1.start()
t2.start()
t3.start()
五、线程相关的其他方法
Thread实例对象的方法:
- isAlive():返回线程是否活动的。
- getName():返回线程名。
- setName():设置线程名。
threading模块提供的一些方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
from threading import Thread
import threading
def work():
import time
time.sleep(3)
print(threading.current_thread().getName())
if __name__ == '__main__':
t = Thread(target=work)
t.start()
print(threading.current_thread().getName())
print(threading.current_thread()) # 主线程
print(threading.enumerate())
print(threading.active_count())
print('主线程/主进程')
六、守护线程
无论是进程还是线程,守护xxx会等待主xxx运行完毕后被销毁
运行完毕并非终止运行
- 对主进程来说,运行完毕指主进程代码运行完毕
- 对主线程来说,运行完毕指主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
1、 主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,
2、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' % name)
if __name__ == '__main__':
t = Thread(target=sayhi, args=('egon', ))
t.setDaemon(True)
t.start()
print('主线程')
print(t.is_alive())
'''
主线程
True
'''
七、全局解释器锁GIL
首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。Python同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。
GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。
保护不同的数据的安全,就应该加不同的锁
1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。
2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
如果多个线程的target=work,那么执行流程是
多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行
解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码。
GIL与Lock
GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理,如下图
GIL与多线程
GIL使得,在同一个时刻同一个进程中只有一个线程被执行。
对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
分析:
我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程
单核情况下,分析结果:
如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜
多核情况下,分析结果:
如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜
结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
参考博客:《python开发线程:线程&守护线程&全局解释器锁》