基础
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。
程序运行时创建主进程,主进程可以再创建子进程。
线程的执行有CPU的调度算法决定,执行的顺序是无序的!
线程调度顺序无法控制,但线程调度方式可以影响!
多线程原理
在多核计算机中,计算机可以不同的处理器处理不同的线程;在处理器数小于总线程数时,处理器则会在不同的线程中切换执行,使得多个线程可以几乎同时运行。
多线程可以同步完成多项任务,虽然不能提高运行效率,但是可以提高资源使用效率来提高系统的效率。 然而,更多的线程会占用更多的内存,线程公用的块模型也可能造成死锁。
并行和并发【多任务底层实现原理】
并发,指的是多个事情,在同一时间段内同时发生了。
并行,指的是多个事情,在同一时间点上同时发生了。
并发的多个任务之间是互相抢占资源的【抢用统一CPU】。并行的多个任务之间是不互相抢占资源的、只有在多CPU且任务数小于等于CPU数的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
正常情况下,大部分任务都是并发的。
Python程序默认单进程
import time
def sing():
for i in range(5):
print("singing...")
time.sleep(0.5)
def dance():
for i in range(5):
print("dancing...")
time.sleep(0.5)
if __name__ == "__main__":
sing()
dance()
程序会先输出”singing…“五次,再输出”dancing…“五次,用时5秒
$ python3 多线程基础.py
singing...
singing...
singing...
singing...
singing...
dancing...
dancing...
dancing...
dancing...
dancing...
使用线程(利用threading模块)
使用threading.Thread()
创建子线程对象
Thread()最重要的参数是target,用于指定子线程执行的分支,target=要执行的函数名【不用带括号!!】
使用.start()启动子线程
import time
import threading
def sing():
for i in range(5):
print("singing...")
time.sleep(0.5)
def dance():
for i in range(5):
print("dancing...")
time.sleep(0.5)
if __name__ == "__main__":
# 使用threading.Thread(target=函数名)创建子线程对象并指定子线程执行的分支
thread_sing = threading.Thread(target=sing)
thread_dance = threading.Thread(target=dance)
# 使用.start()启动子线程
thread_sing.start()
thread_dance.start()
程序会同时执行sing()和dance()两个函数,用时2.5秒
$ python3 多线程基础.py
singing...
dancing...
singing...
dancing...
singing...
dancing...
singing...
dancing...
singing...
dancing...
注意!正常情况下:
主线程会等所有子线程结束后才结束 子线程还在执行时,主进程exit()无效!
一个线程出错,其他线程还会继续执行
# 导入模块
import time
import threading
def sing(num):
try:
for i in range(int(num)):
print("singing...")
time.sleep(0.5)
except:
print("参数错误!")
def dance():
for i in range(5):
print("dancing...")
time.sleep(0.5)
if __name__ == "__main__":
# 使用threading.Thread(target=函数名)创建子线程对象并指定子线程执行的分支
thread_sing = threading.Thread(target=sing)
thread_dance = threading.Thread(target=dance)
# 使用.start()启动子线程
thread_sing.start()
thread_dance.start()
sing()函数没传参,会出错
$ python3 多线程基础.py Exception in thread Thread-1: Traceback (most recent call last): File "/usr/lib/python3.8/threading.py", line 932, in _bootstrap_inner dancing... self.run() File "/usr/lib/python3.8/threading.py", line 870, in run self._target(*self._args, **self._kwargs) TypeError: sing() missing 1 required positional argument: 'num' dancing... dancing... dancing... dancing...
sing()因为未传参而报错,dance()继续执行。
查看线程
查看当前活跃线程
import threading
# 查看所有线程
list = threading.enumerate()
print(list)
返回一个列表,列表中的每一个元素都是线程对象。
使用len()查看线程数量
[<_MainThread(MainThread, started 140483956983616)>, <Thread(Thread-1, started 140483939927808)>, <Thread(Thread-2, started 140483931535104)>]
_MainThread是主线程,
查看线程对象
threading.current_thread()
返回当前线程对象
在主线程中使用就返回_MainThread
多线程间共享全局变量
import threading
import time
# 创建全局变量
num = 0
def work1():
# 声明全局变量
global num
for i in range(11):
print(num)
time.sleep(0.5)
def work2():
global num
# 改变全局变量
for i in range(5):
num +=1
time.sleep(1)
if __name__=="__main__":
# 创建线程
work1thread = threading.Thread(target=work1)
work2thread = threading.Thread(target=work2)
# 启动线程
work1thread.start()
work2thread.start()
# 字线程都结束时再检查num
while len(threading.enumerate()) != 1:
time.sleep(1)
print("num的最终值:")
print(num)
输出:
$ python3 全局变量.py
0
1
2
2
3
3
4
4
5
5
5
num的最终值:
5
多线程中的参数传递
1.创建线程时使用元组传递
threading.Thread(target=函数名,args=(参数1,参数2... ...))
注意:使用元组传参,参数顺序要一致!
2.创建线程时使用字典传递
threading.Thread(target=函数名,kwargs={形参1=实参1,形参2=实参2... ...})
3.创建线程时混合使用元组和字典传递
threading.Thread(target=函数名,args=(参数1,参数2... ...),kwargs={形参1=实参1,形参2=实参2... ...})
注意:使用字典传参,形参名一定要一致!
守护线程
所谓守护线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程。
但是守护线程终止不影响非守护线程继续运行。
使用.setDaemon(True)
设置守护线程
import time
import threading
def sing(num):
try:
for i in range(num):
print("singing...")
time.sleep(0.5)
except:
print("参数错误!")
def dance():
for i in range(5):
print("dancing...")
time.sleep(0.5)
if __name__ == "__main__":
# 使用threading.Thread(target=函数名)创建子线程对象并指定子线程执行的分支
thread_sing = threading.Thread(target=sing,kwargs={"num":5})
thread_dance = threading.Thread(target=dance)
# 设置守护线程
thread_dance.setDaemon(True)
thread_sing.setDaemon(True)
# 使用.start()启动子线程
thread_sing.start()
thread_dance.start()
# 使用exit()结束主进程
print("程序结束")
exit()
输出:
$ python3 多线程基础.py
singing...
dancing...
程序结束
主进程结束时子进程也一起结束。
注意:只要一个进程有至少一个非守护子进程,这个进程就不会终结!
自定义线程类【三步】
自定义类继承threadig.Thread
类,重写run()
方法,使用start()
启动。
应用:多线程下载,多线程爬虫… …
import threading
import time
# 继承threading.Thread
class My_thread(threading.Thread):
def __init__(self,num):
# 先调用父类的__init__()方法
super().__init__()
self.num = num
# 重写run()方法
def run(self):
for i in range(self.num):
time.sleep(1)
print("正在执行子进程的run()",i)
if __name__=="__main__":
mythread = My_thread(5)
mythread.start()
for i in range(5):
time.sleep(1)
print("正在执行r主进程",i)
输出结果:
$ python3 自定义线程类.py
正在执行子进程的run() 0
正在执行r主进程 0
正在执行子进程的run() 1
正在执行r主进程 1
正在执行r主进程 2
正在执行子进程的run() 2
正在执行r主进程 3
正在执行子进程的run() 3
正在执行子进程的run() 4
正在执行r主进程 4
子类从父类继承了start()
方法。
注意:重写__init__()
方法时,一定要先引用父类的__init__()
方法!
部分原码
def start(self):
"""Start the thread's activity.
It must be called at most once per thread object. It arranges for the
object's run() method to be invoked in a separate thread of control.
This method will raise a RuntimeError if called more than once on the
same thread object.
"""
if not self._initialized:
raise RuntimeError("thread.__init__() not called")
if self._started.is_set():
raise RuntimeError("threads can only be started once")
with _active_limbo_lock:
_limbo[self] = self
try:
_start_new_thread(self._bootstrap, ())
except Exception:
with _active_limbo_lock:
del _limbo[self]
raise
self._started.wait()
我们发现,多次start()
同一线程会报错,start()
方法会执行run()
方法。
多线程中存在的问题
-
资源竞争,当同一个变量同时被多个线程修改时不一定会得到想要的值。
import threading import time # 创建全局变量 num = 0 def work1(): # 声明全局变量 global num for i in range(1000000): num+=1 def work2(): global num # 改变全局变量 for i in range(1000000): num +=1 if __name__=="__main__": # 创建线程 work1thread = threading.Thread(target=work1) work2thread = threading.Thread(target=work2) # 启动线程 work1thread.start() work2thread.start() # 子线程都结束时再检查num while len(threading.enumerate()) != 1: time.sleep(1) print("num的最终值:") print(num)
我们想把
i += 1
执行2000000次,输出:$ python3 多线程的问题.py num的最终值: 1311538
这并不是我们想要的值!
我们每次运行得到的值还不一样!
解决办法
对某一正在运行的线程使用join()
方法,使其优先运行【实际上又变回了单进程,同一时间只有一个线程在运行】。
import threading
import time
# 创建全局变量
num = 0
def work1():
# 声明全局变量
global num
for i in range(1000000):
num+=1
def work2():
global num
# 改变全局变量
for i in range(1000000):
num +=1
if __name__=="__main__":
# 创建线程
work1thread = threading.Thread(target=work1)
work2thread = threading.Thread(target=work2)
# 启动线程
work1thread.start()
# 优先执行work1thread
work1thread.join()
work2thread.start()
# 字线程都结束时再检查num
while len(threading.enumerate()) != 1:
time.sleep(1)
print("num的最终值:")
print(num)
这下输出结果就对了。
同步与异步
同步是执行或调用一个方法时,每次都需要拿到对应的结果才会继续往后执行;异步与同步相反,它会在执行或调用一个方法后就继续往后执行,不会等待获取执行结果。
二者的区别就是处理请求发出后,是否需要等待请求结果,再去继续执行其他操作。
互斥锁
使用互斥锁来使某一全局变量在同一时间只能被单一线程访问和修改。
访问已被上锁的全局变量的线程会阻塞,直到该变量解锁
创建的互斥锁继承threading.Lock()
类,使用.acquire()
来给资源上锁,使用.release()
来解锁
import threading
import time
# 创建全局变量
num = 0
def work1():
# 声明全局变量
global num
for i in range(1000000):
# 上锁
lock.acquire()
num+=1
# 解锁
lock.release()
def work2():
global num
# 改变全局变量
for i in range(1000000):
# 上锁
lock.acquire()
num +=1
# 解锁
lock.release()
if __name__=="__main__":
# 创建互斥锁
lock = threading.Lock()
# 创建线程
work1thread = threading.Thread(target=work1)
work2thread = threading.Thread(target=work2)
# 启动线程
work1thread.start()
work2thread.start()
# 字线程都结束时再检查num
while len(threading.enumerate()) != 1:
time.sleep(1)
print("num的最终值:")
print(num)
使用同一全局变量的线程必须全部使用同一个锁对象上锁才能达到互斥锁的效果! 不必要的资源就不要锁!尽可能去少地锁资源,锁太多会导致阻塞太多,使程序变慢!
死锁
当多个线程分别占用一定资源,并分别请求对方占用地资源,则会一直阻塞,形成死锁。
示例
import threading
# 定义函数 根据下标获取列表元素值
def get_value(index):
# 0 1 2 3 4
data_list = [1,3,5,7,9]
# 上锁
lock1.acquire()
print(index,end=" ")
print(data_list[index])
# 解锁
lock1.release()
# 创建10个线程,观察资源的等待状态
if __name__ == '__main__':
# 创建一把锁
lock1 = threading.Lock()
# 循环创建10个线程
for i in range(10):
t1 = threading.Thread(target=get_value, args=(i,))
t1.start()
第5个子线程请求data_list[5]
出错后,未释放锁,其他线程等待访问data_list
形成死锁。