Python 多任务 (多进程multiprocessing、多线程threading)

多任务简述

说明:
由于现在电脑一般都是多核CPU,所用的操作系他也是多任务操作系他,因此如果在同一时间内做多件事,会使得效率大幅提高,这就是多任务的优势。

分类:

  • 目前实现多任务可以通过两种方法:

  • 并发(在一段时间内交替执行多个任务,在某个时刻实际上只有一个任务在执行,只是每个任务交替执行的时间很短,任务交替执行的切换速度很快,在宏观上看起来就像是同时在做多件事情,一般用于单核CPU)

  • 并行(在一段时间内,多核CPU中的每个核心单独做一件事,这是真正的同时在做多件事情)

  • 途径有:

  • 多进程(资源开销大,是真正的并行)

  • 多线程(资源开销小,但实际上是并发)

进程实现多任务

进程:
进程(Process)是资源分配的最小单位,它是操作系统进行资源分配和调度运行的基本单位,可以简单理解为一个正在运行的程序就是一个进程.例如:正在运行的QQ,微信等,他们都是一个进程。一个程序至少拥有一个进程。

默认情况下,程序运行后会创建一个主进程,用来执行代码。但我们可以通过创建子进程的方式,让操作系统为每个进程分配处理机(CPU),从而使得程序可以在多个进程下同时处理多件事,实现多任务。

进程的使用

# 导入多线程包
import multiprocessing as mtp
# 设计进程编号需要导入os
import os


# 测试函数
def test():
    print("启动多任务")
    
# 创建进程类示例
'''
target :执行的目标任务名(函数名)
args   :传递给任务的参数 - 元组形式(无参数时可省略) (注意一个参数时元组的写法,传参须按顺序)
kwargs :传递给任务的参数 - 字典形式(无参数时可省略) (传参按照函数的参数键传递)
name   :进程名(一般不用设置)
grop   :进程组
'''
newProcess = mtp.Process(target = test,name='进程名1',group=None) 
newProcess.daemon = False # 设置创建的子进程为非守护主进程模式(详见下面的注意点3)

# 启动创的进程执行任务
newProcess.start()

# 获取进程编号
## 获取当前进程编号
pid = os.getpid()
print('pid:',pid)

## 获取当前进程的父进程编号
ppid = os.getppid()
print('ppid:',ppid)
pid: 7360
ppid: 21960

示例

import multiprocessing
import time
import os

def test1(times):
    for i in range(times):
        print("执行多进程任务1")
        time.sleep(0.5)

def test2(times):
    for i in range(times):
        print("执行多进程任务2")
        time.sleep(0.5)

newProcess1 = multiprocessing.Process(target = test1,args=(4,))
newProcess2 = multiprocessing.Process(target = test2,kwargs = {'times':4})

if __name__ == '__main__':
    # 启动创的进程执行任务
    startTime = time.time()
    print("【单进程演示开始】")
    test1(4)
    test2(4)
    endTime = time.time()
    print(endTime - startTime)
    print("【多进程演示开始】")
    startTime = time.time()
    newProcess1.start()
    newProcess2.start()
    newProcess1.join()
    newProcess2.join()
    endTime = time.time()
    print('time',endTime - startTime)

【单进程演示开始】
执行多进程任务1
执行多进程任务1
执行多进程任务1
执行多进程任务1
执行多进程任务2
执行多进程任务2
执行多进程任务2
执行多进程任务2
time 4.047036647796631
【多进程演示开始】
执行多进程任务1
执行多进程任务2
执行多进程任务2
执行多进程任务1
执行多进程任务2
执行多进程任务1
执行多进程任务1
执行多进程任务2
time 2.1405797004699707

注意点

  • 1.主进程会在所有子进程都结束之后再结局
  • 2.主进程和子进程虽然是上下级的关系,但其实主进程和子进程也是同时执行的,并不是先执行子进程后再执行主进程
  • 3.如果想要实现主进程结束后结束所有子进程,则需要设置创建的子进程为守护主进程模式

线程实现多任务

线程: 线程是程序执行的最小单位,其同样可以用来实现多任务。但是如果使用多进程来实现多任务是比较耗费资源的,因为进程是分配资源的最小单位,一旦创建一个进程就必须要向其分配一定的资源,就像跟两个人聊QQ就需要打开两个QQ软件一样,很明显这是比较浪费资源的。

一个程序至少有一个进程,而一个进程至少有一个线程,也就说进程是线程的容器。

实际上进程只负责分记资源,而利用这些资源执行程序的是线程,同时线程自己不拥有系统资源,只需要一点儿在运行中必不可少的资源,但线程可与同属一个进程的其它线程共享进程所拥有的全部资源.这就像通过一个QQ软件(一个进程)打开两个窗口(两个线程)跟两个人聊天一样,实现多任务的同时也节省了资源。

线程的使用

# 导入多线程包
import threading
import time


# 测试函数
def test1(times):
    for i in range(times):
        print("正在执行多线程1")
        time.sleep(3)
    
def test2(times):
    for i in range(times):
        print("正在执行多线程2")
        time.sleep(3)
    
# 创建线程类示例
'''
target :执行的目标任务名(函数名)
args   :传递给任务的参数 - 元组形式(无参数时可省略) (注意一个参数时元组的写法,传参须按顺序)
kwargs :传递给任务的参数 - 字典形式(无参数时可省略) (传参按照函数的参数键传递)
daemon : 是否设置为守护线程
name   :线程名(一般不用设置)
grop   :线程组
'''
newThread1 = threading.Thread(target = test1 , args = (3,) , daemon = True)
newThread2 = threading.Thread(target = test2 , kwargs = {'times':3} , daemon = True)

# 启动创的线程执行任务
print("【单线程演示开始】")
test1(3)
test2(3)
print("【多线程演示开始】")
newThread1.start()
newThread2.start()
newThread1.join()
newThread2.join()
print("【多线程演示结束】")
【单线程演示开始】
正在执行多线程1
正在执行多线程1
正在执行多线程1
正在执行多线程2
正在执行多线程2
正在执行多线程2
【多线程演示开始】
正在执行多线程1
正在执行多线程2
正在执行多线程1正在执行多线程2

正在执行多线程1正在执行多线程2

【多线程演示结束】

在上面的输出中,出现的奇怪的换行,这是因为线程的执行顺序是无序的。

线程的互斥

引入:
在之前的说明中,我们提到了线程共享进程的资源,那么线程也必定共享进程的变量资源,倘若我们有俩线程thread1,thread2。thread1对共享变量(初始值为0)做100次+1的操作,而thread2对刚才的共享变量做100次-1的操作,我们让这俩线程启动后,希望的值应该为0。但是实际上这个值是不确定的,可能是任意值。

import threading
total = 0

def add():
    global total
    for i in range(100):
        total += 1

def desc():
    global total
    for i in range(100):
        total -= 1

thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(total)
-46

说明:
我们这里假设每个线程只做一次+1或-1的操作,简要的分析输出结果为-1的情况:

  • 1.thread1获取了共享变量temp的初始值0,在准备对其进行+1操作时,被操作系统剥夺处理机资源(CPU),然后其将保存被剥夺前的线程的相关信息(包括这个共享变量temp的值:0)
  • 2.被剥夺的处理机资源给到了thread2,获取到了temp的值(0),并且其也在准备进行-1操作时被操作系他剥夺处理机资源,其也保存被剥夺前的自身线程的相关信息(包括这个共享变量temp的值:0)
  • 3.处理机资源重新给到thread1,其用到之前保存下来的temp变量的值进行了+1操作,并将结果赋值给共享变量(此时共享变量值为0+1 = 1)
  • 4.处理机资源重新给到thread2,其用到之前保存下来的temp变量(这里的temp值为0,虽然进程中temp的值刚刚被修改为1,但是线程不会去使用进程中的值,而是使用其保存的值)的值进行了-1操作,并将结果赋值给共享变量temp,此时temp的值为0-1=-1,并把结果赋值给temp。
  • 5.最后输出temp的值:-1

为了防止这种情况的发生,我们希望对共享的一些资源进行可控的管理,因此引入了线程的互斥(对某一临界资源(也就是希望被可控管理的共享资源)在某一时刻,只允许一个线程去进行获取、修改等所有与之相关的操作),并使用互斥信号量mutex来管理线程的互斥,下面例子中的lock变量就可以作为互斥信号量。互斥信号量一般初始值为1,只有当互斥信号量等于1时才能把临界资源分配给某个线程,当将临界资源分配给某个线程后,互斥信号量将-1;当线程释放临界资源后,互斥信号量+1

代码示例
Lock
import threading
from threading import Lock # 互斥锁

lock = Lock() # 创建锁实例
total = 0 # 临界资源(共享变量)

def add():
    global total
    global lock
    for i in range(100):
        lock.acquire() # 使用lock.acquire函数尝试获取锁,如果成功获取锁则可执行下面代码,否则将一直被阻塞在这,直至获得锁
        total += 1
        lock.release() # 该线程对临界资源(共享变量)的使用已经结束了,可以释放该资源给其他线程使用了,所有其释放锁

def desc():
    global total
    global lock
    for i in range(100):
        lock.acquire()
        total -= 1
        lock.release()

thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(total)


'''
lock的常用方法

acquire(blocking=True, timeout=-1): 尝试获取锁。如果 blocking 为 True(默认值),则线程会阻塞直到获取到锁;如果设置为 False,则不会阻塞,如果无法立即获取锁会返回 False。timeout 用于指定阻塞的超时时间(以秒为单位),超过时间未获取到锁则返回 False。

release(): 释放锁。

locked(): 如果锁被某个线程持有,返回 True


'''
0
RLock

Rlock是为了解决在同一个线程中锁的多次acquire问题。但是一定要注意在一个线程中需要保证acquire和release的次数要一样。

import threading
from threading import RLock
lock = RLock()
total = 0

def add():
    global total
    global lock
    for i in range(100):
        lock.acquire()
        lock.acquire()
        total += 1
        lock.release()
        lock.release()

def desc():
    global total
    global lock
    for i in range(100):
        lock.acquire()
        total -= 1
        lock.release()

thread1 = threading.Thread(target=add)
thread2 = threading.Thread(target=desc)
thread1.start()
thread2.start()

thread1.join()
thread2.join()
print(total)
0

线程的同步

引入:
在某些情况下,我们需要某个线程先执行,某个线程后续才能正常运行。比如著名的消费者与生产者问题。生产者进程只有先执行,生产资源后,消费者线程才能进行消费。并且这种情况下,共享变量一般初始值为0,最大值不为1,而是视情况而定,也有可能没有最大值,这种共享变量在操作系他原理中我们称之为同步信号Semaphore。与互斥信号量类似,只有当同步信号量大于0时才能把临界资源分配给某个线程,当将临界资源分配给某个线程后,同步信号量将-1;当线程释放临界资源后,同步信号量+1

代码示例
import time
import threading

se = threading.Semaphore(0)  # se1同步信号量的初始值是0 表示生产者生产的商品剩余个数

# 消费者1线程函数
def customer1():
    global se
    print("customer1请求se信号量。")
    se.acquire()  # 向se信号量请求一个信号 表示消费产品
    print("customer1请求信号量成功")
    # 这里不同于之前的mutex变量 其不进行释放资源 表示资源(生产者产生的产品已被消费)

# 消费者2线程函数        
def customer2():
    global se
    print("customer2请求se信号量。")
    se.acquire()  # 向se信号量请求一个信号
    print("customer2请求信号量成功")

# 生产者线程函数
def producer():
    # 释放两个se信号 表示生产了两件产品 
    se.release()
    se.release()


# 主线程函数
def main():
    t1 = threading.Thread(target=producer,name="thread_producer",daemon=True)    # 创建producer子线程
    t2 = threading.Thread(target=customer1,name="thread_customer1",daemon=True)  # 创建cusotmer1子线程
    t3 = threading.Thread(target=customer2,name="thread_customer2",daemon=True)  # 创建customer2子线程
    t1.start()  # 启动producer线程 
    time.sleep(5) # 确保消费者先生产商品
    t2.start()  # 启动customer1线程
    t3.start()  # 启动customer2线程
    t1.join()   
    t2.join()   
    t3.join()  
    print("主线程运行结束!")
    
    
    
if __name__ == "__main__":
    main()
customer1请求se信号量。customer2请求se信号量。
customer1请求信号量成功

customer2请求信号量成功
主线程运行结束!

线程通讯

引入:
实际上,在上面的生产者消费者代码中,就已经用到了一种线程间通讯的方式–“全局变量(共享变量)”,除此之外,我们还可以使用queue消息队列来进行线程间通讯。发送线程可以通过put方法发送消息到queue信息队列,接收线程则使用get方法从信息队列中取消息。

代码示例
import threading 
import queue
import time

#创建一个队列,参数为信息队列设定的最大长度
queue = queue.Queue(1)  #创建一个先进先出的队列
count = 0
lock = threading.Lock()

#定义生产者线程类
class Producer(threading.Thread):
    
    def run(self):
        global queue,count,lock
        while True:
            time.sleep(4)
            print("生产线程开始生产数据")
            lock.acquire()
            count+=2   # 假定其一次生产俩产品
            msg="产品{}".format(count)
            lock.release()
            queue.put(msg)        # 默认阻塞
            print(msg)
            
#定义消费者线程类
class Consumer(threading.Thread):
    
    def run(self):
        global queue,count,lock
        while True:
            print('消费者线程开始消费线程了')
            msg=queue.get()       # 默认阻塞
            print('消费线程得到了数据:{}'.format(msg))
            lock.acquire()
            print("消费者消费了一个产品")
            count-=1
            lock.release()
            time.sleep(4)

if __name__ == '__main__':
    t1 = Producer()
    t2 = Consumer()
    t1.start()
    t2.start()
消费者线程开始消费线程了
生产线程开始生产数据
产品2
消费线程得到了数据:产品2
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了

产品3
消费线程得到了数据:产品3
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了
产品4

消费线程得到了数据:产品4
消费者消费了一个产品
消费者线程开始消费线程了生产线程开始生产数据
产品5

消费线程得到了数据:产品5
消费者消费了一个产品
生产线程开始生产数据消费者线程开始消费线程了
产品6

消费线程得到了数据:产品6
消费者消费了一个产品
消费者线程开始消费线程了
生产线程开始生产数据
产品7
消费线程得到了数据:产品7
消费者消费了一个产品
消费者线程开始消费线程了生产线程开始生产数据
产品8

消费线程得到了数据:产品8
消费者消费了一个产品
...(程序为死循环,将一直运行)

注意点

  • 1.线程内部的调度顺序是无序的,即有可能出现最先调用start方法启动的线程在最后才被调度启动的情况。因为i一个进程下的所有线程共享进程所有的资源,当然包括CPU的调度机会,而一个进程只能获取一个CPU的调度机会,因此一个进程内在某一时刻最多只有一个线程在被调度运行,其他线程处于就绪态。所有进程中的多任务是并发的
  • 2.主线程会在所有子线程都结束之后再结局
  • 3.如果想要实现主线程结束后结束所有子线程,则需要设置创建的子线程为守护主线程模式
  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NUDTer2026

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值