并发:在一段时间内,交替执行多个任务
例如:对于单核CPU处理多任务,操作系统轮流让各个任务交替执行,假如:软件1执行0.01s,切换到软件2,软件2执行0.01s,再切换到软件3,执行0.01s,这样反复执行下去。
并行:在一段时间内,真正的同时一起执行多个任务
例如:对于多核CPU,操作系统会给CPU的每个内核安排一个执行任务(任务数小于或等于CPU核心数)
进程介绍:
在Python中,想要实现多任务可以使用进程来完成
进程:是计算机资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位,通俗理解:一个正在运行的程序就是一个进程。
创建进程的步骤
1、导入进程包
import multiprocessing
2、通过进程类创建进程对象
进程对象 = multiprocessing.Process()
Process 方法参数说明
target 执行的目标任务名,这里指的是函数名
name 进程名,一般不设置
group 进程组,目前只能使用None
3、启动进程执行任务
进程对象.start()
import multiprocessing
import time
def coding():
for i in range(3):
print("coding....")
time.sleep(0.2)
def music():
for i in range(3):
print("music....")
time.sleep(0.2)
if __name__ == '__main__':
coding_process = multiprocessing.Process(target=coding)
music_process = multiprocessing.Process(target=music)
coding_process.start()
music_process.start()
进程执行带有参数的任务
args 以元组的方式给执行任务传参
kwargs 以字典的方式给执行任务传参
当元组只有一个参数时,需要使用 逗号 结尾,以区分它是元组还是小括号。多个参数时,按照顺序对应赋值给形参。
字典传参时,key值必须和函数的形参一致
def coding(num):
for i in range(num):
print("coding....")
time.sleep(0.2)
def music(count):
for i in range(count):
print("music....")
time.sleep(0.2)
if __name__ == '__main__':
#当元组只有一个参数时,需要使用 逗号 结尾,以区分它是元组还是小括号
coding_process = multiprocessing.Process(target=coding, args=(3,))
#字典传参时,key值必须和函数的形参一致
music_process = multiprocessing.Process(target=music, kwargs={"count": 2})
coding_process.start()
music_process.start()
获取进程编号
当程序中进程的数量越来越多,就无法区分主进程和子进程以及不同的子进程,为了方便管理,每个进程都有自己的编号。通过编号区分不同的进程。
首先导入os模块,直接通过模块中的os对象获取
import os
1、获取当前进程的编号
os.getpid()
2、获取当前进程的父进程编号
os.getppid()
进程间不共享全局变量
实际上,创建一个子进程就是把主进程的资源进行拷贝产生一个新的进程,这里主进程和子进程时相互独立的。
主进程和子进程的结束顺序
主进程会等待所有子进程执行结束再结束。
打印显示主进程结束,但是主进程并没有真的结束,最下面显示 Process finished 才代表主进程结束。这就说明了主进程会等待子进程结束之后才会结束。
方法一:设置守护主进程,来实现主进程结束之后子进程也直接销毁
方法二:手动销毁子进程
在主进程结束之前,手动销毁子进程
import multiprocessing
import time
def work():
for i in range(10):
print("工作中...")
time.sleep(0.2)
print("子进程结束")
if __name__ == '__main__':
work_process = multiprocessing.Process(target=work)
work_process.start()
# 主进程休息1s ,假装执行其他工作
time.sleep(1)
# 手动销毁子进程
work_process.terminate()
print("主进程结束")
实现多任务的另一种方式----线程
在Python中,想要实现多任务还可以使用多线程来完成
进程是分配资源的最小单位,一旦创建进程就会分配一定的资源,就像聊QQ,你要和两个人聊天,就需要打开两个QQ软件,一样是比较浪费资源的。
线程是程序执行的最小单位,实际上,进程只负责分配资源,而利用这些资源执行程序的是线程,也就说进程是线程的容器,一个进程中最少有一个线程来负责执行程序,同时线程自己不拥有系统资源,只需要一点在运行中必不可少的资源(这些资源用完就会归还,不会一致占用)。但它可与同属一个进程的其他线程共享进程的所有资源。这就像通过一个QQ软件,你和两个人聊QQ,只需要打开两个窗口。
线程实现多任务的同时也节约了资源。
线程的创建步骤
1、导入线程模块
import threading
2、通过线程类创建线程对象
线程对象 = threading.Thread(target = 任务名)
target 执行的目标任务名,这里指的是函数名(方法名)
name 线程名,一般不用设置
group 线程组,目前只能使用None
3、启动线程执行任务
线程对象.start()
线程执行带有参数的任务
和进程一样
args 以元组的方式给执行任务传参
同样,元组只有一个元素需要逗号结尾,多个形参按照参数顺序进行形参赋值
kwargs 以字典方式给执行任务传参
同样,字典的key值需要和任务的形参名保持一致
主线程和子线程的结束顺序
对比进程,主线程会等待所有子线程结束后,主线程再结束
设置主线程结束后,子线程也结束
方法一:设置守护主线程
只需要在创建线程时设置:daemon=True 即可。
coding_threading = threading.Thread(target=coding,daemon=True)
方法二:同样设置,不过是通过函数设置,上面是初始化参数设置
子线程对象.setDaemon(True)
coding_threading.setDaemon(True)
线程间的执行顺序
线程之间的执行顺序是无序的,他们之间顺序是通过CPU的调度决定的
代码演示:
通过threading.current_thread()获取线程信息。在sleep时,可能CPU被其他线程获取执行权,每次所以打印顺序不同。证明了线程之间的执行顺序是无序的,通过CPU调度决定
线程之间共享全局变量
这点和进程不同,进程是将所有资源拷贝一份到自己进程中。
线程:多个线程都是在同一个进程中,多个线程使用的资源都是同一个进程中的资源,因为多线程之间是共享全局变量的。
代码演示:
import threading
import time
# 全局变量
my_list = []
def write_data():
for i in range(3):
print("add", i)
my_list.append(i)
def read_data():
print("read:", my_list)
if __name__ == '__main__':
write_thread = threading.Thread(target=write_data)
read_thread = threading.Thread(target=read_data)
write_thread.start()
time.sleep(0.5) # 因为线程执行无序,可能还没写完就读了,所以保证读取是在写入之后
read_thread.start()
线程之间共享全局变量数据出现错误问题
import threading
# 全局变量
g_num = 0
def sum_num1():
global g_num
for i in range(10000000):
g_num += 1
print("sum1:", g_num)
def sum_num2():
global g_num
for i in range(10000000):
g_num += 1
print("sum2:", g_num)
if __name__ == '__main__':
sum1_threading = threading.Thread(target=sum_num1)
sum2_threading = threading.Thread(target=sum_num2)
sum1_threading.start()
sum2_threading.start()
线程之间全局变量是共享的。
sum1 线程从 g_num获取值为0,进行加1操作,然后再将1赋值给全局变量g_num,如果sum1线程在将赋值给全局变量这步时,失去了CPU的执行权,sum2线程获取全局变量的值仍然为0,线程sum2执行加1操作。并将加1后的值赋值给全局变量。这时两个线程都处于将值返回给全局变量的过程中,那么最后的结果就是,进行两次加1操作,但是,全局变量的值仍然为1。
解决方案
方案一: 线程同步
保证在同一时刻只能有一个线程去操作全局变量(例如上面代码,只能在线程进行加1操作并且赋值给全局变量之后,其他线程才能获取到全局变量的值)
通过互次锁实现
互次锁:对共享数据进行锁定,保证同一时刻只有一个线程去操作。多个线程同时抢锁,抢到锁的线程先执行,没有抢到锁的线程进行等待。等锁释放完之后,其他线程再去抢这个锁。
使用互次锁:
1、互次锁的创建
mutex = threading.Lock()
2、上锁
mutex.acquire()
3、释放锁
mutex.release()
演示代码:
import threading
# 全局变量
g_num = 0
def sum_num1():
# 上锁
mutex.acquire()
global g_num
for i in range(10000000):
g_num += 1
print("sum1:", g_num)
# 解锁
mutex.release()
def sum_num2():
mutex.acquire()
global g_num
for i in range(10000000):
g_num += 1
print("sum2:", g_num)
mutex.release()
if __name__ == '__main__':
# 创建锁
mutex = threading.Lock()
sum1_threading = threading.Thread(target=sum_num1)
sum2_threading = threading.Thread(target=sum_num2)
sum1_threading.start()
sum2_threading.start()
mutex就是锁,但是注意,想要实现串行化,必须使用的是同一把锁。
这样虽然不会出现数据错误问题,但是这样效率低,和单线程差不多。
进程和线程的区别
1、关系对比
1)、线程是依附在进程里面的,没有进程就没有线程
2)、一个进程默认提供一条线程
2、区别对比
1)、进程之间不共享全局变量
2)、线程之间共享全局变量,但是要注意资源竞争问题,解决办法:互次锁或者线程同步
3)、创建进程的开销比创建线程大
4)、进程是资源分配的基本单位,线程是CPU调度的基本单位
5)、线程不能独立执行,必须依附在进程中
3、优缺点对比
进程优缺点:
优点:可以使用多核 缺点:资源开销大
线程优缺点:
优点:资源开销小 缺点:不能使用多核
一个进程只能使用到一个内核,所以线程之间只能争夺一个内核(CPU)执行权