欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

python之多线程与多进程

程序员文章站 2022-07-12 21:43:39
...

操作系统

任务调度

时间片
大部分操作系统的任务调度采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片

运行状态:任务正在执行时的状态叫做运行状态
就绪状态:任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于他的时间片的到来。
并发:每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速切换,给人感觉就是多个任务在“同时执行”,即并发。

并发的本质:是任务切片串行的结果,而非真正的并行。
注:理解并发与并行,串行与并行

进程

程序与进程
计算机的核心是CPU,他承担了所有的计算任务,而操作系统是计算机的管理者,它负责任务的调度,资源的分配和管理,程序是运行在操作系统之上的。

进程一般由程序、数据集合和进程控制块三部分组成。程序控制快(Program Control Block,PCB)包含进程的描述信息和控制信息,是进程存在的唯一标志。

进程具有的特征:

  • 动态性:进程是程序的一次执行过程,是临时的,有生命周期的,是动态产生的,动态消亡的;
  • 并发性:任何进程都可以和其他进程一起并发执行;
  • 独立性:进程是系统进行资源分配和调度的一个独立单位;
  • 结构性:进程由程序、数据和进程控制块三部分组成

线程

出现原因:随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,无法满足越来越复杂的程序要求。
线程:是程序中一个单一的顺序控制流程,是程序执行流的最小单位。

进程线程简单理解

进程是操作系统进行资源分配的最小单元,资源包括CPU、内存、磁盘等IO设备等等,而线程是CPU调度的基本单位。举个简单的例子来帮助理解:我们电脑上同时运行的浏览器和视频播放器是两个不同的进程,进程可能包含多个子任务,这些子任务就是线程,比如视频播放器在播放视频时要同时显示图像、播放声音、显示字幕,这就是三个线程。

进程与线程的关系

一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)
进程是资源分配最小单位,线程是程序执行的最小单位
进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。
线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

进程有独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径,线程有自己的堆栈和局部变量,但线程没有单独的地址空间,它使用相同的地址空间共享数据,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

总的来说就是:

  • 进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。
  • 线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
  • 多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
  • CPU切换一个线程比切换进程花费小;创建一个线程比进程开销小;线程占用的资源要比进程少很多。
  • 进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
  • 操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。
  • 改善程序结构。一个既长又复杂的进程可以考虑分为多个线程,成为几个独立或半独立的运行部分,这样的程序会利于理解和修改。
  • 从函数调用上来说,进程创建使用fork()操作;线程创建使用clone()操作

I/O密集与CPU密集
I/O密集型应用
CPU密集型应用

多进程

**多进程multiprocessing模块提供远程与本地的并发,**在一个multiprocessing库的典型使用场景下,所有的子进程都是由一个父进程启动起来的,这个父进程成为madter进程,这个父进程非常重要,他会管理一系列的对象状态,一旦这个进程退出,子进程很可能处于一个不稳定的状态,这个进程最好尽可能做最少的事情,以便保持其稳定性。

import multiprocessing
import time
 
def worker_1(interval):
    print("worker_1")
    time.sleep(interval)
    print("end worker_1")
    
def worker_2(interval):
    print("worker_2")
    time.sleep(interval)
    print("end worker_2")
    
def worker_3(interval):
    print("worker_3")
    time.sleep(interval)
    print("end worker_3")

if __name__ == "__main__":
    p1 = multiprocessing.Process(target = worker_1, args = (2,))
    p2 = multiprocessing.Process(target = worker_2, args = (9,))
    p3 = multiprocessing.Process(target = worker_3, args = (4,))
 	# 启动子进程
    p1.start()
    p2.start()
    p3.start()
 
    print("The number of CPU is:" + str(multiprocessing.cpu_count()))
    for p in multiprocessing.active_children():
        print("child   p.name:" + p.name + "\tp.id" + str(p.pid))
    print("END!!!!!!!!!!!!!!!!!")

结果:

worker_1
The number of CPU is:8
child   p.name:Process-15	p.id6244
child   p.name:Process-14	p.id6241
child   p.name:Process-13	p.id6240
END!!!!!!!!!!!!!!!!!
worker_2
worker_3
end worker_1
end worker_3
end worker_2

deamon:主进程结束,子进程随之结束

import multiprocessing
import time
 
def worker(interval):
    print("work start:{0}".format(time.ctime()))
    time.sleep(interval)
    print("work end:{0}".format(time.ctime()))

if __name__ == "__main__":
    p = multiprocessing.Process(target = worker, args = (3,))
    p.daemon = True # 添加了daemon属性
    p.start()
    print("end!")
end!
work start:Tue Apr 16 11:01:19 2019
work end:Tue Apr 16 11:01:22 2019

join:阻塞当前进程,直到调用join方法的那个进程执行完,在继续执行当前进程

import multiprocessing
import time
 
def worker_1(interval):
    print("worker_1")
    time.sleep(interval)
    print("end worker_1")

if __name__ == "__main__":
    p1 = multiprocessing.Process(target = worker_1, args = (2,))
    p1.start()
    p1.join() 
    print("main end!")

结果:

worker_1
end worker_1
main end!

如果不添加p1.join()
结果为:

main end!
worker_1
end worker_1

通信:进程之前需要传递参数,数据不能直接获取

import multiprocessing as mp

def washer(dishes, output):
    for dish in dishes:
        print('Washing', dish, 'dish') 
        output.put(dish)

def dryer(input):
    while True:
        dish = input.get() 
        print('Drying', dish, 'dish')
        input.task_done()

dish_queue = mp.JoinableQueue()
dryer_proc = mp.Process(target=dryer, args=(dish_queue,))
dryer_proc.daemon = True
dryer_proc.start()

dishes = ['salad', 'bread', 'entree', 'dessert'] 
washer(dishes, dish_queue)
dish_queue.join()

Drying salad dish
Drying bread dish
Drying entree dish
Drying dessert dish
Washing salad dish
Washing bread dish
Washing entree dish
Washing dessert dish

多线程

线程运行在进程内部,可以访问进程的所有内容。
Python的标准库提供了两个模块:_thread和threading,_thread是低级模块,它不支持守护线程,当主线程退出时,所有子线程都会被强行退出。threading是高级模块,对_thread进行了封装,支持守护线程,绝大多数情况下,我们只需要使用threading这个高级模块。

import threading
import time
 
def target():
    print('%s is running' % threading.current_thread().name)
    time.sleep(1)
    print('%s is ended' % threading.current_thread().name)

print('%s is running' % threading.current_thread().name)
t = threading.Thread(target=target)
t.start()
t.join()
print('%s is ended' % threading.current_thread().name)
MainThread is running
Thread-4 is running
Thread-4 is ended
MainThread is ended
import threading
import queue
import time


def washer(dishes, dish_queue):
    for dish in dishes:
        print("Washing", dish)
        time.sleep(2)
        dish_queue.put(dish)


def dryer(dish_queue):
    while True:
        dish = dish_queue.get()
        print ("Drying", dish)
        time.sleep(10)
        dish_queue.task_done()


dish_queue = queue.Queue()

for n in range(2):
    dryer_thread = threading.Thread(target=dryer, args=(dish_queue,))
    dryer_thread.start()
dishes = ['salad', 'bread', 'entree', 'desert'] 
washer(dishes, dish_queue)
dish_queue.join()
Washing salad
Washing bread
Drying salad
Washing entree
Drying bread
Washing desert
Drying entree
Drying desert

与进程相比较线程是轻量级的

线程安全问题与锁的机制
只对全局变量读,不会出现问题,但是对全局变量进行写操作,则会出现问题。

import threading, time


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        global n, lock
        time.sleep(1)
        print(n , self.name)
        n += 1
            
if "__main__" == __name__:
    n = 1
    ThreadList = []
    for i in range(1, 200):
        t = MyThread()
        ThreadList.append(t)
    for t in ThreadList:
        t.start()
    for t in ThreadList:
        t.join()
128 Thread-153
129 Thread-165
129 Thread-173
129 Thread-196
129 Thread-157
129 Thread-188
129 Thread-148

问题:n最后只有129,多个线程内部输出了同一个n值

import threading, time


class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
    def run(self):
        global n, lock
        time.sleep(1)
        if lock.acquire():
            print(n , self.name)
            n += 1
            lock.release()
            
if "__main__" == __name__:
    n = 1
    ThreadList = []
    lock = threading.Lock()
    for i in range(1, 200):
        t = MyThread()
        ThreadList.append(t)
    for t in ThreadList:
        t.start()
    for t in ThreadList:
        t.join()


1 Thread-1
2 Thread-42
3 Thread-6
4 Thread-7
5 Thread-3
6 Thread-8
7 Thread-9
8 Thread-10

总结

  • 线程适合解决I/O问题

    如果代码是IO密集型,多线程可以明显提高效率。例如制作爬虫,绝大多数时间爬虫是在等待socket返回数据。某个线程等待IO的时候其他线程可以继续执行。

    如果代码需要读入多个文件,在等待其他文件读入的同时,另一个线程可以处理已经读好的文件。

  • 使用进程等来处理CPU问题。

Python与多线程

操作系统通过给不同的线程分配时间片(CPU运行时长)来调度线程,当CPU执行完一个线程的时间片后就会快速切换到下一个线程,时间片很短而且切换切速度很快以至于用户根本察觉不到。早期的计算机是单核单线程的,多个线程根据分配的时间片轮流被CPU执行,如今绝大多数计算机的CPU都是多核的,多个线程在操作系统的调度下能够被多个CPU并发执行,程序的执行速度和CPU的利用效率大大提升。绝大多数主流的编程语言都能很好地支持多线程,然而python由于GIL锁无法实现真正的多线程。

Python实际上不允许多线程。上面说过python有一个threading包但是如果你想加快代码运行速度,或者想并行运行,这不是一个好主意。Python有一个机制叫全局解释器锁(GIL)。GIL保证每次只有一个线程在解释器中跑。一个线程获得GIL,之后再交给下一个线程。所以,看起来是多个线程在同时跑,但是实际上每个时刻只有CPU核在跑一个线程,没有真正利用多个CPU核跑多个线程。就是说,多个线程在抢着跑一个CPU核。
但是还是有使用threading包的情况的。比如你真的想跑一个可以线程间抢占的程序,看起来是并行的。或者有很长时间的IO等待线程,这个包就会有用。但是threading包不会使用多核去跑代码。真正的多核多线程可以通过多进程,一些外部库如Spark和Hadoop,或者用Python代码去调用C方法等等来实现。
全局解释器锁(GIL)
无论你启多少个线程,你有多少个cpu, Python在执行的时候在同一时刻只允许一个线程运行。因为python的线程是调用操作系统的原生线程,原生线程是通过C语言提供原生接口,相当于C的一个函数。启动的时候就是调用的C语言的接口,超出了python的控制范围,python的控制范围是只在python解释器这一层,所以python控制不了C接口,它只能等结果。所以它不能控制让哪个线程先执行,因为是一块调用的,只要一执行,就是等结果,这个时候4个线程独自执行,所以结果就不一定正确了。有了GIL,就可以在同一时间只有一个线程能够工作。虽然这4个线程都启动了,但是同一时间我只能让一个线程拿到这个数据。其他的几个都干等。GIL(全局解释器锁)是加在python解释器里面。
简单理解:
用篮球比赛的例子来帮助理解:把篮球场看作是CPU,一场篮球比赛看作是一个线程,如果只有一个篮球场,多场比赛要排队进行,就是一个简单的单核多线程的程序;如果有多块篮球场,多场比赛同时进行,就是一个简单的多核多线程的程序。然而python有着特别的规定:每场比赛必须要在裁判的监督之下才允许进行,而裁判只有一个。这样不管你有几块篮球场,同一时间只允许有一个场地进行比赛,其它场地都将被闲置,其它比赛都只能等待。

不能实现真正的多线程,但是python有很多方法能解决这一问题,比如使用多进程、C语言扩展、ctypes

每个进程都包含至少一个线程:主线程,每个主线程可以开启多个子线程,由于GIL锁机制的存在,每个进程里的若干个线程同一时间只能有一个被执行;但是使用多进程就可以保证多个线程被多个CPU同时执行。

参考文献:
Python模块-多线程与多进程
python多线程与多进程理解