线程理论
特点:
1.开启一个进程时,一般会开启多个线程;
2.一个进程下的多个线程共享进程的内存空间;
3.开启线程比开启进程开销更小.
举例:开启一个字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
工作方式:使用多个线程和进程时,主机操作系统负给每个进程和线程分配一个小的时间片,并在所有活动任务之间快速循环,给每个任务分配一个可用的cpu周期.
线程共享内存示例:
from threading import Thread
n = 100
def run(m):
global n
n = m
if __name__ == '__main__':
t = Thread(target=run, args=(10,))
t1 = Thread(target=run, args=(20,))
t.start()
t.join()
t1.start()
t1.join()
print('n:',n ) # 20
创建线程开销小:创建一个进程,就是创建一个车间,涉及到申请空间,而且在该空间内建至少一条流水线,但是创建线程,就只是在一个车间内造一条流水线,无需申请空间,所以创建开销小.
# 线程开启的要比进程的快,从主进程打印顺序的前后就可以看出
from threading import Thread
from multiprocessing import Process
def run():
print('run>>>>>>')
if __name__ == '__main__':
# t = Thread(target=run)
# t.start()
'''
run>>>>>>
主
'''
p = Process(target=run)
p.start()
print('主')
'''
主
run>>>>>>
'''
Thread模块
方法:
isAlive()
: 返回线程是否活动的;
getName()
: 返回线程名;
setName()
: 设置线程名;
threading.currentThread()
: 返回当前的线程变量;
threading.enumerate()
:返回一个包含正在运行的线程的list
。正在运行指线程启动后、结束前,不包括启动前和终止后的线程;
threading.activeCount()
:返回正在运行的线程数量,与len(threading.enumerate())
有相同的结果。
from threading import (Thread, activeCount, enumerate, current_thread)
import os
import time
def run():
print('子线程<%s> start' % current_thread().name)
time.sleep(2)
print('子线程<%s> finish' % current_thread().name)
if __name__ == '__main__':
t = Thread(target=run)
t.start()
# 返回当前活跃的线程数量
# print(activeCount())
# 返回当前进程活跃的线程对象列表
print(' 当前活跃线程列表: %s' % enumerate())
t.join()
print('子线程是否存活' % t.isAlive())
print('主线程<%s> 结束' % current_thread().name)
守护线程
无论是线程还是进程, 都遵循守护xx会在主xx运行完毕后被销毁;
线程 & 进程运行完毕的区别
主进程在其代码结束后就已经算运行完毕了,直接回收守护子进程,然后等非守护的子进程都运行完毕后回收资源.
主线程在其他非守护线程运行完毕后才算运行完毕(守护线程在此时就被回收).
from threading import Thread
from multiprocessing import Process
import time
def foo():
print(123)
time.sleep(1)
print("end123")
def bar():
print(456)
time.sleep(2)
print("end456")
# t2运行完毕, 此时切换到主线程,直接回收守护线程thread<t1>
if __name__ == '__main__':
# t1=Thread(target=foo)
t1=Process(target=foo)
# t2=Thread(target=bar)
t2=Process(target=bar)
t1.daemon=True
t1.start()
t2.start()
print("main-------")
# 等待所有非守护子线程运行完毕, 即thread<t2>
线程互斥锁
功能同进程互斥锁一样.
from threading import Thread, Lock
import time
n = 100
def run(mutex):
global n
time.sleep(0.1)
mutex.acquire()
# 此处若修改涉及io操作就很明显了
n -= 1
print(n)
mutex.release()
if __name__ == '__main__':
mutex = Lock()
t_lst = []
for i in range(100):
t = Thread(target=run, args=(mutex, ))
# 加入线程列表中
t_lst.append(t)
# 启动每个线程
t.start()
for i in t_lst:
i.join()
print('主线程: n=%s' %n)
全局解释器锁GIL
代码执行过程:
- 子线程先拿到用户代码执行权限;
- 再去解释器拿解释器代码的执行权限;
- 拿到解释器权限后将用户代码作为参数传递给解释器,由解释器执行用户代码.
下图所示,保证python解释器同一时间只能执行一个任务的代码:
GIL
和lock
是两把锁,保护的数据不一样.前者是解释器级别的,保护的是解释器级别的数据,比如垃圾回收,后者是保护用户自己开发的应用程序的数据.
多线程 & 多进程 选择
多线程用于IO密集型,例如socket/爬虫/web;
多进程用户计算密集型,例如金融分析.
# i/o密集型
from multiprocessing import Process
from threading import Thread
import threading
import os, time
def work():
print('子进程<%s> 开始'% os.getpid())
time.sleep(3)
print('子进程<%s> ================='% os.getpid())
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 本机为4核
start = time.time()
for i in range(100):
# 多进程: 8.8s
# 1. 创建进程耗费时间;
# 2. 四核并行, 遇到io阻塞切换较慢
p = Process(target=work)
# 多线程: 3.05s
# 1. 所有线程并发启动程序
# 2. 遇到阻塞切换速度快
# p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
stop = time.time()
print('run time is %s' % (stop - start))
# 计算密集型
from multiprocessing import Process
from threading import Thread
import os,time
def work():
print('子进程<%s>' % os.getpid())
res=0
for i in range(100000000):
res += i
print('子进程<%s> %s' % (os.getpid(), res))
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(4):
# 耗时14.3s多,开始四个进程,运用四个cpu同时运算
p=Process(target=work)
# 耗时26.8s多,因为一个进程内,同一时刻,
# 只能有一个线程使用cpu计算,没有发挥多核优势
# p=Thread(target=work)
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
死锁Lock
两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象.若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁, 这些永远在互相等待的进程称为死锁进程.
互斥锁只能aquire()
一次,想要再次拿到必须先释放掉.
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):
'''
1. thread1先执行,会优先拿到B锁,
2. thread2执行func1,拿到A锁, 等待thread1释放B锁
3. thread1继续运行,准备到A锁, 等待thread2释放A锁
4. 两个线程执手相看泪眼, 就这么一直耗着!
'''
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(3):
t = MyThread()
t.start()
递归锁Rlock
可以被同一个线程获取多次,每被获取一次内置计数器加1,在此期间其他线程不能获取,直到计数器为0.
from threading import Thread, RLock
import time
mutexA = mutexB = RLock()
class MyThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
"""一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,
这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
"""
print('<%s>f1' % self.name)
mutexA.acquire()
print('<%s>f1 拿到A锁' %self.name)
mutexB.acquire()
print('<%s>f1 拿到B锁' % self.name)
mutexB.release()
print('<%s>f1 释放B锁' %self.name)
mutexA.release()
print('<%s>f1 释放A锁' %self.name)
def func2(self):
'''一旦释放完后,所有的线程都公竞争抢,谁先抢到谁使用'''
print('<%s>f2' % self.name)
mutexB.acquire()
print('<%s>f2 拿到B锁' % self.name)
# 抢到 B锁 的线程已经绑定R锁,
# 其他的线程必须等你释放所有的acquire才能继续抢
print('<%s>f2 阻塞' % self.name)
time.sleep(2)
mutexA.acquire()
print('<%s>f2 拿到A锁' %self.name)
mutexA.release()
print('<%s>f2 释放A锁' % self.name)
mutexB.release()
print('<%s>f2 释放B锁' %self.name)
信号量Semaphore
也是一把锁,但是可以允许设定最大值.
例如s_lock = Semaphore(5)
可以允许最多同时被5个线程抢到并执行.
Semaphore
管理一个内置的计数器,每当调用acquire()
时内置计数器-1
;
调用release()
时内置计数器+1
;计数器不能小于0;当计数器为0时,acquire()
将阻塞线程直到其他线程调用release()
.
from threading import Thread, Semaphore, Lock
import threading
import time
def func():
print('准备抢锁')
sm.acquire()
print('<%s> 拿到锁' % threading.current_thread().getName())
time.sleep(2)
# 待阻塞过后,随机释放内部线程的锁,空出位置后,
# 同时, 其他的线程又可以抢锁,进入sm中;
sm.release()
print('<%s> 释放锁' % threading.current_thread().getName())
if __name__ == '__main__':
sm = Semaphore(2)
# sm = Lock()
for i in range(5):
t = Thread(target=func)
t.start()
信号Event
线程之间根据event对象的状态来执行,其他线程也可以设置event对象的状态;
Event对象中的信号标志默认为False
.
如果有线程等待一个Event对象,而这个Event对象的标志为False
,那么这个线程将会被一直阻塞直至该标志为True
;
一个线程如果将一个Event对象的信号标志设置为True
,它将唤醒所有等待这个Event对象的线程;
如果一个线程等待一个已经被设置为True
的Event对象,那么它将忽略这个事件,继续执行;
方法
event = Event()
: 设置信号对象event
;
event.is_set() = False
: 默认为False
;
event.set()
: 设置event
为True
;
from threading import Thread, Event
import threading
import time
def conn_mysql():
count = 1
# 若event的状态值为False则执行;
while not event.is_set():
if count > 3:
raise TimeoutError('链接超时')
print('<%s>第%s次尝试链接'%(threading.current_thread().getName(), count))
# 如果event.is_set()==False将阻塞线程, 若期间event被设置为True,则立即结束等待,往下执行
event.wait(3)
count += 1
print('<%s>链接成功' % threading.current_thread().getName())
def check_mysql():
print('\033[45m[%s]正在检查mysql\033[0m' % threading.current_thread().getName())
time.sleep(5)
event.set() # 设置event状态为True,所有阻塞池的线程激活进入就绪状态,等待操作系统调用
if __name__ == '__main__':
event = Event()
conn1 = Thread(target=conn_mysql)
conn2 = Thread(target=conn_mysql)
check = Thread(target=check_mysql)
conn1.start()
conn2.start()
check.start()
定时器
指定n秒之后执行某个动作,实质上是开启了子线程取执行动作.
格式: t = Timer(interval, func)
.
from threading import Timer, current_thread
def say_hi():
print('<%s> hello world!' % current_thread().name)
# 指定1秒之后执行sayhi这个函数
t = Timer(2, say_hi)
t.start()
print('<%s> 主线程' % current_thread().name)
'''
<MainThread> 主线程
<Thread-1> hello world!
'''
随机验证码定时刷新
import random
import string
from threading import Timer
class Code:
def __init__(self):
self.make_cache()
def make_cache(self, interval=6):
"""6s后会自动生成随机生成验证码"""
self.cache = self.make_code()
self.t = Timer(interval, self.make_cache)
self.t.start()
def make_code(self, n=4):
res = ''
for i in range(n):
s = str(random.choice(string.hexdigits))
res += s
print('\n'+res)
return res
def check(self):
while True:
code = input('请输入您的验证码>>> ').strip()
if code == self.cache:
print('验证码输入正确!')
# 取消已设定的定时器
self.t.cancel()
break
if __name__ == '__main__':
c = Code()
c.check()
线程Queue
同进程的Queue类似,可以实现线程之间安全通讯.
常用方法:
q.put
: 用以插入数据到队列中,put方法还有两个可选参数:blocked
和timeout
.如果blocked=True
(默认值),并且timeout
为正值,该方法会阻塞timeout
指定的时间,直到该队列有剩余的空间,如果超时,会抛出Queue.Full
异常;如果blocked=False
,但该Queue
已满,会立即抛出Queue.Full
异常.
q.get
: 可以从队列读取并且删除一个元素。同样,get方法有两个可选参数:blocked
和timeout
。如果blocked=True
(默认值),并且timeout
为正值,那么在等待时间内没有取到任何元素,会抛出Queue.Empty
异常。如果blocked=False
,有两种情况存在,如果Queue有一个值可用,则立即返回该值,否则,如果队列为空,则立即抛出Queue.Empty
异常;
q.get_nowait()
:同q.get(False)
;
q.put_nowait()
:同q.put(False)
;
q.empty()
: 调用此方法时q为空则返回True
,该结果不可靠,比如在返回True的过程中,如果队列中又加入了项目。
q.full()
: 调用此方法时q已满则返回True
,该结果不可靠,比如在返回True的过程中,如果队列中的项目被取走。
q.qsize()
: 返回队列中目前项目的正确数量,结果也不可靠,理由同q.empty()
和q.full()
一样.
队列:queue.Queue()
,先进先出:
import queue
q=queue.Queue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
堆栈: class queue.LifoQueue(maxsize=0)
,先进后出:
import queue
q=queue.LifoQueue()
q.put('1')
q.put('2')
q.put('3')
print(q.get())
print(q.get())
print(q.get())
优先级队列: class queue.PriorityQueue(maxsize=0)
: 存储数据时可设置优先级的队列.
import queue
q=queue.PriorityQueue()
# put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),
# 数字越小优先级越高
q.put((20,'22'))
q.put((1,'100'))
q.put((100,'2'))
print(q.get())
print(q.get())
print(q.get())
'''
结果(数字越小优先级越高,优先级高的优先出队):
(1, '100')
(20, '22')
(100, '2')
'''
进程池和线程池
限定同时运行的最大线程和进程个数,不是传统意义上的开启线程和进程.
concurrent.futures
模块提供了高度封装的异步调用接口;
ThreadPoolExecutor
线程池模块;
ProcessPoolExecutor
进程池模块;
常用方法
submit(fn, *args, **kwargs)
:异步提交任务;
map(func, *iterables, timeout=None, chunksize=1)
shutdown(wait=True)
: 相当于进程池的pool.close()
+pool.join()
操作;
result(timeout=None)
: 取得结果;
add_done_callback(fn)
: 回调函数;
创建线程池:
# 线程池创建
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(10)
pool.submit(func, *args) # 此时会立即创建并启动线程
进程池实例化:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import os, time
from multiprocessing import Process, current_process
def task(n):
time.sleep(1)
print('线程<%s><%s>正在运行' %(current_process().name, os.getpid()))
return n**2
if __name__ == '__main__':
# 生成进程池对象,最大同时运行进程数目为4
pool = ProcessPoolExecutor(4)
futures = []
for i in range(5):
# 1. 将任务及其参数扔进进程池
# 2. 进程池自行启动进程
# 3. 返回执行结果
future = pool.submit(task, i)
futures.append(future)
# 1. 等待进程池内的进程执行完
# 2. 关闭关闭进程池
pool.shutdown(wait=True)
print('进程池内部进程运行完毕')
for f in futures:
# 取得返回结果
print(type(f), f.result())
print('主')
改进版本:可以运用map(func, *iterables,timeout=None,chunksize=1)
取代for循环submit的操作.
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import os, time
def task(n):
time.sleep(1)
print('进程<%s>正在运行' % os.getpid())
return n**2
if __name__ == '__main__':
# 生成进程池子
process_pool = ProcessPoolExecutor(max_workers=4)
futures = []
# 取代for循环,加入4个进程任务到进程池子
process_pool.map(task, range(1, 5))
# 等待池内所有任务执行完毕回收完资源后才继续
process_pool.shutdown(wait=True)
print('主进程<%s>' % os.getpid())
线程池同步调用示例:串行执行线程池内的线程
import time
import random
from concurrent.futures import ThreadPoolExecutor, thread
def get_ticket(name):
'''购票'''
print('%s 购票中' % name)
time.sleep(random.randint(2,4))
price = random.randint(1000, 2000)
return {'name':name, 'price':price}
def board(ticket):
'''选座位'''
name = ticket['name']
seat_num = random.randint(1, 100)
time.sleep(1)
print('%s, 您的座位是%s' % (name, seat_num))
if __name__ == '__main__':
# 生成线程池
pool = ThreadPoolExecutor(10)
# 同步调用,提交任务后,由于要获取结果,所以在原地阻塞,一旦拿到结果,立即执行下一行代码
ticket1 = pool.submit(get_ticket, 'alex').result()
ticket2 = pool.submit(get_ticket, 'jim').result()
ticket3 = pool.submit(get_ticket, 'bob').result()
board(ticket1)
board(ticket2)
board(ticket3)
print('购票完成!')
异步调用加回调机制
import time
import random
from concurrent.futures import ThreadPoolExecutor
import threading
def get_ticket(name):
print('%s 购票中 --<%s>' %
(name, threading.current_thread().getName()))
time.sleep(random.randint(2, 4))
price = random.randint(1000, 2000)
return {'name':name, 'price':price}
def board(ticket):
# 传入的是对象,还要拿取返回结果
ticket = ticket.result()
name = ticket['name']
seat_num = random.randint(1, 100)
time.sleep(1)
print('%s, 您的座位是%s' % (name, seat_num))
if __name__ == '__main__':
pool = ThreadPoolExecutor(10)
# 将自身作为对象传给回调函数`add_done_callback()`作为参数
pool.submit(get_ticket, 'alex').add_done_callback(board)
pool.submit(get_ticket, 'jim').add_done_callback(board)
pool.submit(get_ticket, 'kate').add_done_callback(board)
pool.submit(get_ticket, 'bob').add_done_callback(board)
进程池线程池小练习:多线程异步抓取网络数据
import requests
import time
from concurrent.futures import ThreadPoolExecutor
from threading import current_thread
def get(url):
print('<%s> 正在获取%s的信息' %(current_thread().getName(), url))
time.sleep(2)
response = requests.get(url)
return {'url': url, 'context': response.text}
def parse(future):
context = future.result()['context']
url = future.result()['url']
print('The result of %s is %s'%(url, len(context)))
if __name__ == '__main__':
urls = [
'https://www.cnblogs.com/fqh202',
'http://www.cnblogs.com/fqh202/p/8416062.html',
'http://www.cnblogs.com/fqh202/p/8409933.html',
'http://www.cnblogs.com/fqh202/p/8350463.html',
'http://www.cnblogs.com/fqh202/p/8301777.html',
]
# 生成线程池
pool = ThreadPoolExecutor(3)
for url in urls:
# 1. 往线程池增加线程任务
# 2. 将线程调用程序返回的结果作为参数传入parse()函数
pool.submit(get, url).add_done_callback(parse)
模拟socket线程池异步
# 服务端==========================================================
from socket import *
from threading import Thread, current_thread
from concurrent.futures import ThreadPoolExecutor
import os
def run(conn):
"""进入新线程,开始接收数据"""
while True:
try:
print('线程<%s>准备接收数据--------' % os.getpid())
data = conn.recv(1024)
if not data:
# 若没收到数据,有可能另一端管道已经断开连接,
# 所以这个线程应该断开,否则会无限循环接收数据
print('线程<%s>断开' % os.getpid())
break
send_data = '发送自<多线程><%s>: %s' % (current_thread().name,
data.decode('utf-8'))
conn.send(send_data.encode('utf-8'))
except ConnectionResetError:
break
# 若客户端那边的连接断开,那么这边的conn也应该断开
conn.close()
def server(ip, port, pool):
server = socket(AF_INET, SOCK_STREAM)
server.bind((ip, port))
server.listen(5)
while True:
# 生成套接字对象, 等待客户上门
print("进入待连接状态>>> ")
# 主线程停留于此,一直等待
conn, addr = server.accept()
pool.submit(run, conn)
server.close()
if __name__ == '__main__':
# 创建线程池,最大运行线程数量为2
pool = ThreadPoolExecutor(2)
server('127.0.0.1', 8810, pool)
# 客户端==========================================================
from socket import *
client = socket(AF_INET, SOCK_STREAM)
client.connect(('127.0.0.1', 8810))
while True:
msg = input('>>> ').strip()
if not msg: continue
client.send(msg.encode('utf-8'))
back_data = client.recv(1024)
print(back_data.decode('utf-8'))