线程
若将进程比喻为一个工厂,那么线程就是其中的一个工人。
那么之前我们为了解决问题创建的多进程就等于创建了多个工厂,而每个工厂里面却只放入了一个工人,毫无疑问,这是一种非常浪费资源的现象。
接下来我们就来了解一下线程。
首先我们来具体的了解下线程与进程的区别
线程与进程的区别
1.线程与进程相比占用的系统资源小很多
2.线程共用进程的空间,进程则有自己的内存空间
3.线程可以直接访问他们的父进程的数据,进程则是对他们的父进程进行了数据的副本复制
4.线程与线程可以直接进行通讯,进程则必须使用进程间通讯的方法
5.线程可以对同一空间内的线程有较大的控制权限,进程则只能控制他们的子进程
6.线程的变化可能会影响同一空间内的其他线程,主进程变化则不会影响其余的进程
如何使用线程
由于线程的使用方法与进程类似,再次不多做介绍
直接上实例代码(基于多线程的服务端与客户端通信)
#serve
from threading import Thread
from socket import *
def communicate(conn):
while True:
msg = conn.recv(1024)
print(msg)
conn.send(msg.upper())
conn.close()
def get_accept(ip, port):
print('start')
serve = socket(AF_INET, SOCK_STREAM)
serve.bind((ip, port))
serve.listen(5)
while True:
conn, addr = serve.accept()
t = Thread(target=communicate, args=(conn,))
t.start()
serve.close()
if __name__ == '__main__':
get_accept('127.0.0.1', 8080)
#client
import socket
client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client.connect(('127.0.0.1',8080))
while True:
cmd = input('>>>:')
client.send(cmd.encode('utf-8'))
msg = client.recv(1024)
print(msg)
socket.close()
守护线程
守护线程其实本质是与守护进程是一致的,但是由于进程与线程的机制不同,因此两者的表现形式会有一定的差距。
进程与进程之间是相互独立的,因此主进程并不会收到别的进程影响,会在主进程的代码执行完后便直接死亡,它的守护进程也会因此就结束。
但是先运行的线程不同,他会等待除了所有的非守护线程结束后才会结束,也正因如此,它的守护线程并不会在首先运行的线程代码运行结束后直接死亡,而是在其余的非守护线程结束,且该线程结束后才结束。
#threading模块的一些方法
threading.getname()#获得当前线程的名称
threading.setname()#设置当前线程的名称
threading.isalive()#判断线程是否存活
threading.activeCount()#返回存活的线程总数
GIL
那么我们学习了线程之后,进程是否就是完全无用了呢,答案是否定的。
在cpython中,在一个进程中,同一时刻只有一个线程被执行,原因是python中有GIL。GIL的本质就是一把互斥锁,它主要的目的就是为了保证数据的安全。
那么有些人会说,既然同一时间只能有一个线程被执行,那python的多线程就没有用处了,这也是错误的。
首先我们要明白,cpu他的主要作用是用于计算,因此有无多核最大的区别就是计算时花费的时间不同。因此若全是io操作的程序,那么多核便毫无作用,这种时候我们在选择使用线程还是进程的时候便应该使用线程。因为进程会占用大量的资源,并且启动时间也远远大于线程。
而若是一个全是计算的程序,那么我们就应该优先考虑使用多进程。因为多进程它本身不会被GIL锁所限制
关于GIL的具体内容我们可以参考这个博客:https://www.cnblogs.com/linhaifeng/articles/7449853.html
死锁与递归锁
死锁现象简单来说就是多个线程在争抢锁的时候,由于创建者并没有设置好导致的程序被锁死在原地的情况
import time
from threading import Thread,Lock,current_thread
lock1 = Lock()
lock2 = Lock()
class Test(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
lock1.acquire()
print('%s拿到了锁1' % current_thread().getName())
lock2.acquire()
print('%s拿到了锁2' % current_thread().getName())
lock2.release()
lock1.release()
def f2(self):
lock2.acquire()
print('%s拿到了锁2' % current_thread().getName())
time.sleep(0.1) #让线程停止0.1s,导致别的线程启动抢到了锁1
lock1.acquire()
print('%s拿到了锁1' % current_thread().getName())
lock1.release()
lock2.release()
if __name__ == '__main__':
for i in range(10):
t = Test()
t.start()
如上述代码所示,由于我们在f2函数中让线程停止了0.1s所以导致别的线程抢到了线程1接下来需要的锁1,而又因为线程1拿到了别的线程需要的锁2,因此导致了两个线程互相卡住的情况,导致我们的程序卡在了原地。这就是死锁现象
那么如何解决这个问题,我们使用的是一个RLock模块,他可以让一把锁acquire多次,每acquire一次加一次计数,计数为0时就彻底释放,简单的说就是把之前用两把锁改为用一把锁,保证一个线程未运行完之前不运行下一个线程
import time
from threading import Thread,current_thread,RLock
lock1 = lock2 = RLock()
class Test(Thread):
def run(self):
self.f1()
self.f2()
def f1(self):
lock1.acquire()
print('%s拿到了锁1' % current_thread().getName())
lock2.acquire()
print('%s拿到了锁2' % current_thread().getName())
lock2.release()
lock1.release()
def f2(self):
lock2.acquire()
print('%s拿到了锁2' % current_thread().getName())
time.sleep(0.1)
lock1.acquire()
print('%s拿到了锁1' % current_thread().getName())
lock1.release()
lock2.release()
if __name__ == '__main__':
for i in range(10):
t = Test()
t.start()
信号量Semaphore
这个模块简单的说就是可以限制线程运行的最大数量。使用方法基本与lock一致
import time,random
from threading import Semaphore,Thread,current_thread
s = Semaphore(5)
class Test(Thread):
def run(self):
s.acquire()
print(f'线程:{current_thread().getName()}')
time.sleep(random.randint(1,3))
s.release()
if __name__ == '__main__':
for i in range(10):
t = Test()
t.start()
event事件
这个模块主要用于我们需要一些线程在某个线程操作之后再执行的时候可以使用。
#这个模块的本质其实类似于设置了一个全局变量,通过判断这个全局变量来设置各种状态,下文我们假设其设置的全局变量为event
event.isSet() # 返回当前enent的状态
event.wait() # 若event为False,阻塞线程,直到变成True,或者我们可以设置时间,等到时间结束后自动让行
event.set() # 设置event为True
event.clear() # 设置event为False
定时器
指定多少秒后运行线程,使用方法于Thread一致,不过需要在初始化时添加一个设定的时间
from threading import Timer
def test():
print('test===>')
if __name__ == '__main__':
t = Timer(1,test)
t.start()
线程queue
线程queue于进程的queue的方法一致,也是用于两个线程之间进行通信的
concurrent.futures
因为线程池于进程池概念且用法一样,因此放到一起讲
进程池的概念简单来说就是防止由于过多的进程导致系统的内存被撑爆的情况
若是只有少数的进程被启动,这种时候我们是不需要使用进程池的概念的,因为进程池会降低效率,它的本质就是创建你设定的进程个数,然后用这些进程去完成你的任务,因此最多在程序中只会有你设定的进程个数在运行
使用方法
#使用concurrent.funture模块
import time
import os
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
def task():
print('====>%s'%os.getpid())
time.sleep(2)
if __name__ == '__main__':
p = ProcessPoolExecutor(2)
for i in range(10):
p.submit(task)
print('主')
上述是基本用法,我们若想实现类似join的方法,可以使用p.shutdown
import time
import os
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
def task():
print('====>%s'%os.getpid())
time.sleep(2)
if __name__ == '__main__':
p = ProcessPoolExecutor(2)
for i in range(10):
p.submit(task)
p.shutdown()
print('主')
异步调用:简单的说异步调用就是提交任务后不会再原地等待,程序是并发执行
同步调用:同步调用则是再提交任务后再原地等待,直到任务的结果返回,程序是串行执行
ps:concurrent.funtures 是一个高度封装异步调用的模块
回调函数
import time
import os
from concurrent.futures import ProcessPoolExecutor,ThreadPoolExecutor
def task(n):
print('====>%s'%os.getpid())
time.sleep(2)
return n**2
def parse(n):
n = n.result()
print(n)
if __name__ == '__main__':
p = ProcessPoolExecutor(2)
for i in range(10):
p.submit(task,i).add_done_callback(parse)
p.shutdown()
print('主')
回调函数再python中的用法本身并不难,只要在submit()方法后面添加一个add_done_callback()方法即会在函数结束后立刻执行回调函数
不过值得注意的是,submit()方法返回的是一个对象,因此我们要调用这个方法的result()方法才能获得返回的值