多进程和多线程

我只是一个虾纸丫 提交于 2019-12-16 04:55:42

多进程和多线程

一、多任务

生活中,你可能一边听歌,一边写作业;一边上网,一边吃饭。。。这些都是生活中的多任务场景。电脑也可以执行多任务,比如你可以同时打开浏览器上网,听音乐,打开pycharm编写代码…。简单的说多任务就是同一时间内运行多个程序

  • 单核CPU实现多任务原理:操作系统轮流让各个任务交替执行,QQ执行2us,切换到微信,在执行2us,再切换到陌陌,执行2us……。表面是看,每个任务反复执行下去,但是CPU调度执行速度太快了,导致我们感觉就行所有任务都在同时执行一样

  • 多核CPU实现多任务原理:真正的秉性执行多任务只能在多核CPU上实现,但是由于任务数量远远多于CPU的核心数量,所以,操作西永也会自动把很多任务轮流调度到每个核心上执行

  • 并发和并行

    • 并发:当有多个线程在操作时,如果系统只有一个CPU,则它根本不可能真正同时进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间 段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状。.这种方式我们称之为并发(Concurrent)。
    • 并行:当系统有一个以上CPU时,则线程的操作有可能非并发。当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
  • 实现多任务的方式:

    • 多进程模式;
    • 多线程模式;
    • 协程。

二、多进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,
是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

对于操作系统来说,一个任务就是一个进程。比方说打开浏览器就是启动一个浏览器的进程,在打开一个记事本就启动一个记事本进程,如果打开两个记事本就启动两个记事本进程。

  • 优点:
    • 稳定性高,一个进程崩溃了,不会影响其他进程
  • 缺点:
    • 创建进程开销巨大
    • 操作系统能同时运行进程数目有限

1.进程创建

在linux下可以使用fork函数创建进程,在windows系统上可以引入multiprocessing模块,创建进程。我们可以使用multiprocessing模块中Process类创建新的进程

Process类说明

方法名称 参数 功能
__init__() name:进程名称
args:任意位置参数
kwargs:任意关键字参数
target:进程实例所调动的对象
group:一般用不到
构造方法
start() 启动进程
terminate() 结束进程
join() timeout:等等秒数,可选 是否等等进程执行结束
run() 如果没有给定target参数,
对这个对象调用start()方法时,
就将执行对象中的run()方法
is_alive() 判断进程实例是否还在执行
常用属性 说明
name 进程名称
pid 当前进程的PID
import multiprocessing
import time

def child(interval):
    print("子进程开始")
    time.sleep(interval)
    print("子进程结束")

if __name__ == "__main__":
	print("父进程开始")
    p1 = multiprocessing.Process(target = child, args = (1,))
    p1.start()

    print("cpu数目:" + str(multiprocessing.cpu_count()))
    for p in multiprocessing.active_children():
        print("子进程名称:p.name:" + p.name + "\tp.id" + str(p.pid))
    print("父进程结束")
  • 进程间不能共享数据
import os
from multiprocessing import Process,Pool
from time import sleep
from random import randint

num = 100
def work():
    print("子进程开始")
    global num
    num += 1
    print("子进程:num=",num)
    print("子进程结束")
if __name__ == "__main__":
    print("主进程开始")
    p = Process(target=work)
    p.start()
    p.join()
    print("主进程:num=", num)
    print("主进程结束")

2.进程同步

临界资源(临界区):指一次只能允许一个进程使用的共享资源称为临界资源;

同步:指为完成某种任务而建立的两个和多个进程,这些进程在合作的过程中需要协调工作次序进行有序的访问而出现等待所产生的制约关系。

互斥:指两个或多个进程访问临界资源时只能一个进程访问,其他进程等待的一种相互制约的关系。

import time
# 文件内容是一个数字:100
def shakedown(fileName,lock):
    with lock:
        with open(fileName,'r') as fp:
            num = int(fp.read().strip())
            num -= 1
            print("当前票数:{} ".format(num))
            time.sleep(3)
        with open(fileName,'w') as fp:
            fp.write(str(num))

if __name__ == '__main__':
    from multiprocessing import Process,Lock
    lock = Lock()
    for i in range(5):
        p = Process(target=shakedown,args=('1.txt',lock))
        p.start()

3.进程通信

进程之间是隔离的,要实现数据共享需要通过进程通信实现,进程通信可以通过队列和管道进行。队列Queue用于创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。

q.put方法用以插入数据到队列中
put方法还有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,该方法会阻塞timeout指定的时间,直到该队列有剩余的空间。
如果超时,会抛出Queue.Full异常。如果blocked为False,但该Queue已满,会立即抛出Queue.Full异常。

q.get方法可以从队列读取并且删除一个元素。
get方法有两个可选参数:blocked和timeout。
如果blocked为True(默认值),并且timeout为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty异常。
如果blocked为False,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty异常.

q.empty():调用此方法时q为空则返回True,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full():调用此方法时q已满则返回True,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize():返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()和q.full()一样
  • 队列
from multiprocessing import Queue,Process
from time import sleep
from random import randint
q = Queue(3)

def consume(q):
   while 1:
       sleep(randint(1,3))
       res = q.get()
       if  res:
           print("消费者拿到了:{}".format(res))
       else:
           break

def product(q):
   for i in range(5):
       sleep(randint(1, 3))
       print("生产者生产了:包子%d"%i)
       q.put("包子%d"%i)

if __name__ == "__main__":
   q = Queue()
   c = Process(target=consume,args=(q,))
   c.start()
   product(q)
   q.put(None)
   c.join()
   print("main")
  • 管道
Pipe([duplex]):在进程之间创建一条管道,并返回元组(conn1,conn2),其中conn1,conn2表示管道两端的连接对象,强调一点:必须在产生Process对象之前产生管道
参数:dumplex默认管道是全双工的,如果将duplex射成False,conn1只能用于接收,conn2只能用于发送。
返回值:
    conn1.recv():接收conn2.send(obj)发送的对象。如果没有消息可接收,recv方法会一直阻塞。如果连接的另外一端已经关闭,那么recv方法会抛出EOFError。
    conn1.send(obj):通过连接发送对象。obj是与序列化兼容的任意对象

4.进程池

开多进程的目的是为了并发,如果有多核,通常有几个核就开几个进程,进程开启过多,效率反而会下降,但很明显需要并发执行的任务要远大于核数,这时我们就可以通过维护一个进程池来控制进程数目。

进程池Pool可以提供指定数量的进程,供用户调用,当有新的请求提交到pool中时,如果池还没有满,那么就会创建一个新的进程用来执行该请求;但如果池中的进程数已经达到规定最大值,那么该请求就会等待,直到池中有进程结束,就重用进程池中的进程。

Pool(self,[processes  [,initializer [, initargs]]]):创建进程池
参数:
    processes:要创建的进程数,如果省略,将默认使用cpu_count()的值
    initializer:是每个工作进程启动时要执行的可调用对象,默认为None
    initargs:是要传给initializer的参数组
返回值:进程池对象
常见方法
   close()  关闭进程池
   terminate() 立即终止所有工作进程,同时不执行任何清理或结束任何挂起工作。
   join() 等所有进程退出,这个方法只能在close或terminate后执行
  • apply_async 非阻塞模式,池子中进程异步运行

    apply_async(self, func, args=(), kwds={}, callback=None,error_callback=None)

import os
from multiprocessing import Process,Pool
from time import sleep
from random import randint

def worker(num):
    print("进程{}开始".format(num))
    sleep(randint(1,3))
    print("进程{}结束".format(num))
if __name__ == "__main__":
    pool = Pool(3)
    for x in range(5):
        pool.apply_async(worker,(x,))
    pool.close()
    pool.join()
    print("主进程结束")
  • apply 阻塞模式,池子中进程同步执行,也就是一个进程执行完毕后再执行下一个进程

三、多线程

线程,有时被称为轻量进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。由于线程之间的相互制约,致使线程在运行中呈现出间断性。线程也有就绪、阻塞和运行三种基本状态。就绪状态是指线程具备运行的所有条件,逻辑上可以运行,在等待处理机;运行状态是指线程占有处理机正在运行;阻塞状态是指线程在等待一个事件(如某个信号量),逻辑上不可执行。每一个程序都至少有一个线程,若程序只有一个线程,那就是程序本身。

线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程

多线程:多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。具有这种能力的系统包括对称多处理机、多核心处理器以及芯片级多处理(Chip-level multithreading)或同时多线程(Simultaneous multithreading)处理器。 在一个程序中,这些独立运行的程序片段叫作“线程”(Thread),利用它编程的概念就叫作“多线程处理(Multithreading)”。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程(台湾译作“执行绪”),进而提升整体处理性能

  • 优点:
    • 使用线程可以把占据长时间的程序中的任务放到后台去处理。
    • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
    • 程序的运行速度可能加快
    • 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。

Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。

threading 模块提供的其他方法:

  • threading.currentThread(): 返回当前的线程变量。
  • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
  • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:

  • run(): 用以表示线程活动的方法。
  • start():启动线程活动。
  • join([time]): 等待至线程中止。
  • isAlive(): 返回线程是否活动的。
  • getName(): 返回线程名。
  • setName(): 设置线程名。

1.线程创建

Thread(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None))

参数:

​ target:线程执行体

​ args:任意位置参数

​ kwargs:任意关键字参数

import threading
from random import randint
from time import sleep

def run(name):
    print("{}线程开始".format(name))
    sleep(randint(1,3))
    print("{}线程结束".format(name))

if __name__ == "__main__":
    print("主线程:{}".format(threading.current_thread().name))
    t1 = threading.Thread(target=run,args=("线程1",))
    t1.start()
    t1.join()
    print("主线程结束")
  • 线程之间数据共享
    多线程和多进程最大的不同在于,多进程中,同一个全局变量,每个子进程各自有一份拷贝,互不影响。而在多线程中,所有变量都由线程共享,所以任何一个变量都可以被任意线程所修改。因此,多个线程同时改变一个变量,容易把内容改乱了
import threading
import random
import time
      
money = 1000
      
def run1():
    global money
    for i in range(100):
        money -= 1
      
def run2():
    global money
    for i in range(100):
        money -= 1
      
if __name__ == "__main__":
    #创建线程
    th1 = threading.Thread(target=run1, name="th1")
    th2 = threading.Thread(target=run2, name="th2")
      
    #启动
    th1.start()
    th2.start()
      
    #等待子线程结束
    th1.join()
    th2.join()
      
    print("money = %d"%money)

2.线程同步

如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。

使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。

多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。为了避免这种情况,引入了锁的概念。

    import threading
    import random
    import time
    
    lock = threading.Lock()
    
    list1 = [0]*10
    
    def run1():
        global list1
    
        #获取线程锁,如果已上锁,则阻塞等待锁的释放
        lock.acquire()
        for i in range(len(list1)):
            list1[i] = 1
            time.sleep(0.2)
        lock.release()
    
    def run2():
        global list1
        with lock:
            for i in range(len(list1)):
                print(list1[i],end=' ')
                time.sleep(0.2)
    
    if __name__ == "__main__":
        #创建线程
        th1 = threading.Thread(target=run1,  name="th1")
        th2 = threading.Thread(target=run2,  name="th2")
    
        #启动
        th1.start()
        th2.start()
    
        #等待子线程结束
        th1.join()
        th2.join()
    
        print("\nlist1 = {}".format(list1))
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!