python多线程和多进程详解_python—多线程与多进程

线程包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每天线程执行不同的任务。多线程共享主进程的资源,可能还会改变其中的变量,此时要加上线程锁,每次执行完一个线程再执行下一个线程。

如何实现多线程?python解释器中一个线程做完了任务然后做IO(文件读写)操作时,线程退出,下一个线程运行。

以上可以看出,多线程适用于IO密集型的任务量(文件存储、网络通信)。

GIL全局解释器锁

在同一时刻,只能有一个线程在一个cpu上执行字节码,没法像C和JAVA一样将多个线程映射到多个CPU上执行,但是GIL会根据执行的字节码行数和时间片以及遇到的IO操作的时候主动释放锁,让其他字节码进行。

作用:限制多线程同时执行,保证同一时刻只有一个线程执行。

原因:多线程是可以共享变量的,同时执行可能会导致数据被污染造成数据混乱,这是线程的不安全性,引入互斥锁。

互斥锁:确保某段关键的代码数据只能有一个线程从头到尾完整执行,保证了这段代码数据的安全性,但是这也会导致死锁。

死锁:多个子线程在等待对方解除占用状态,但是都不先解锁,互相等待,导致死锁。

基于GIL的存在,在遇到大量IO操作代码时,使用多线程效率更高。

t1 = threading.Thread(target=你写的函数名,args=(传入变量(如果只有一个变量就必须在后加上逗号),),name=随便取一个线程名):#把一个线程实例化给t1,这个线程负责执行target=你写的函数名

t1.start():#负责执行启动这个线程

t1.join():#必须要等待你的子线程执行完成后再执行主线程

t1.setDeamon(True):#当你的主线程执行完毕后,不管子线程有没有执行完成都退出主程序,注意不能和t1.join()一起使用。

threading.current_thread().name:#打印出线程名

单线程版本的话,函数在执行时只能串联完成,即必须等前一个函数执行完才能执行下一个函数。多线程版本的话当前一个函数线程被阻塞(如sleep函数)的时候,此时等待调度的命令执行另一个子线程,线程将被系统自动随机选择。

start() #方法是启动一个子线程,线程名就是我们定义的name

run() #方法并不启动一个新线程,就是在主线程中调用了一个普通函数而已。

'''如果想启动多线程就必须使用start()'''

t1.start()

t1.join()

t2.start()

t2.join()

'''如果这样放的话,就是先执行线程1然后等待线程1执行完毕,然后执行线程2等待线程2执行完毕。类似于单线程的顺序执行'''

t1.start()

t2.start()

t1.join()

t2.join()

'''这样的话就是线程1和线程2执行完毕后在执行主线程,那么这里就可以执行多线程'''

线程间的通信

在一个进程中,不同子线程负责不同的任务,t1子线程负责获取到数据,t2子线程负责把数据保存的本地,那么他们之间的通信使用Queue来完成。因为再一个进程中,数据变量是共享的,即多个子线程可以对同一个全局变量进行操作修改,Queue是加了锁的安全消息队列。

import threading

import time

import queue

q = queue.Queue(maxsize=5) #q在t1和t2两个子线程之间通信共享,一个存入数据,一个使用数据。

def t1(q):

while 1:

for i in range(10):

q.put(i)

def t2(q):

while not q.empty():

print('队列中的数据量:'+str(q.qsize()))

# q.qsize()是获取队列中剩余的数量

print('取出值:'+str(q.get()))

# q.get()是一个堵塞的,会等待直到获取到数据

print('-----')

time.sleep(0.1)

t1 = threading.Thread(target=t1,args=(q,))

t2 = threading.Thread(target=t2,args=(q,))

t1.start()

t2.start()

线程同步

如果没有控制多个线程对同一资源的访问,对数据造成破坏,使得线程运行的结果不可预期,这种现象称为“线程不安全”

同步的意思是:进程或线程A和B一块配合,A执行到1一定程度的时候要依靠B的某个结果,于是停下来,示意B运行;B运行后,再将结果传给A;A再继续操作。

线程锁实现同步控制

线程锁使用threading.Lock()实例化,使用acquire()上锁,使用release()释放锁,acquire()和release()必须同时成对存在

def run1():

while 1:

if l1.acquire():

# 如果第一把锁上锁了

print('我是老大,我先运行')

l2.release()

# 释放第二把锁

def run2():

while 1:

if l2.acquire():

# 如果第二把锁上锁了

print('我是老二,我第二运行')

l3.release()

# 释放第三把锁

def run3():

while 1:

if l3.acquire():

# 如果第三把锁上锁了

print('我是老三,我最后运行')

l1.release()

# 释放第一把锁

t1 = threading.Thread(target=run1)

t2 = threading.Thread(target=run2)

t3 = threading.Thread(target=run3)

l1 = threading.Lock()

l2 = threading.Lock()

l3 = threading.Lock()

# 实例化三把锁

l2.acquire()

l3.acquire()

t1.start()

t2.start()

t3.start()

output:

我是老大,我先运行

我是老二,我第二运行

我是老三,我最后运行

我是老大,我先运行

我是老二,我第二运行

我是老三,我最后运行

我是老大,我先运行

我是老二,我第二运行

我是老三,我最后运行

.....

条件变量实现同步精准控制

信号量实现定量的线程同步

semaphore适用于控制进入数量的锁,好比文件的读写操作,写入的时候一般只用一个线程写,如果多个线程同时执行写入操作的时候,会造成写入数据混乱,读取的时候可以用多个线程读取,即写与写是互斥的,读与写不是互斥的,读与读不是互斥的。

BoundedSemaphore,这种锁允许一定数量的线程同时更改数据,不是互斥锁。

import time

import threading

def run(n, se):

se.acquire()

print("run the thread:%s" % n)

time.sleep(1)

se.release()

# 设置允许5个线程同时运行

semaphore = threading.BoundedSemaphore(5)

for i in range(20):

t = threading.Thread(target=run, args=(i,semaphore))

t.start()

运行后,即5个一批的线程被放行,用来控制进入某段代码的线程数量。

事件实现线程锁同步

事件线程锁的运行机制:

全局定义一个Flag,如果Flag的值为False,当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不在阻塞。类似于交通红绿灯,属于红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。

调用wait()方法将等待信号。 is_set():判断当前是否状态 调用set()方法会将Flag设置为True。 调用clear()方法会将事件的Flag设置为False。

import threading

import time

import random

boys = ['此时一位捡瓶子的靓仔路过\n------------','此时一位没钱的网友路过\n------------','此时一位推着屎球的屎壳郎路过\n------------']

event = threading.Event()

def lighter():

event.set()

while 1:

ti = (random.randint(1, 10))

time.sleep(ti)

print('等待 {} 秒后'.format(str(ti)))

event.clear()

time.sleep(ti)

event.set()

def go(boy):

while 1:

if event.is_set():

# 如果事件被设置

print('在辽阔的街头')

print(boy)

time.sleep(random.randint(1, 5))

else:

print('在寂静的田野')

print(boy)

event.wait()

print('突然,一辆火车驶过')

time.sleep(5)

t1 = threading.Thread(target=lighter)

t1.start()

for boy in boys:

t2 = threading.Thread(target=go,args=(boy,))

t2.start()

线程池编程

在线程池中,主线程可以获取任意一个线程的状态以及返回结果,并且当一个线程完成后,主线程能立即得到结果。

模板套用

# coding:utf-8

from concurrent.futures import ThreadPoolExecutor

# 导入线程池模块

import threading

# 导入线程模块,作用是获取当前线程的名称

import os,time

def task(n):

print('%s:%s is running' %(threading.currentThread().getName(),os.getpid()))

# 打印当前线程名和运行的id号码

time.sleep(2)

return n**2

# 返回传入参数的二次幂

if __name__ == '__main__':

p=ThreadPoolExecutor()

#实例化线程池,设置线程池的数量,不填则默认为cpu的个数*5

l=[]

# 用来保存返回的数据,做计算总计

for i in range(10):

obj=p.submit(task,i)

# 这里的obj其实是futures的对象,使用obj.result()获取他的结果

# 传入的参数是要执行的函数和该函数接受的参数

# submit是非堵塞的

# 这里执行的方式是异步执行

# -----------------------------------

# # p.submit(task,i).result()即同步执行

# -----------------------------------

# 上面的方法使用range循环有个高级的写法,即map内置函数

# p = ThreadPoolExecutor()

# obj=p.map(task,range(10))

# p.shutdown()

# 这里的obj的值就是直接返回的所有计算结果,不属于futures对象

# -----------------------------------

l.append(obj)

# 把返回的结果保存在空的列表中,做总计算

p.shutdown()

# 所有计划运行完毕,关闭结束线程池

print('='*30)

print([obj.result() for obj in l])

#上面方法也可写成下面的方法

# with ThreadPoolExecutor() as p: #类似打开文件,可省去.shutdown()

# future_tasks = [p.submit(task, i) for i in range(10)]

# print('=' * 30)

# print([obj.result() for obj in future_tasks])

线程池优秀的设计理念在于:他返回的结果并不是执行完毕后的结果,而是futures的对象,这个对象会在未来存储线程执行完毕的结果,这一点也是异步编程的核心。python为了提高与统一可维护性,多线程多进程和协程的异步编程都是采取同样的方式。

多进程

因为GIL存在,无法利用到多核CPU的优势,这个时候多进程出来了,它能利用多个CPU并发的优势实现并行。

多进程:消耗CPU操作,CPU密集计算

多线程:大量的IO操作。

进程间切换的开销要大于线程间开销,对操作系统来说开多线程比开多进程消耗的资源少一些,不同的进程是由主进程完全复制后,每个进程独立隔开,不像多线程对全局变量直接修改,因为是完全复制独立的,所以当主进程结束后,子进程仍然执行。

多进程必须注意的是,要加上

if __name__ == '__main__':

pass

进程池套用模板:

from concurrent.futures import ProcessPoolExecutor

import os,time,random

def task(n):

print('%sis running' %os.getpid())

time.sleep(2)

return n**2

if __name__ == '__main__':

p=ProcessPoolExecutor() #不填则默认为cpu的个数

l=[]

start=time.time()

for i in range(10):

obj=p.submit(task,i) #submit()方法返回的是一个future实例,要得到结果需要用obj.result()

l.append(obj)

p.shutdown() #类似用from multiprocessing import Pool实现进程池中的close及join一起的作用

print('='*30)

# print([obj for obj in l])

print([obj.result() for obj in l])

print(time.time()-start)

#上面方法也可写成下面的方法

# start = time.time()

# with ProcessPoolExecutor() as p: #类似打开文件,可省去.shutdown()

# future_tasks = [p.submit(task, i) for i in range(10)]

# print('=' * 30)

# print([obj.result() for obj in future_tasks])

# print(time.time() - start)

进程间的通信可以用queue或者pipe,pipe只适用于两个进程间的通信,而queue没有限制,但是pipe性能高于queue。

多线程和多进程总结

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

多线程

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

GIL锁

GIL是什么呢?仍然用篮球比赛的例子来帮助理解:把篮球场看作是CPU,一场篮球比赛看作是一个线程,如果只有一个篮球场,多场比赛要排队进行,就是一个简单的单核多线程的程序;如果有多块篮球场,多场比赛同时进行,就是一个简单的多核多线程的程序。然而python有着特别的规定:每场比赛必须要在裁判的监督之下才允许进行,而裁判只有一个。这样不管你有几块篮球场,同一时间只允许有一个场地进行比赛,其它场地都将被闲置,其它比赛都只能等待。

多进程

每个进程都包含至少一个线程:主线程,每个主线程可以开启多个子线程,由于GIL锁机制的存在,每个进程里的若干个线程同一时间只能有一个被执行;但是使用多进程就可以保证多个线程被多个CPU同时执行。如果觉得不好理解,请看下面的两段代码。我分别在我的电脑上运行python多线程和多进程的代码,大家对比一下就明白了。我的CPU是i5-6400T,2.20Ghz,4核4线程。

上图左边用python多线程写了一段代码,主线程加上15个子线程总共16个线程(见最左边的线程列表),每个线程都运行了一个死循环,可是CPU的使用率一直稳定在34%,接近三分之二的CPU被闲置。如果类似的代码用C、C++或Java写,CPU使用率会迅速飙升到100%,电脑运行变得非常卡甚至死机。下面将代码改成多进程,看看运行情况。修改后CPU的利用率,一直保持在100%。通过对比我们不难发现python编写多进程能更充分地利用多核CPU的性能,大大提升程序的运行速度。我们用死循环这种极端的例子只是为了对比python多线程与多进程的差别,实际编程中肯定不会这么写。

python多线程和多进程不存在优劣之分,两者都有着各自的应用环境。线程几乎不占资源,系统开销少,切换速度快,而且同一个进程的多个线程之间能很容易地实现数据共享;而创建进程需要为它分配单独的资源,系统开销大,切换速度慢,而且不同进程之间的数据默认是不可共享的。掌握了两者各自的特点,才能在实际编程中根据任务需求采取更加适合的方案。

参考博客:python的多线程与多进程​baijiahao.baidu.comhttps://blog.csdn.net/lzy98/article/details/88819425​blog.csdn.net

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值