Python入门基础(10)——并发编程

一、概念介绍

1、线程与进程的基础概念

这里就不详细介绍了,直接被百度吧,一大堆

2、全局解释器锁(GIL)

(1)GIL全称全局解释器锁Global Interpreter Lock,GIL并不是Python的特性,它是在实现Python解析器(CPython)时

所引入的一个概念。

(2)GIL是一把全局排他锁,同一时刻只有一个线程在运行。

  •  毫无疑问全局锁的存在会对多线程的效率有不小影响。甚至就几乎等于Python是个单线程的程序。
  • multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。

二、多线程

1、单线程的开始与结束

import threading  #导入线程模块,可以创建多个线程,并且可以在多线程间进行通信与同步
import time  #导入时间模块,一般并发编程都要用到


def thread_run(name):
    print("%s's first thread!" % name)  #%s用于输出变量,和C差不多
    time.sleep(5)  #停顿5秒

if __name__ == '__main__':
    LiMing = threading.Thread(target=thread_run("李明"))
    Zhangsan = threading.Thread(target=thread_run,args=('张三',))
    LiMing.start()
    Zhangsan.start()

上面直接导入了一个线程的实例,首先要解决如下几个问题:

(1)导入线程模块有两种方法

#第一种方法
import threading  #导入线程模块,可以创建多个线程,并且可以在多线程间进行通信与同步

LiMing = threading.Thread(target=thread_run("李明"))  #创建线程
LiMing.start()

#第二种方法
from threading import Thread  #导入线程模块

LiMing = Thread(target=thread_run("李明"))  #创建线程
LiMing.start()

(2)传参问题

#共有两种方法
from threading import Thread  #导入线程模块

LiMing = Thread(target=thread_run("李明"))  #第一种方法,直接在函数里面写参数
ZhangSan = Thread(target=thread_run(),  args=("张三",))   #第二种方法,多写一个参数
LiMing.start()

我看了很多线程类的实例,大都用第二种方法,第一种方法是在写代码的时候,程序默认写出来的

(3)start()函数

开启线程,开启之后,线程就会独立运行它的目标函数(也就是创建线程时的target函数)

(4)join()函数

开启之后,就会阻塞主线程的向下执行,直到调用join方法的线程执行结束以后,方才会继续运行

#不加入join函数
import time
import threading

def thread_run(name):
    time.sleep(2)
    print("%s's first thread!!!"% name)


mike = threading.Thread(target=thread_run, args=('Mike', ))
jone = threading.Thread(target=thread_run, args=('jone', ))

mike.start()
jone.start()
print('main thread is running!!!')

执行结果:
main thread is running!!!
jone's first thread!!!
Mike's first thread!!!

上面执行结果,是因为开启mike和jone线程之后,当他们执行的时候,会先停顿2秒时间,这两秒中,主程序会继续执行,所以先输出:main thread is running!!!,然后另外两个线程停顿完毕,就相继执行(注意这里是并发的)

import time
import threading

def thread_run(name):
    time.sleep(2)
    print("%s's first thread!!!"% name)


mike = threading.Thread(target=thread_run, args=('Mike', ))
jone = threading.Thread(target=thread_run, args=('jone', ))

mike.start()
jone.start()
mike.join()    #阻塞子线程mike直到mike线程执行完毕
jone.join()    #阻塞子线程jone直到jone线程执行完毕
print('main thread is running!!!')

输出结果:
Mike's first thread!!!
jone's first thread!!!
main thread is running!!!

上面启动mike.join后,就会阻塞其他线程(我是这么理解的),直到mike线程的目标程序执行结束,方才会执行下面的代码

#注意join函数只会阻塞主线程,不会阻塞其他线程
import time
import threading

def thread_run1(name):
    time.sleep(5)
    print("%s's first thread!!!"% name)

def thread_run2(name):
    time.sleep(2)
    print("%s's first thread!!!"% name)

mike = threading.Thread(target=thread_run1, args=('Mike', ))
jone = threading.Thread(target=thread_run2, args=('jone', ))

mike.start()
jone.start()
mike.join()    #阻塞子线程mike直到mike线程执行完毕
jone.join()    #阻塞子线程jone直到jone线程执行完毕
print('main thread is running!!!')

输出结果:
jone's first thread!!!   #可以看出jone是输出的
Mike's first thread!!!
main thread is running!!!

我先说一下,上面这个代码的执行顺序,首先是建立了主线程,然后主线程建立了mike线程,mike线程开启就开始执行thread_run1,先停顿5秒,与此同时(应该还是并发的)jone线程建立,开始执行thread_run2,然后要停顿2秒,之后mike调用了join函数,这时候主函数就停在了mike.join()这行代码中,不再往下运行(也就是不再执行jone.join()代码,知道上面mike线程执行结束),也就是说阻塞了主线程,只有当mike线程运行结束,主线程才会继续;

但是这个时候,jone线程并没有被阻塞,经过2秒后,它就开始输出:jone's first thread!!!,而又过了3秒(总共5秒),mike线程休眠结束,开始输出:mike's first thread!!!,这时候主线程开始启动,输出:main thread is running!!!

(5)主线程与子线程的优先级

#主线程与新建的线程优先级
import time
import threading

def thread_run1(name):     #mike线程的执行函数
    for i in range(100):
        print(i)
    print("%s's first thread!!!"% name)

def thread_run2(name):     #jone线程的执行函数
    print("%s's first thread!!!"% name)

mike = threading.Thread(target=thread_run1, args=('Mike', ))
jone = threading.Thread(target=thread_run2, args=('jone', ))

mike.start()
print('main thread is running!!!')
jone.start()
print('main thread is running!!!')

上面这几行代码的执行,是想看一看主线程与其他线程的优先级,下面粘贴一下(执行了很多遍,每一遍都不一样)



大致优先级是,同时并发执行,没有明确的优先级(当然可能是有优先级的,只是我现在还没有接触到),当然第二幅图中两句话的输出紧挨着,这是一个偶然现象,之后又试了几次,两者并没有挨着

但是总的来说,一旦线程建立,就会立刻执行自己的目标函数,执行顺序与主线程可以说是同步的

2、线程Thread的常用函数

(1)is_alive()函数

用来判断线程是否还在运行,当线程创建成功,但是还没start()开启时,会返回False;当线程已经执行后并结束,也会返回False

import time
import threading

def thread_run(name):
    time.sleep(2)
    print("%s's first thread!!!"% name)

mike = threading.Thread(target=thread_run, args=('Mike', ))
print("mike's status: %s" % mike.isAlive())
mike.start()
print("mike's status: %s" % mike.isAlive())
mike.join()
print("mike's status: %s" % mike.isAlive())
print('main thread is running!!!')

输出结果:
mike's status: False
mike's status: True
Mike's first thread!!!
mike's status: False
main thread is running!!!

(2)name函数

name属性表示线程的线程名,默认是Thread -x ,x是序号,由1开始,第一个创建的线程名字就是:Thread-1

import time
import threading

def thread_run(name):
    print("%s's first thread!!!"% name)
    time.sleep(5)

mike = threading.Thread(target=thread_run, args=('Mike', ), name='Th-mike')    #name设置线程名
jone = threading.Thread(target=thread_run, args=('jone', ))    #默认线程name是Thread-X

mike.start()
jone.start()
print(mike.name)    #打印线程名
print(jone.name)    #打印线程名

输出结果:
Mike's first thread!!!
jone's first thread!!!
Th-mike
Thread-1

注意:上面的线程名字是可以随意起的,但是你一旦不写,就会执行默认线程名,另外区分好线程名和线程的引用名

(3)setName()和getName()

setName()顾名思义,就是设置线程的名字;而getName()则是得到线程的名字;因为这两个都比较简单,就一起写了

import time
import threading

def thread_run(name):
    print("%s's first thread!!!"% name)
    time.sleep(5)

mike = threading.Thread(target=thread_run, args=('Mike', ))
jone = threading.Thread(target=thread_run, args=('jone', ))    #默认线程name是Thread-X

mike.setName('Thread-mike')    #name设置线程名

noe = threading.Thread(target=thread_run, args=('noe', ))    #默认线程name是Thread-X
mike.start()
jone.start()
print(mike.getName())    #打印线程名
print(jone.name)    #打印线程名
print(noe.name)    #打印线程名

输出结果:
Mike's first thread!!!
jone's first thread!!!
Thread-mike
Thread-2   #因为mike一开始用的是thread-1,所以jone就只能紧接着变成2了
Thread-3   #这里其实我有点不太懂了,mike在noe创建之前就已经抛弃了thread-1,但是为什么noe依然是3呢

好吧,getName()和.name其实是一回事,最后一点疑问有点没有必要,人家就是这样的规矩,记住就可以了,毕竟不是什么内涵东西

(4)daemon()

这个函数还是很有作用的

  • 当daemon = False时,线程不会随主线程退出而退出(默认时,就是daemon = False)
  • 当daemon = True时,当主线程结束,其他子线程就会被强制结束(不管你有没有执行完,就好像玩游戏一样,一点你关闭了总游戏开关,这个游戏就彻底 吉吉了)

import time
import threading

def thread_mike_run(name):
    time.sleep(1)
    print('mike thread is running 1S')
    time.sleep(5)
    print("%s's first thread!!!"% name)

def thread_jone_run(name):
    time.sleep(2)
    print("%s's first thread!!!"% name)


mike = threading.Thread(target=thread_mike_run, args=('Mike', ), daemon=True)    #设置daemon为True
jone = threading.Thread(target=thread_jone_run, args=('jone', ))


mike.start()
jone.start()
print('main thread')   

输出结果:
main thread
mike thread is running 1S
jone's first thread!!!

从上面可以看出,本来mike线程应该输出两行语句,但是只输出了一句,下面我来解释一下整个流程:主线程创建,然后在主线程基础上,又创建了mike和jone两个线程,然后主线程输出了一行代码,mike线程等待了1秒输出了一行代码,jone线程等待了2秒输出了一行语句,至此主线程执行结束(主要是等待jone线程执行完,因为jone的daemon设置为false,表示它是由主线程代理的,相当于他就是主线程的一部分)。

而mike线程因为设置daemon为true,所以它不归主线程管了,管你有没有执行结束(反正你又不归我管了),我主线程要结束了,主线程一结束,其他任何线程都会强制关机!

(5)setDaemon()

用于设置daemon的值,这里就不再多说了

mike.setDaemon(True)    #设置mike线程的daemon为True

3、线程threading常用方法函数

上面说了一堆,其实都是thread的方法函数,thread只是threading模块的一部分,那么threading模块有哪些函数呢?

可以参考:Python3入门线程threading常用方法

这个大神写的博客不错,很详细,推荐

4、多线程的顺序执行与并发执行

(1)

from threading import Thread  #导入线程模块
import time   #导入时间模块

def my_counter():   #定义一个数数函数
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True

def main():    #主函数
    thread_array = {}
    start_time = time.time()    #计算当前时间,单位秒
    for tid in range(2):
        t = Thread(target=my_counter)   #线程调用数数函数
        t.start()       #线程开始执行
        t.join()        #线程执行完,阻塞一下,不让for循环结束,直到当前t这个线程结束
    end_time = time.time()   #计算结束时间
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':
    main()

上面是顺序执行,所需要的时间为12秒(大概,运行了几次大都不一样),这里不懂代码if _name_ =='_main_'的可以参考博客:如何简单地理解Python中的if __name__ == '__main__'

总结来说,就是你导入的模块中也有main函数,如果你运行main,那么导入的模块中main函数也会被运行,这就不是我们想要的结果了,这行判断的意思是:当.py文件被直接运行时,if __name__ == '__main__'之下的代码块将被运行;当.py文件以模块形式被导入时,if __name__ == '__main__'之下的代码块不被运行。

好了接下来看,两个线程的并发执行:

from threading import Thread  #导入线程模块
import time   #导入时间模块,用来计算时间

def my_counter():  #还是数数函数
    i = 0
    for _ in range(100000000):
        i = i + 1
    return True

def main():
    thread_array = {}  #定义一个词典,key与value类型是不限的
    start_time = time.time()   #获得开始时刻的时间
    for tid in range(2):      #这个for循环,是获得两个线程,并让其同时开始(是并发,不是并行)
        t = Thread(target=my_counter)
        t.start()
        thread_array[tid] = t  #将这两个线程导入到字典中,0—第一个线程,1—第二个线程
    for i in range(2):      #这个for循环,是将两个线程关闭
        thread_array[i].join()
    end_time = time.time()   #获得结束时间
    print("Total time: {}".format(end_time - start_time))

if __name__ == '__main__':  #让导入模块中的main函数不执行
    main()

这里计算结果是13(不过有波动,有时候是12秒多)

三、线程池

1、为什么要用线程池

参考网页:线程池原理及Python实现

说白了,就是要灵活控制线程的数量,一般传统创建线程,是来一个任务创建一个线程,但是如果当前时间来了很多任务,而这些任务完成时间很多,但是数量非常大,那么我们其实只需要几个线程就可以完成,比创建大批量的线程更好(因为线程的创建和使用需要大量的时间,占用大量的内存资源),线程池可以固定线程的数量

2、线程池的实现

参考网页:多种方法实现Python线程池

 

四、多进程

1、multiprocessing是跨平台版本的多进程模块,它提供了一个Process类来代表一个进程对象,下面展示一个示例代码:

from multiprocessing import Process   #导入多进程模块
import time       #导入时间模块
 
def f(n):     #定义一个平方函数
    time.sleep(1)   #这里每次之前开始,先停顿1秒
    print (n*n)

if __name__=='__main__':   #禁止导入的模块下面代码执行
    for i in range(10):    #0-9,相当于循环10次
        p = Process(target=f,args=[i,])   #每次循环都创建一个进程,这个进程的目标是执行上面定义的f函数,传递的参数是i(也就是n=i)
        p.start()     #进程开始执行,注意进程执行是并发的
可以看到,如果是单个进程顺序执行,执行时间至少是10秒以上(因为每次执行都要停顿1秒),但是10个进程并行执行,只需要1秒多

另外,我发现执行结果每次都不一样(并不是按照顺序输出的,而是无序),猜测这种并行执行(我也没有搞清楚是并行,还是并发)

如果在p.start()下面加一行:p.join(),那么它就是顺序执行的了,输出也是每个1秒输出1个数值(顺序输出的),这是因为他每次都要等上一个进程执行结束,才会开始下一个进程

2、进程间通信

Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递

from multiprocessing import Process, Queue  #导入多进程和队列
import time  #导入时间模块


def write(q):  #写函数
    for i in ['A', 'B', 'C', 'D', 'E']:
        print('Put %s to queue' % i)  #先打印列表中的元素
        q.put(i)    #将元素依次加入队列中
        time.sleep(0.5)   #停顿0.5秒


def read(q):  #读函数
    while True:
        v = q.get(True)
        print('get %s from queue' % v)
        if (v == 'E'): break;


if __name__ == '__main__':
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    pw.start()  #写进程开始
    pr.start()  #读进程开始
    pr.join()   #这里是阻塞其他进程,要等到读进程结束以后其他进程才会开始
    pr.terminate()

执行结果:


五、多进程与多线程对比

一般情况下,多个进程的内存资源是相互独立的,而多线程可以共享同一个进程中的内存资源

from multiprocessing import Process
import threading
import time

lock = threading.Lock()  #锁

def run(info_list, n):
    lock.acquire()
    info_list.append(n)
    lock.release()
    print('%s\n' % info_list)


if __name__ == '__main__':
    info = []
    for i in range(10):
        # target为子进程执行的函数,args为需要给函数传递的参数
        p = Process(target=run, args=[info, i])
        p.start()
        p.join()
    time.sleep(1)  # 这里是为了输出整齐让主进程的执行等一下子进程
    print('------------threading--------------')
    for i in range(10):        #这里是线程,上面是进程
        p = threading.Thread(target=run, args=[info, i])
        p.start()
        p.join()
        
输出结果:
[0]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
------------threading--------------
[0]
[0, 1]
[0, 1, 2]
[0, 1, 2, 3]
[0, 1, 2, 3, 4]
[0, 1, 2, 3, 4, 5]
[0, 1, 2, 3, 4, 5, 6]
[0, 1, 2, 3, 4, 5, 6, 7]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

进程资源不共享,所以每个进程不会在之前进程资源基础上进行扩充,每个list仅有一个数字,而线程则不同,他们共享同一个list,所以每一个线程都会在原有基础上加一个数字

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值