重谈异步编程-多线程

Python中的多线程需要导入threading包。

线程在操作系统中有过介绍,就不再过多阐述了。直接通过代码演示。

如何创建线程?

import threading
def fun():
    pass
t1 = threading.Thread(target=fun)
print(t1)

输出:

<Thread(Thread-1, initial)>

可以看到,线程的创建不需要像协程一样写协程函数。fun就是一个普通的函数。

输出的t1是Thread对象,也就是线程对象。后边的Thread-1是协程的名字。initial是协程的状态,因为协程还没开始运行,所以状态是初态。

创建线程是用的threading.Thread()方法创建的,Thread方法中包含的参数有很多。做一下简单介绍。

class Thread(group=None, target=None, name=None, args=(), kwargs={},daemon=None)

group用的比较少,不说;target传入的是将哪个函数加入到该线程里边,所以这个位置参数是一个函数名称;name是给线程起名字,一般是用字符串;args是刚才传入函数的位置参数的实参,注意要用元组的形式;kwargs是刚才传入函数的关键字参数的实参;daemon是把该线程设定为是否是守护线程,写True或False,是一个布尔值,后边我们会详细讲到。

下边我们将上边的代码丰富一下。

import threading
def fun(age,**kwargs):
    print('我叫',kwargs['name'],',今年',age,'岁了',sep='')
t1 = threading.Thread(target=fun,args=(18,),name = 't1',kwargs={'name':'张三'})
t1.start()
print(t1)

输出:

我叫张三,今年18岁了
<Thread(t1, stopped 14028)>

可以看到,线程的名字已经成功改为了t1,线程的状态已经是停止了。

那么我们学线程的目的是什么?为了实现线程的并发操作。

例:

import threading
import time
def fun():
    print('hello')
    time.sleep(2)
    print('world')
def fun1():
    print('how are you?')
    time.sleep(2)
    print('fine')
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()

输出:

hello
how are you?
world
fine

好的,看起来结果确实是我们需要的,交替进行,通过观察输出的话,能够看到整个程序执行是需要2s的时间。

我们用程序来计算一下时间:

例:

import threading
import time
def fun():
    print('hello')
    time.sleep(2)
    print('world')
def fun1():
    print('how are you?')
    time.sleep(2)
    print('fine')
start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()
end_time = time.time()
print(end_time-start_time)

输出:

hello
how are you?
0.001026153564453125
world
fine

哎呀,我们发现输出并不是我们想要的,首先是输出时间的位置不对,输出的时间也不对,很明显,输出时间的代码在开始就被执行了,那这是为什么呢?

我们加一行代码,打印一下:threading.enumerate(),这个enumerate方法的功能是返回线程的信息,即有多少个线程,和线程的名字,状态。

import threading
import time
def fun():
    print('hello')
    time.sleep(2)
    print('world')
def fun1():
    print('how are you?')
    time.sleep(2)
    print('fine')
#start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()
# end_time = time.time()
# print(end_time-start_time)
print(threading.enumerate())

输出:

hello
how are you?
[<_MainThread(MainThread, started 2404)>, <Thread(t1, started 4660)>, <Thread(t2, started 2952)>]
fine
world

我们发现,输出的内容中间,线程居然是3个,除了t1和t2之外,还有一个主线程。在上述代码中,主线程、t1、t2是同时执行的,也就是说并不是我们想的那样,t1和t2交叉执行完之后,再执行时间的计算输出,那我们如何解决这问题呢?也就是说等t1和t2执行完毕之后,再执行主线程?加两行代码就可以,jion()。

例:

import threading
import time
def fun():
    print('hello')
    time.sleep(2)
    print('world')
def fun1():
    print('how are you?')
    time.sleep(2)
    print('fine')
start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print(end_time-start_time)

输出:

hello
how are you?
fine
world
2.0018341541290283

发现,没有问题了。那么这里Thread.jion()的作用是什么呢?简单来说,就是等子线程结束之后,再执行主线程。准确一点是说,主线程碰到了Thread.jion(),会阻塞,一直等到子进程执行完,再唤醒主线程,主线程继续执行。另外Thread.jion()中还有一个参数,就是timeout,简单来说,就是主线程最多阻塞这么多时间,如果子线程还没有结束,就不等了,继续执行主线程。


输出:

hello
how are you?
0.716256856918335
worldfine

====================================================================================================================================================================================================

那么下边一个话题,多线程并发真的能够节省时间吗?

我们看两个例子,一个是CPU密集型的两个线程,一个是IO为主的两个线程。

CPU密集型:

import threading
import time
def fun():
    for i in range(10000000):
        sum = i*i*i*i
def fun1():
    for i in range(10000000):
        sum = i*i*i
start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print('time',end_time-start_time)

输出:

time 2.053481340408325

 

import time
def fun():
    for i in range(10000000):
        sum = i*i*i*i
def fun1():
    for i in range(10000000):
        sum = i*i*i
start_time = time.time()
fun()
fun1()
end_time = time.time()
print('time',end_time-start_time)

输出:

time 2.016674518585205

我们可以看到,使用多线程和不适用多线程,耗时差不多,并且多线程甚至还略微多一丢丢时间,是因为线程的切换也是有时间开销的。

IO型为主:

import threading
import time
def fun():
    time.sleep(1)
def fun1():
    time.sleep(1)
start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1')
t2 = threading.Thread(target=fun1,name = 't2')
t1.start()
t2.start()
t1.join()
t2.join()
end_time = time.time()
print('time',end_time-start_time)

输出:

time 1.0026021003723145

 

import time
def fun():
    time.sleep(1)
def fun1():
    time.sleep(1)
start_time = time.time()
fun()
fun1()
end_time = time.time()
print('time',end_time-start_time)

输出:

time 2.0009641647338867

我们可以看到,IO型任务的情况下,多线程确实是要快大约一倍。在例子中,我们用time.sleep()来模拟IO。在真正的IO中,CPU是不用处理IO工作的,在time.sleep()中,CPU也是不需要工作的,所以是一样的。

那么我有一个结论:如果线程任务是以CPU密集型为主,计算时间是不会缩减,反而会略微增加;如果线程任务是以IO为主的,计算时间会较少很多,甚至一半。这是为什么呢?这是因为Python中有GIL锁,简单来说,即使计算机有多个CPU,也只能有一个线程工作。那么为什么多线程在爬虫中也有应用,可以一定程度的提高效率呢?是因为爬虫本身就是下载数据,也就是IO工作。

====================================================================================================================================================================================================

我们来讲下一个话题,在最开始介绍Theard方法时,提到了守护线程,那么什么是守护线程呢?我们仍然以上边例子为例,进行修改。

import threading
import time
def fun():
    print('hello')
    time.sleep(3)
    print('world')
def fun1():
    print('how are you?')
    time.sleep(3)
    print('fine')
start_time = time.time()
t1 = threading.Thread(target=fun,name = 't1',daemon=True)
t2 = threading.Thread(target=fun1,name = 't2',daemon=True)
t1.start()
t2.start()
t1.join(timeout=0.3)
t2.join(timeout=0.4)
end_time = time.time()
print(end_time-start_time)

输出:

hello
how are you?
0.7178466320037842

可以看到,t1和t2的最后一个输出并没有执行,程序就结束了。在t1和t2的实例化中,只是加入了参数的设置:daemon=True。这就是守护线程。

总结说,如果daemon=False(默认值),该进程为非守护进程,反之如果daemon=True,该进程就是守护进程。守护,顾名思义,守护并且共存亡的意思。主线程结束,子线程也结束。

====================================================================================================================================================================================================

再谈下一个话题,如何保证两个子进程交替执行。

例:

import threading
import time
# 定义线程运行函数
def ou():
    for i in range(0,10,2):
        print(i)
        time.sleep(0.5)
def ji():for i in range(1,10,2):
        print(i)
        time.sleep(0.5)
if __name__ == '__main__':
    th = threading.Thread(target=ji)
    th2 = threading.Thread(target=ou)
    th.start()
    th2.start()

输出:

0
1
23

5
4
76

89

我们发现输出并没有规律,但是我就是想让两个进程交替执行,输出的奇偶交叉,怎么办?

可以参考我前边的案例。

 

posted @ 2021-11-04 18:37  理工—王栋轩  阅读(74)  评论(0编辑  收藏  举报