Py004-01-18多线程实现并发套接字

多线程实现并发套接字

非并发套接字(最初版)

server.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import socket

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
phone.bind(('127.0.0.1',8083)) #0-65535:0-1024给操作系统使用
phone.listen(5)

print('starting...')
while True: # 链接循环
conn,client_addr=phone.accept()
print(client_addr)

while True: #通信循环
try:
data=conn.recv(1024)
if not data:break #适用于linux操作系统
print('客户端的数据',data)

conn.send(data.upper())
except ConnectionResetError: #适用于windows操作系统
break
conn.close()

phone.close()

client.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import socket

phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)

phone.connect(('127.0.0.1',8081))

while True:
msg=input('>>: ').strip() #msg=''
if not msg:continue
phone.send(msg.encode('utf-8')) #phone.send(b'')
# print('has send')
data=phone.recv(1024)
# print('has recv')
print(data.decode('utf-8'))

phone.close()

并发版本

仅仅需改善server.py

线程版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from socket import *
from threading import Thread

def communicate(conn):
while True:
try:
data=conn.recv(1024)
if not data:break
conn.send(data.upper())
except ConnectionResetError:
break

conn.close()

def server(ip,port):
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip,port))
server.listen(5)

while True:
conn, addr = server.accept()
t=Thread(target=communicate,args=(conn,))
t.start()

server.close()

if __name__ == '__main__':
server('127.0.0.1', 8081)

线程池版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from socket import *
from concurrent.futures import ThreadPoolExecutor

def communicate(conn):
while True:
try:
data=conn.recv(1024)
if not data:break
conn.send(data.upper())
except ConnectionResetError:
break

conn.close()

def server(ip,port):
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip,port))
server.listen(5)

while True:
conn, addr = server.accept()
pool.submit(communicate,conn)

server.close()

if __name__ == '__main__':
pool=ThreadPoolExecutor(2)
server('127.0.0.1', 8081)

Py004-01-17线程queue

线程queue

queue is especially useful in threaded programming when information must be exchanged safely between multiple threads.

有三种不同的用法

队列

class queue.Queue(maxsize=0) #队列:先进先出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import queue

q=queue.Queue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())
# print(q.get()) 只put三次 但是get第四次就会阻塞了 队列里就三个。怎么可能取到第四个



'''
结果(先进先出):
first
second
third
'''
----------------------------------------------------------
# 队列限制数量
import queue

q2=queue.Queue(3)
q2.put('first')
q2.put('second')
q2.put('third')

# 这样队列满了就阻塞 默认阻塞
q2.put(4) # 等价于 q2.put(4,block=True)
# q2.put(4,block=True,timeout=3) # 3秒内没人把值取走 也报错
----------------------------------------------------------
import queue

q3=queue.Queue(3)
q3.put('first')
q3.put('second')
q3.put('third')

# 这样队列满了就不阻塞 于是报错
q3.put(4,block=False) # 报错 queue.Full
# 同样效果的不等待
# q3.put_nowait(4)

--------------------------------------------------------
# 取值的时候设置超时时间 超过就报错
q3.get(block=True,timeout=3)

堆栈

class queue.LifoQueue(maxsize=0) #堆栈:last in fisrt out(后进先出)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import queue

q=queue.LifoQueue()
q.put('first')
q.put('second')
q.put('third')

print(q.get())
print(q.get())
print(q.get())



'''
结果(后进先出):
third
second
first
'''

优先级队列

class queue.PriorityQueue(maxsize=0) #优先级队列:存储数据时可设置优先级的队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import queue

q=queue.PriorityQueue()
#put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高
q.put((20,'a'))
q.put((10,'b'))
q.put((30,'c'))

print(q.get())
print(q.get())
print(q.get())



'''
结果(数字越小优先级越高,优先级高的优先出队):
(10, 'b')
(20, 'a')
(30, 'c')
'''

Py004-01-16信号量和Event

信号量

信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from threading import Thread,Semaphore
import threading
import time

def func():
sm.acquire()
print('%s 占住了坑位' %threading.current_thread().getName())
time.sleep(3)
print('%s 解脱了-------------------\n' % threading.current_thread().getName())
sm.release()

if __name__ == '__main__':
# 公共厕所只有3个坑
sm=Semaphore(3)

# 23个人冲进厕所
for i in range(23):
t=Thread(target=func)
t.start()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from threading import Thread,Semaphore,currentThread
import time,random

# 公共厕所只有三个坑
sm=Semaphore(3)

def task():
with sm:
print('%s get 厕所' %currentThread().getName())
time.sleep(random.randint(1,3))
print('%s out 厕所----------\n' % currentThread().getName())


if __name__ == '__main__':
for i in range(10):
t=Thread(target=task)
t.start()

解析

1
2
3
4
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

Event

线程的一个关键特性是每个线程都是独立运行且状态不可预测。

如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

1
2
3
4
5
6
7
8
9
from threading import Event

event.isSet():返回event的状态值;是否被触发

event.wait():如果 event.isSet()==False将阻塞线程;

event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;

event.clear():恢复event的状态值为False。

上课实例

在高中以下,老师没宣布下课大部分人都会停在座位上。。。佯装上课。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from threading import Thread,Event
import time

event=Event()

def student(name):
print('学生%s 正在听课----------' %name)
event.wait()
print('学生%s 下课并开始自由活动--吃鸡吃鸡!!!' %name)


def teacher(name):
print('老师%s 正在授课' %name)
time.sleep(7)
event.set()


if __name__ == '__main__':
stu1=Thread(target=student,args=('刘备',))
stu2=Thread(target=student,args=('关羽',))
stu3=Thread(target=student,args=('张飞',))
t1=Thread(target=teacher,args=('诸葛',))

stu1.start()
stu2.start()
stu3.start()
t1.start()

'''
学生调用 event.wait() 就会一直等待老师 宣布下课(调用event.set())
所以开始上课后 直到老师 通知下课(event.set())学生才被释放
'''

有木有学生不等老师的情况?

有,大学里 老师讲课学生不会等老师宣布,因为你会逃课

设置超时时间

如果20分钟内你不点名,我就走了。。。哈哈哈

1
2
3
4
5
# wait能接受参数,单位是秒——超时时间
def student(name):
print('学生%s 正在听课----------' %name)
event.wait(2)
print('学生%s 下课并开始自由活动--吃鸡吃鸡!!!' %name)

例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from threading import Thread,Event,currentThread
import time

event=Event()

def conn():
n=0
while not event.is_set():
if n == 3:
print('%s try too many times' %currentThread().getName())
return
print('%s try %s' %(currentThread().getName(),n))
event.wait(0.5)
n+=1

print('%s is connected' %currentThread().getName())


def check():
print('%s is checking' %currentThread().getName())
time.sleep(5)
event.set()


if __name__ == '__main__':
for i in range(3):
t=Thread(target=conn)
t.start()
t=Thread(target=check)
t.start()

定时器

看看javascript的定时器

1
2
3
4
5
6
7
8
9
10
setTimeout(function(){
console.log('冲咖啡。。。')
},1000)
// 一秒后 执行该函数


setInterval(function(){
console.log('抢票。。。')
},5000)
// 每5秒 看一下出票信息

python 的定时器,指定n秒后执行某操作

1
2
Timer(延迟时间,执行的函数,args=?,kwargs=?) 
支持给 执行函数传递参数
1
2
3
4
5
6
7
from threading import Timer

def hello():
print("hello, world")

t = Timer(1, hello)
t.start() # after 1 seconds, "hello, world" will be printed

Py004-01-15死锁和递归锁

死锁现象

所谓死锁:

是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程,如下就是死锁

通俗的讲:

就是你和你同事回家时拿错了彼此的钥匙,于是俩人都开不了门。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()

class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)

mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()

mutexA.release()

def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)

mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()

mutexB.release()

if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()

执行效果

1
2
3
4
5
Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
#出现死锁,整个程序阻塞住

递归锁

解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。

这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from threading import Thread,RLock
import time

mutexA=mutexB=RLock() #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止

class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('\033[41m%s 拿到A锁\033[0m' %self.name)

mutexB.acquire()
print('\033[42m%s 拿到B锁\033[0m' %self.name)
mutexB.release()

mutexA.release()

def func2(self):
mutexB.acquire()
print('\033[43m%s 拿到B锁\033[0m' %self.name)
time.sleep(2)

mutexA.acquire()
print('\033[44m%s 拿到A锁\033[0m' %self.name)
mutexA.release()

mutexB.release()

if __name__ == '__main__':
for i in range(10):
t=MyThread()
t.start()

非常像堆栈操作

1
2
3
4
5
问索引2的左括号对应的右括号在哪?
(((((()()()())))(()))())

( ==> +1
) ==> -1

Py004-01-14GIL全局解释器锁

GIL全局解释器锁

本质就是一个互斥锁

引子

定义:

1
2
3
4
5
6
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple 
native threads from executing Python bytecodes at once. This lock is necessary mainly
because CPython’s memory management is not thread-safe. (However, since the GIL
exists, other features have grown to depend on the guarantees that it enforces.)

结论:在Cpython解释器中,同一个进程下开启的多线程,同一时刻只能有一个线程执行,无法利用多核优势

首先需要明确的一点是GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。>有名的编译器例如GCC,INTEL C++,Visual C++等。Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。像其中的JPython就没有GIL。然而因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL归结为Python语言的缺陷。所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL

GIL介绍

GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

要想了解GIL,首先确定一点:每次执行python程序,都会产生一个独立的进程。例如python test.py,python aaa.py,python bbb.py会产生3个不同的python进程

验证python test.py只会产生一个进程

1
2
3
4
5
6
7
8
9
10
11
12
13
#test.py内容
import os,time
print(os.getpid())
time.sleep(1000)

#打开终端执行
python3 test.py

#在windows下查看
tasklist |findstr python

#在linux下下查看
ps aux |grep python

在一个python的进程内,不仅有test.py的主线程或者由该主线程开启的其他线程,还有解释器开启的垃圾回收等解释器级别的线程,总之,所有线程都运行在这一个进程内,毫无疑问

1
2
3
4
1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码)
例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。

2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

综上:

如果多个线程的target=work,那么执行流程是

多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码三

GIL与Lock

机智的同学可能会问到这个问题:Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?

首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock,如下图

分析:

  1. 100个线程去抢GIL锁,即抢执行权限
  2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
  3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
  4. 直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程

代码示范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from threading import Thread,Lock
import os,time
def` work():
global n
lock.acquire()
temp=n
time.sleep(0.1)
n=temp-1
lock.release()
if __name__ == '__main__':
lock=Lock()
n=100
l=[]
for i in range(100):
p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()

print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99

GIL与多线程

有了GIL的存在,同一时刻同一进程中只有一个线程被执行

听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php才是最牛逼的语言?

别着急啊,老娘还没讲完呢。

要解决这个问题,我们需要在几个点上达成一致:

  1. cpu到底是用来做计算的,还是用来做I/O的?

  2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能

  3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。

如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,

反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高

结论:

  1. 对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
  2. 当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

假设我们有四个任务需要处理,处理方式肯定是要玩出并发的效果,解决方案可以是:

方案一:开启四个进程
方案二:一个进程下,开启四个线程

单核情况下,分析结果:

如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜
如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜

多核情况下,分析结果:

如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行用不上多核,方案一胜
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

结论:

现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

多线程性能测试

如果并发的多个任务是计算密集型:多进程效率高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res=0
for i in range(100000000):
res*=i


if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(4):
p=Process(target=work) #耗时5s多
p=Thread(target=work) #耗时18s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))

如果并发的多个任务是I/O密集型:多线程效率高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)
print('===>')

if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(400):
# p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
p=Thread(target=work) #耗时2s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))

应用:

1
2
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析

Py004-01-13互斥锁

互斥锁

多线程下操作同一数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
from threading import Thread

n = 100

def task():
global n
temp = n
time.sleep(0.1)
n = temp-1

if __name__ == '__main__':
arr = []
for i in range(100):
t = Thread(target=task)
arr.append(t)
t.start()

# 保证所有线程都执行完毕
for t in arr:
t.join()

print('主',n)

执行结果:
主 99

此时就不安全了,100个线程都操作了但是数据还是99

互斥锁的应用

牺牲效率来保证数据的安全——将并行变成串行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import time
from threading import Thread,Lock

n = 100

def task(mutex):
global n
mutex.acquire()
temp = n
time.sleep(0.1)
n = temp-1
mutex.release()

if __name__ == '__main__':
mutex=Lock()
arr = []
for i in range(100):
t = Thread(target=task,args=(mutex,))
arr.append(t)
t.start()

# 保证所有线程都执行完毕
for t in arr:
t.join()

print('主',n)

Py004-01-12守护线程

守护线程

无论是进程还是线程,都遵循:守护xxx会等待主xxx运行完毕后被销毁

需要强调的是:运行完毕并非终止运行

1
2
3
1、对主进程来说,运行完毕指的是主进程代码运行完毕

2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕

详细解释:

1
2
3
1、主进程在其代码结束后就已经算运行完毕了(守护进程在此时就被回收),然后主进程会一直等非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束,

2、主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,而进程必须保证非守护线程都运行完毕后才能结束。

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)

if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.setDaemon(True) #必须在t.start()之前设置
t.start()

print('主线程')
print(t.is_alive())

执行结果

1
2
主线程
True

练习

思考下述代码的执行结果有可能是哪些情况?为什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from threading import Thread
import time

def foo():
print(123)
time.sleep(1)
print("end123")

def bar():
print(456)
time.sleep(3)
print("end456")

if __name__ == '__main__':
t1=Thread(target=foo)
t2=Thread(target=bar)

t1.daemon=True
t1.start()
t2.start()
print("main-------")

'''
123
456
main-------
end123 (守护线程虽然要等主线程结束才陪葬 ,但是此守护线程已经执行结束——死了)
end456 (主线程要等待其他非守护线程结束而结束)
'''

Py004-01-11线程其他属性和方法

Thread对象的其他属性或方法

介绍

1
2
3
4
5
6
7
8
9
Thread实例对象的方法
# isAlive(): 返回线程是否活动的。
# getName(): 返回线程名。
# setName(): 设置线程名。

threading模块提供的一些方法:
# threading.currentThread(): 返回当前的线程变量。
# threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
# threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from threading import Thread
import threading
from multiprocessing import Process
import os

def work():
import time
time.sleep(3)
print(threading.current_thread().getName())


if __name__ == '__main__':
#在主进程下开启线程
t=Thread(target=work)
t.start()

print(threading.current_thread().getName())
print(threading.current_thread()) #主线程
print(threading.enumerate()) #连同主线程在内有两个运行的线程
print(threading.active_count())
print('主线程/主进程')

执行结果

1
2
3
4
5
MainThread
<_MainThread(MainThread, started 140735268892672)>
[<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>]
主线程/主进程
Thread-1

主线程等待子线程结束

1
2
3
4
5
6
7
8
9
10
11
12
from threading import Thread
import time
def sayhi(name):
time.sleep(2)
print('%s say hello' %name)

if __name__ == '__main__':
t=Thread(target=sayhi,args=('egon',))
t.start()
t.join()
print('主线程')
print(t.is_alive())

执行结果

1
2
3
egon say hello
主线程
False

Py004-01-10线程和进程的区别

线程和进程的区别

谁的开启速度快?

1、在主进程下开启线程

1
2
3
4
5
6
7
8
9
from threading import Thread

def work():
print('hello')

if __name__ == '__main__':
t=Thread(target=work)
t.start()
print('主线程/主进程')

执行结果如下,几乎是t.start ()的同时就将线程开启了,然后先打印出了hello,证明线程的创建开销极小

1
2
hello
主线程/主进程

2、在主进程下开启子进程

1
2
3
4
5
6
7
8
9
10
from multiprocessing import Process

def work():
print('hello')

if __name__ == '__main__':
#在主进程下开启子进程
p=Process(target=work)
p.start()
print('主线程/主进程')

执行结果如下,p.start ()将开启进程的信号发给操作系统后,操作系统要申请内存空间,让好拷贝父进程地址空间到子进程,开销远大于线程

1
2
主线程/主进程
hello

瞅一瞅pid?

1、在主进程下开启多个线程,每个线程都跟主进程的pid一样

1
2
3
4
5
6
7
8
9
10
11
12
from threading import Thread
import os

def work():
print('hello',os.getpid())

if __name__ == '__main__':
t1=Thread(target=work)
t2=Thread(target=work)
t1.start()
t2.start()
print('主线程/主进程pid',os.getpid())

执行结果

1
2
3
hello 7939
hello 7939
主线程/主进程 7939

2、开多个进程,每个进程都有不同的pid

1
2
3
4
5
6
7
8
9
10
11
12
from multiprocessing import Process
import os

def work():
print('hello',os.getpid())

if __name__ == '__main__':
p1=Process(target=work)
p2=Process(target=work)
p1.start()
p2.start()
print('主线程/主进程',os.getpid())

执行结果

1
2
3
主线程/主进程 7951
hello 7952
hello 7953

同一进程内的线程共享该进程的数据?

1、进程之间地址空间是隔离的

1
2
3
4
5
6
7
8
9
10
11
12
13
from multiprocessing import Process
import os

def work():
global n
n=0

if __name__ == '__main__':
n=100
p=Process(target=work)
p.start()
p.join()
print('主',n)

执行结果如下,毫无疑问子进程p已经将自己的全局的n改成了0,但改的仅仅是它自己的,查看父进程的n仍然为100

1
主 100

2、同一进程内开启的多个线程是共享该进程地址空间的

1
2
3
4
5
6
7
8
9
10
11
12
13
from threading import Thread
import os

def work():
global n
n=0

if __name__ == '__main__':
n=100
t=Thread(target=work)
t.start()
t.join()
print('主',n)

执行结果如下, 查看结果为0,因为同一进程内的线程之间共享进程内的数据

1
主 0

Py004-01-09线程

threading模块介绍

multiprocess模块的完全模仿了threading模块的接口,二者在使用层面,有很大的相似性,因而不再详细介绍

方式一:

1
2
3
4
5
6
7
8
9
10
11
12
from threading import Thread
import time

def task(name):
time.sleep(2)
print('%s say hi'%name)

if __name__ == '__main__':
for i in range(10):
t = Thread(target=task, args=('aa%s'%i,))
t.start()
print('主')

方式二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from threading import Thread
import time

class Sayhi(Thread):
def __init__(self,name):
super().__init__()
self.name=name
def run(self):
time.sleep(2)
print('%s say hello' % self.name)

if __name__ == '__main__':
t = Sayhi('egon')
t.start()
print('主线程')