一、概念介绍
1、线程与进程的基础概念
这里就不详细介绍了,直接被百度吧,一大堆
2、全局解释器锁(GIL)
(1)GIL全称全局解释器锁Global Interpreter Lock,GIL并不是Python的特性,它是在实现Python解析器(CPython)时
所引入的一个概念。
(2)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()
这个函数还是很有作用的
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,所以每一个线程都会在原有基础上加一个数字