目录
一、开启线程的两种方式
练习:
二、线程之间数据共享
三、线程对象的其他属性和方法
四、守护线程
五、线程互斥锁
六、GIL全局解释器锁
七、死锁与递归锁
八、信号量
九、event事件
十、定时器
十一、线程queue
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
线程是操作系统能进行运算调度的最小单位,是一串指令的集合。它包含在进程当中。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间,相当于一个车间内有多条流水线,都共用一个车间的资源。
每个进程都自带一个线程。
开启一个线程的开销要远远小于开进程
开进程:
申请内存空间,耗时
将代码拷贝到申请的内存空间中,耗时
开线程:
不需要申请内存空间,直接开启
进程与线程的区别:
1、线程共享内存空间;进程的内存是独立的
2、线程共享进程中的数据;进程独立
3、同一个进程的线程之间可以直接交流(数据共享,传递);两个进程想通信,必须
通过一个中间代理来实现
4、创建新线程很简单,创建新进程需要对其父进程进行一次克隆
5、一个线程可以控制和操作同一进程里的其他线程;但是进程只能操作子进程
6、对一个线程的修改可能会影响其他线程;但对进程的修改不会影响子进程
为什么用多线程呢?
1. 多线程共享一个进程的地址空间
2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用
3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。
4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)
案例:
开启一个文字处理软件进程,该进程肯定需要办不止一件事情,比如监听键盘输入,处理文字,定时自动将文字保存到硬盘,这三个任务操作的都是同一块数据,因而不能用多进程。只能在一个进程里并发地开启三个线程,如果是单线程,那就只能是,键盘输入时,不能处理文字和自动保存,自动保存时又不能输入和处理文字。
线程开启时,并不需要导入文件,因此可以不写在if __name__ == '__main__'里面。
1、直接开
from threading import Thread
import time
def task(name):
print("%s is running" % name)
time.sleep(1)
print("%s is over" % name)
t = Thread(target=task, args=('lxx',))
t.start()
print('主')
2、继承类重写run方法
from threading import Thread
import time
class MyThread(Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print("%s is running" % self.name)
time.sleep(1)
print("%s is over" % self.name)
if __name__ == '__main__':
t = MyThread('lsb')
t.start()
print('主')
1、开启三个线程,一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
from threading import Thread
# 一个接收用户输入,一个将用户输入的内容格式化成大写,一个将格式化后的结果存入文件
msg_l = []
format_l = []
def get_input():
while True:
msg = input(">>:").strip()
if len(msg) == 0:
continue
if msg == 'q':
break
msg_l.append(msg)
def exchange():
while True:
if msg_l:
res = msg_l.pop()
format_l.append(res.upper())
def save():
while True:
if format_l:
with open('db.txt', 'a', encoding='utf-8') as f:
res = format_l.pop()
f.write(res + '\n')
if __name__ == '__main__':
t1 = Thread(target=get_input)
t2 = Thread(target=exchange)
t3 = Thread(target=save)
t1.start()
t2.start()
t3.start()
2、多线程实现并发通信
服务端
from threading import Thread
import socket
server = socket.socket()
server.bind(('127.0.0.1', 8080))
server.listen(5)
def task(conn):
while True:
try:
msg = conn.recv(1024)
if len(msg)==0:
break
print(msg.decode('utf-8'))
conn.send(msg.upper())
except ConnectionResetError:
break
if __name__ == '__main__':
while True:
conn,addr = server.accept()
t = Thread(target=task, args=(conn,))
t.start()
客户端
import socket
client = socket.socket()
client.connect(('127.0.0.1', 8080))
while True:
cmd = input(">>:").encode("utf-8")
if len(cmd) == 0:
continue
client.send(cmd)
msg = client.recv(1024)
print(msg.decode("utf-8"))
all the threads in a process have the same view of the memory.
所有在同一个进程里的线程是共享同一块内存空间的。
from threading import Thread
x = 100
def task():
global x
x = 666
t = Thread(target=task)
t.start()
t.join()
print(x) # 666
from threading import Thread, active_count, current_thread
import os
import time
def task(name):
print("%s is running" % name, os.getpid()) # 获取进程号,2660
print("%s is running" % name, current_thread().name) # Thread-1
time.sleep(1)
print("%s is over" % name)
def info(name):
print("%s is running" % name, os.getpid()) # 2660,可以发现他们同属一个进程
print("%s is running" % name, current_thread().name) # Thread-2
time.sleep(1)
print("%s is over" % name)
t = Thread(target=task, args=('lxx',))
t1 = Thread(target=info, args=('lsb',))
t.start()
t1.start()
t.join()
print(active_count()) # 当前存活的线程数:2(t结束了)
print(os.getpid()) # 2660
print(current_thread().name) # MainThread
print(current_thread().getName()) # MainThread
守护线程会等待主线程运行完毕后被销毁
运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
from threading import Thread
import time
def task(name):
print("%s is running" % name)
time.sleep(1)
print("%s is over" % name)
if __name__ == '__main__':
t = Thread(target=task, args=('lsb',))
t.daemon = True
t.start()
print('主')
# 输出:
# lsb is running
# 主
涉及到数据安全的问题,线程也需要加锁,保证数据的安全性。加锁会牺牲程序运行的效率。
from threading import Thread, Lock
import time
import random
mutex = Lock()
n = 100
def task():
global n
mutex.acquire()
tmp = n
time.sleep(0.1)
n = tmp - 1
mutex.release()
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(n)
用多线程互斥锁来模拟抢票:
info.txt文件内容: {"ticket": 5}
from threading import Thread,Lock
import time
import json
import random
lock = Lock()
def search(i):
with open('info.txt', 'r', encoding='utf-8') as f:
data = json.load(f)
print("用户[%s]查询余票数量为:%s" % (i, data.get('ticket')))
return data
def buy(i):
time.sleep(random.randint(1, 3))
lock.acquire()
data = search(i)
# time.sleep(random.randint(1, 3))
if data.get('ticket') > 0:
with open('info.txt', 'w', encoding='utf-8') as f:
data['ticket'] -= 1
json.dump(data, f)
print('用户[%s]抢票成功' % i)
else:
print('用户[%s]抢票失败' % i)
lock.release()
if __name__ == '__main__':
t_ls = []
for i in range(10):
t = Thread(target=buy,args=(i,))
t.start()
t_ls.append(t)
for t in t_ls:
t.join()
定义:
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解释器解释之后才能被cpu执行
GIL存在的原因是Cpython解释器的内存管理不是线程安全的。
如果多个线程的target=work,那么执行流程是:
多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行,解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,保证python解释器同一时间只能执行一个任务的代码
同一个进程下的多个线程不能实现并行但是可以并发,多个进程下的线程能够实现并行。
那么python解释器的多线程是不是就没有用?
四个任务:计算密集型的任务 每个任务耗时10s
单核情况下:
多线程好一点,消耗的资源少
多核情况下:
执行四个进程,10s多一点
执行四个线程,40s多
四个任务:IO密集型的任务 每个任务IO耗时10s
单核情况下:
多线程好一点
多核情况下:
多线程好一点
多线程与多进程都有自己的优点,要根据项目需求合理选择
案例:
计算密集型:
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(8):
p = Process(target=work) # 耗时26.254438400268555
# p = Thread(target=work) # 耗时46.23871946334839
p.start()
l.append(p)
for p in l:
p.join()
stop = time.time()
print("run time is %s" % (stop - start))
IO密集型:
def work():
time.sleep(2)
if __name__ == '__main__':
l = []
print(os.cpu_count()) # 本机为4核
start = time.time()
for i in range(100):
p = Process(target=work) # 耗时17.006431341171265
# p = Thread(target=work) # 耗时2.0185012817382812
p.start()
l.append(p)
for p in l:
p.join()
stop = time.time()
print("run time is %s" % (stop - start))
GIL全局解释器锁与普通锁的对比:
对于不同的数据,要想保住安全,需要加不同的锁处理
GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock
from threading import Thread, Lock
import time
mutex = Lock()
n = 100
def task():
global n
mutex.acquire()
tmp = n
time.sleep(0.1)
n = tmp - 1
mutex.release()
t_list = []
for i in range(100):
t = Thread(target=task)
t.start()
t_list.append(t)
for t in t_list:
t.join()
print(n) # 0
以上代码想得到预期的结果就需要自己加锁,解释器级别的锁并不能保证我们自己的数据的安全性。
自定义锁一次acquire必须对应一次release,不能连续acquire
递归锁可以连续的acquire,每acquire一次计数加一:针对的是第一个抢到锁的人
from threading import Thread,Lock,RLock
import time
mutexA = Lock()
mutexB = Lock()
class MyThread(Thread):
def run(self):
self.fun1()
self.fun2()
def fun1(self):
mutexA.acquire()
print("%s 抢到A锁了" % self.name)
mutexB.acquire()
print("%s 抢到B锁了" % self.name)
mutexB.release()
print("%s 释放B锁了" % self.name)
mutexA.release()
print("%s 释放A锁了" % self.name)
def fun2(self):
mutexB.acquire()
print("%s 抢到B锁了" % self.name)
time.sleep(1)
mutexA.acquire()
print("%s 抢到A锁了" % self.name)
mutexA.release()
print("%s 释放A锁了" % self.name)
mutexB.release()
print("%s 释放B锁了" % self.name)
for i in range(100):
t = MyThread()
t.start()
'''
输出:
Thread-1 抢到A锁了
Thread-1 抢到B锁了
Thread-1 释放B锁了
Thread-1 释放A锁了
Thread-1 抢到B锁了
Thread-2 抢到A锁了
原因:
Thread-1线程先抢到了A锁,其他线程就阻塞在A锁处,等待A锁的释放。
Thread-1线程执行完后续操作后释放了A锁,这时其他现场又抢到了A锁,
这时Thread-1线程执行fun2时,抢到B锁后需要抢A锁,而Thread-2线程
抢到了A锁需要抢B锁,谁也不会主动释放,这就造成了死锁的问题。
通过RLock可以解决死锁。
'''
通过递归锁RLock解决死锁
from threading import Thread,Lock,RLock
import time
mutexA = mutexB = RLock()
# 抢锁之后会有一个计数 抢一次计数加一 针对的是第一个抢到锁的人
class MyThread(Thread):
def run(self):
self.fun1()
self.fun2()
def fun1(self):
mutexA.acquire()
print("%s 抢到A锁了" % self.name)
mutexB.acquire()
print("%s 抢到B锁了" % self.name)
mutexB.release()
print("%s 释放B锁了" % self.name)
mutexA.release()
print("%s 释放A锁了" % self.name)
def fun2(self):
mutexB.acquire()
print("%s 抢到B锁了" % self.name)
time.sleep(1)
mutexA.acquire()
print("%s 抢到A锁了" % self.name)
mutexA.release()
print("%s 释放A锁了" % self.name)
mutexB.release()
print("%s 释放B锁了" % self.name)
for i in range(100):
t = MyThread()
t.start()
Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程
普通的互斥锁相当于独立卫生间,一群人抢一个厕所,即一堆线程抢一把锁。
而信号量相当于公共厕所,多个线程抢多把锁。
from threading import Thread, Semaphore
import time
import random
sm = Semaphore(5) # 5把锁
def task(name):
sm.acquire()
print("%s 正在蹲坑" % name)
# 模拟蹲坑耗时
time.sleep(random.randint(0, 5)) # 0代表刘志鹏沙雕脱了裤子刚蹲下去放了个屁就站起来了
sm.release()
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=("刘志鹏%s号" % i,))
t.start()
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法wait、clear、set。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时
就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
clear:将“Flag”设置为False
set:将“Flag”设置为True
模拟等红绿灯1
from threading import Thread,Event
import time
event = Event()
def light():
print("红灯亮着")
time.sleep(3)
event.set() # 解除阻塞,给event发信号
print("绿灯亮了")
def car(i):
print("%s 正在等红灯" % i)
event.wait() # 阻塞
print("%s 走你" % i)
t1 = Thread(target=light, )
t1.start()
for i in range(10):
t = Thread(target=car, args=(i,))
t.start()
'''
红灯亮着
0 正在等红灯
1 正在等红灯
2 正在等红灯
3 正在等红灯
4 正在等红灯
5 正在等红灯
6 正在等红灯
7 正在等红灯
8 正在等红灯
9 正在等红灯
绿灯亮了
4 走你5 走你0 走你2 走你
9 走你8 走你
6 走你
3 走你1 走你
7 走你
'''
模拟等红绿灯2
import time
import threading
event = threading.Event()
def light():
count = 0
event.set() # 设置标志位,相当于先设置绿灯
while True:
if count > 5 and count < 10: # 改红灯
event.clear() # 清空标志位
print("\033[41m 红灯亮了\033[0m")
elif count > 10:
event.set()
count = 0
else:
print("\033[42m 绿灯亮了\033[0m")
time.sleep(1)
count += 1
def car(name):
while True:
if event.is_set():
print("%s 走你" % name)
time.sleep(1)
else:
print("%s 正在等绿灯" % name)
event.wait()
print("%s 绿灯亮了,走你" % name)
l = threading.Thread(target=light)
l.start()
c1 = threading.Thread(target=car, args=("BWM", ))
c2 = threading.Thread(target=car, args=("AODI", ))
c1.start()
c2.start()
定时器,指定n秒后执行某操作
from threading import Timer
def hello():
print("hello, world")
t = Timer(1, hello)
t.start() # after 1 seconds, "hello, world" will be printed
queue队列 :使用import queue,用法与进程Queue一样
import queue
# 先进先出
q = queue.Queue(3)
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
# 后进先出
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get())
print(q.get())
print(q.get())
# 可设置优先级
q = queue.PriorityQueue()
q.put((10,'a'))
q.put((-1,'b'))
q.put((100,'c'))
print(q.get())
print(q.get())
print(q.get())