【Python】多线程
概念
同一时间内能够执行多个任务
执行方式
并发:多个任务交替去运行
并发:多喝CPU中,多个任务是在多个CPU上运行的
一般来说:并发和并行是同时存在的,是操作系统自动调用的
实现方式
- 多进程实现
- 多线程实现
- 协程实现
进程的概念
概念
- 程序:安装的程序,写的代码
- 进程:运行起来的程序就是进程,是操作系统进行资源分配的基本单位
- 线程:一个进程中会默认有一个线程,线程是真正执行任务的
作用
进程是实现多任务的一种方式
进程状态
- 新建态 创建一个进程,分配资源
- 就绪态 只差时间片
- 运行态 获取到了时间片
- 阻塞态 等待外部条件的满足
- 死亡态 进程执行结束
注意
- 只能是有就绪态到运行态
- 阻塞的条件满足,就再次进入就绪态,等待CPU
进程类
- 导包
import multiprocessing
- 函数
Process([group [,target [,name [,args [, kwargs]]]]])
'''
group: 指定进程组,目前只能None
target:执行的目标名
name : 进程名字
args: 以元组的方式给执行进程传参
kwargs:以字典的方式给执行进程传参
'''
- Process常用的方法
start():启动子进程实例(创建子进程)
join():等待子进程结束
# 书写在哪个进程,哪个进程阻塞式等待
# 哪个进程.join() 就是等待哪个进程执行结束
terminate():不管任务是是否完成,终止子进程
import multiprocessing
import time # 让进程休息多少秒
def sing():
for i in range(5):
print('唱歌')
time.sleep(0.1) # 手动让进程进入阻塞态,单位是s
def dance():
for i in range(5):
print('跳舞')
time.sleep(0.1)
if __name__ == '__main__':
# 创建进程对象
process1 = multiprocessing.Process(target=sing)
process2 = multiprocessing.Process(target=dance)# 不能加()否则成了函数调用
# 启动进程对象
process1.start()
process2.start()
# 等待结束
process1.join()
process2.join()
- 获取进程的pid
# 获取当前的进程对象
multiprocessing.current_process()
import multiprocessing
import time
import os
def sing():
print(multiprocessing.current_process().name) # 获取name
for i in range(5):
print('唱歌')
time.sleep(0.1)
def dance():
print(multiprocessing.current_process().name)
for i in range(5):
print('跳舞')
time.sleep(0.1)
if __name__ == '__main__':
# 获取当前进程的id
print(multiprocessing.current_process().name)
# 创建进程对象
process1 = multiprocessing.Process(target=sing)
process2 = multiprocessing.Process(target=dance)
# 启动进程对象
process1.start()
process2.start()
# 查看进程的pid
# print(os.getpid())
# 等待结束
process1.join()
process2.join()
import os
os.getpid() # 获取进程的pid
os.getppid() # 获取进程的ppid
os.kill(pid,9) # 杀死指定的进程
# 获取进程的id的第二种方法:
multiprocessing.current_process().pid
进程注意点
- 进程之间不共享全局变量
import multiprocessing
import time
g_list = []
def add_data():
for i in range(5):
g_list.append(i)
print(f'添加了 {i}')
time.sleep(0.1)
print('add_data:', g_list)
def read():
print('read:', g_list)
if __name__ == '__main__':
process_add = multiprocessing.Process(target=add_data)
process_read = multiprocessing.Process(target=read)
process_add.start()
process_add.join() # 阻塞等待进程执行完毕
process_read.start()
process_read.join()
- 主进程会等待子进程执行结束再结束
主进程结束,意味着程序结束。理论上,一个进程结束了,主进程必须结束
import multiprocessing
import time
def func():
for i in range(5):
print(i)
time.sleep(0.5)
print('子进程运行结束')
if __name__ == '__main__':
fork = multiprocessing.Process(target=func)
fork.start()
time.sleep(0.5)
print('主进程运行结束,理论上程序结束')
- 想让子进程随着主进程结束而结束
- 方法一:terminate()
# 使用方法 terminate()
import multiprocessing
import time
def func():
for i in range(5):
print(i)
time.sleep(0.5)
print('子进程运行结束')
if __name__ == '__main__':
fork = multiprocessing.Process(target=func)
fork.start()
time.sleep(0.5)
print('主进程运行结束...')
fork.terminate()
'''
0
主进程运行结束...
'''
- 方法二:将子进程设置为守护进程 daemon = True
if __name__ == '__main__':
fork = multiprocessing.Process(target=func)
fork.daemon = True # 先设置再运行
fork.start()
time.sleep(0.5)
print('主进程运行结束...')
'''
0
主进程运行结束...
'''
线程
- 线程是进程中执行的一个的分支
- 线程依附在继承中
- 线程是CPU调度的基本单位
作用
实现多任务
多线程的使用
- 导入线程模块
import threading
- 创建线程对象
Tread([group [,target [,name [,args [, kwargs]]]]])
-
group 线程组,目前只能使用None
-
target 执行目标任务名
-
args 元组的形式传参
-
kwargs 字典的形式传参
-
name 线程名 一般不用设置
-
线程启动
线程名.start()
import threading
import time
def sing():
for i in range(5):
print('sing')
time.sleep(0.1)
def dance():
for i in range(5):
print('dance')
time.sleep(0.1)
if __name__ == '__main__':
thread1 = threading.Thread(target=sing)
thread2 = threading.Thread(target=dance)
thread1.start()
thread2.start()
'''
sing
dance
sing
dance
sing
dance
sing
dance
sing
dance
'''
- 查看线程名
threading.current_thread().name
线程传参
import threading
import time
def sing(name, work):
for i in range(5):
print(f'{name}sing{work}...')
time.sleep(0.1)
def dance(name):
for i in range(5):
print(f'{name}dance...')
time.sleep(0.1)
if __name__ == '__main__':
thread1 = threading.Thread(target=sing, args=('周深', '大鱼海棠'))# 元组传参
thread2 = threading.Thread(target=dance, kwargs={'name': '探戈'})# 字典传参
thread1.start()
thread2.start()
'''
周深sing大鱼海棠...
探戈dance...
探戈dance...
周深sing大鱼海棠...
周深sing大鱼海棠...
探戈dance...
探戈dance...周深sing大鱼海棠...
周深sing大鱼海棠...探戈dance...
'''
线程的注意点
- 线程之间是无序的 – 就是抢占资源,所以是随机的
- 主线程会等待所有的子线程执行结束再结束
证明:
import threading
import time
def sing():
for i in range(5):
print('sing...')
time.sleep(0.1)
def dance():
for i in range(5):
print('dance...')
time.sleep(0.1)
if __name__ == '__main__':
thread1 = threading.Thread(target=sing)
thread2 = threading.Thread(target=dance)
thread1.start()
thread2.start()
time.sleep(0.3)
print('主线程结束,理论上程序结束')
'''
sing...
dance...
sing...
dance...
sing...
dance...
主线程结束,理论上程序结束
sing...dance...
sing...dance...
'''
想让线程随着进程一起结束可以使用以下方法:
# 方法:设置为守护线程daemon = True
- 线程之间共享全局变量
import threading
g_list = []
def add():
for i in range(5):
g_list.append(i)
print(g_list)
def show_global():
print(g_list)
if __name__ == '__main__':
thread = threading.Thread(target=add)
show_global()
print('-' * 30)
thread.start()
print('-' * 30)
show_global()
'''
[]
------------------------------
[0, 1, 2, 3, 4]
------------------------------
[0, 1, 2, 3, 4]
'''
- 线程直接共享全局变量数据出现错误问题
# 创建2个线程,对全局变量进行处理10000000次,看全局变量的大小
import threading
g_num = 0
def run():
global g_num
for i in range(10000000):
g_num += 1 # 该操作不是原子的
print(threading.current_thread().name, g_num)
if __name__ == '__main__':
thread1 = threading.Thread(target=run)
thread2 = threading.Thread(target=run)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f'g_num:{g_num}')
'''
# 每次的运行结果都不一样
Thread-2 12233499
Thread-1 12531189
g_num:12531189
'''
解决方法
- 线程同步解决
阻塞式等待
thread1.start()
thread1.join() # 先搞完线程1 再去搞线程2
thread2.start()
thread2.join()
- 互斥锁解决
互斥锁
概念
对共享数据进行锁定,保证同一时间只有一个线程去访问
作用
保护共享数据,避免资源竞争
使用
# 1.创建锁
mutex = threading.Lock()
# 2.上锁
mutex.acquire()
# 3,释放锁
mutex.release()
import threading
mutex = threading.Lock()
g_num = 0
def run():
global g_num
mutex.acquire() # 上锁
for i in range(10000000):
g_num += 1
mutex.release() # 解锁
print(threading.current_thread().name, g_num)
if __name__ == '__main__':
thread1 = threading.Thread(target=run)
thread2 = threading.Thread(target=run)
thread1.start()
thread2.start()
thread1.join()
thread2.join()
print(f'g_num:{g_num}')
'''
Thread-1 10000000
Thread-2 20000000
g_num:20000000
'''
死锁
概念
一直使得线程在等待
危害
在造成应用程序停止响应,不能再处理其他的任务
import threading
mutex1 = threading.Lock()
mutex2 = threading.Lock()
mutex3 = threading.Lock()
g_num = 0
def run():
mutex1.acquire()
mutex2.acquire()
mutex3.acquire()
print('run')
return # 不释放锁直接返回
def printf():
mutex3.acquire()
mutex2.acquire()
mutex1.acquire()
print('printf')
if __name__ == '__main__':
thread1 = threading.Thread(target=run)
thread2 = threading.Thread(target=printf())
thread1.start()
thread2.start()
进程和线程的对比
关系对比
- 线程是依附在进程内部的,没有进程就没有线程
- 一个进程默认只有一个线程,进程可以创建多个线程
关系对比
- 进程不共享全局变量
- 线程之间共享全局变量,但是要注意资源竞争的问题,解决方法,线程同步或者互斥锁
- 创建进程的资源开销大于创建线程的
- 进程是OS分配的基本单位,线程是CPU调度的基本单位
- 线程不能独立执行,进程能够独立进行
- 多线程开发比但进程多线程开发稳定性强
优缺点对比
-
进程的优缺点
- 优点:可以多核
- 缺点:资源开销大
-
现成的优缺点
- 优点:资源开销小
- 缺点:不能使用多核
-
进程适合 计算密集型(大量的数学计算)
-
线程适合 大量的IO密集型(读写操作,爬虫)
GIL
全局解释器锁
GIL保证同一时间,只有一个线程使用CPU
一个进程有一个锁,GIL不是python的特性,只是Cpython解释器的概念,历史遗留问题
GIL锁什么时候释放
- 当前线程执行超时的时候
- 再当前线程执行阻塞式等待会自动释放(input/io)
- 当前执行完成时
GIL的弊端
GIL对计算密集型的程序会产生影响,因为计算密集型会占用系统资源
GIL相当于单线程运算
对IO密集型影响不大,因为等的输入输出足够消耗时间了,单线程,多线程都得等
解决方法
- 更换解释器
- 使用多继承解决多线程
- 子进程使用C语言实现