05-7 万字长文:实现多线程(下)

05-7 万字长文:实现多线程(下)

5.3 守护线程

你好,我是悦创。在线程中有一个叫作守护线程的概念,如果一个线程被设置为守护线程,那么意味着这个线程是“不重要”的,这意味着,如果主线程结束了而该守护线程还没有运行完,那么它将会被强制结束。在 Python 中我们可以通过 setDaemon 方法来将某个线程设置为守护线程。

如果要修改成守护线程,那你就得在 thread.start() 前面加一个:

需要在我们启动之前设置。

「示例一如下:」

添加之前:

import threading, time

def start(num):
time.sleep(num)
print(threading.current_thread().name) # 当前线程的名字
print(threading.current_thread().isAlive())
print(threading.current_thread().ident)

print('start') # 主线程开始
thread = threading.Thread(target=start,name='my first thread', args=(1,))# 可以使用 for 循环来添加多个
thread.start()
print('stop') # 主线程结束

# 运行结果
start
stop
my first thread
True
15816

添加之后:

import threading, time

def start(num):
time.sleep(num)
print(threading.current_thread().name) # 当前线程的名字
print(threading.current_thread().isAlive())
print(threading.current_thread().ident)

print('start') # 主线程开始
thread = threading.Thread(target=start,name='my first thread', args=(1,))# 可以使用 for 循环来添加多个
thread.setDaemon(True) # 在 start 开始之前设置
thread.start()
print('stop') # 主线程结束

# 运行结果
start
stop

我们可以看见,程序直接运行:start、stop,执行到 **print('stop') 它就结束了。**也就随着我们的主线程结束而结束。并不管它里面还有什么没有执行完。(也不会管他里面的 time.sleep())我们的主线程一结束,我们的守护线程就会随着主线程一起销毁。

「我们日常启动的是非守护线程,守护线程用的较少。」

守护线程会伴随主线程一起结束,setDaemon 设置为 True 即可。

「示例二如下:」

添加之前:

import threading, time

def target(second):
print(f'Threading {threading.current_thread().name} is runing')
print(f'Threading {threading.current_thread().name} sleep {second}s')
time.sleep(second)
print(f'Threading {threading.current_thread().name} is ended')

print(f'Threading {threading.current_thread().name} is runing')
t1 = threading.Thread(target=target, args=[2])
t1.start()
t2 = threading.Thread(target=target, args=[5])
t2.start()
print(f'Threading {threading.current_thread().name} is ended')

# 运行结果
Threading MainThread is runing
Threading Thread-1 is runing
Threading Thread-1 sleep 2s
Threading Thread-2 is runing
Threading Thread-2 sleep 5s
Threading MainThread is ended
Threading Thread-1 is ended
Threading Thread-2 is ended

添加之后:

import threading, time

def target(second):
print(f'Threading {threading.current_thread().name} is runing')
print(f'Threading {threading.current_thread().name} sleep {second}s')
time.sleep(second)
print(f'Threading {threading.current_thread().name} is ended')

print(f'Threading {threading.current_thread().name} is runing')
t1 = threading.Thread(target=target, args=[2])
t1.start()
t2 = threading.Thread(target=target, args=[5])
t2.setDaemon(True)
t2.start()
print(f'Threading {threading.current_thread().name} is ended')

# 运行结果
Threading MainThread is runing
Threading Thread-1 is runing
Threading Thread-1 sleep 2s
Threading Thread-2 is runing
Threading Thread-2 sleep 5s
Threading MainThread is ended
Threading Thread-1 is ended

在这里我们通过 setDaemon 方法将 t2 设置为了守护线程,这样主线程在运行完毕时,t2 线程会随着线程的结束而结束。

运行结果:

Threading MainThread is runing
Threading Thread-1 is runing
Threading Thread-1 sleep 2s
Threading Thread-2 is runing
Threading Thread-2 sleep 5s
Threading MainThread is ended
Threading Thread-1 is ended

可以看到,我们没有看到 Thread-2 打印退出的消息,Thread-2 随着主线程的退出而退出了。

不过细心的你可能会发现,这里并没有调用 join 方法,如果我们让 t1 和 t2 都调用 join 方法,主线程就会仍然等待各个子线程执行完毕再退出,不论其是否是守护线程。

5.4 互斥锁

接下来是比较难的知识点,还是从简单的知识点开始。

「比方」说我们现在有两个线程,一个是求加一千万次,另一个是减一千万次。按原本得计划来说,一个加一千万一个减一千万结果应该还是零。可是最终得结果并不是等于零,我们多运行几次会发现几次得出来得结果并不相同。多线程代码如下:

import threading
import time

number = 0

def addNumber(i):
time.sleep(i)
global number
for i in range(1000000):
number += 1
print("加",number)

def downNumber(i):
time.sleep(i)
global number
for i in range(1000000):
number -= 1
print("减",number)

print("start") # 输出一个开始
thread = threading.Thread(target = addNumber, args=(2,)) #开启一个线程(声明)
thread2 = threading.Thread(target = downNumber, args=(2,)) # 开启第二个线程(声明)
thread.start() # 开始
thread2.start() # 开始
thread.join()
thread2.join()
# join 阻塞在这里,直到我们得阻塞线程执行完毕才会向下执行
print("外", number)
print("stop")

就算单线程也会出现两个值:1000000 与 -1000000,两个函数谁先运行就是输出谁的结果,为什么呢?因为两个函数调用的是全局变量 「number」 所以,如果先运行加法函数,加法得到的结果是 1000000 ,那全局下的 number 的值也会变成:1000000 ,那减法的操作亦然就是 0。反过来也是一个意思。代码如下:

import threading
import time

number = 0

def addNumber(i = None):
# time.sleep(i)
global number
for i in range(1000000):
number += 1
print("加",number)

def downNumber(i = None):
# time.sleep(i)
global number
for i in range(1000000):
number -= 1
print("减",number)

addNumber()
downNumber()
print(number)

# 运行结果
加 1000000
减 0
0

# 反过来运行
downNumber()
addNumber()
print(number)

# 运行结果
减 -1000000
加 0
0

# 再来一个差不多的例子:
import threading
import time

number = 0

def addNumber():
global number
for i in range(1000000):
number += 1
print("加",number)
return number

def downNumber():
global number
for i in range(1000000):
number -= 1
print("减",number)
return number

sum_num = downNumber() + addNumber()
print("Result", sum_num)

# 输出
减 -1000000
加 0
Result -1000000


# 修改以下代码,其他不变:
sum_num = addNumber() + downNumber()

# 输出
加 1000000
减 0
Result 1000000

由上面的多线程代码,我可以发现结果:两个线程操作同一个数字,最后得到的数字是混乱的。为什么说是混乱的呢?

我们现在所要做的是一个赋值,number += 1 其实也就是 number = number + 1,的这个操作。而在我们的 Python 当中,我们是先:计算右边的,然后赋值给左边的,一共两步。

我先来看一下正确的运行流程:

# 我们的 number = 0
# 第一步是先运行我们的代码:
a = number + 1 # 等价于 0+1=1
# 也就是先运行右边的,然后赋值给 a

number = a # 然后,再把 a 的结果赋值个 number

# 上面运行完加法之后,我们加下来运行减肥的操作。
b = number - 1 # 等价于 1-1 = 0
# 然后,赋值个 number

# 最后 number 等于 0
number = 0

上面的过成是正确的流程,可在多线程里面呢?

number = 0 # 开始初始值 0
a = number+1 # 等价于 0+1=1
# 这个地方要注意!!!
# 在运行完上面一步的时候,还没来得急把结果赋值给 number
# 就开始运行减法操作:
b = number-1 # 等价于 0-1=-1
# 然后,这两个运行结束之后就被赋值:
number=b # b = -1
number=a # a = 1

# 最终得结果为:
number = 1

上面就是我们刚才结果错乱得原因,也就是说:我们计算和赋值是两部分,但是该多线程它没有顺序执行,这也就是我们所说的线程不安全。

因为,执行太快了,两个线程交互交织在一起,最终得到我们这个错误结果。以上就是线程不安全的问题。

这就是需要 「Lock 锁」,给它上一把锁,来达到我们 「number」 的效果,这个时候为了避免错误,我们要给他上一把锁了。再给你讲解上锁之前呢,「接下来,我们来讲一点复杂的例子:」

在一个进程中的「多个线程是共享资源的」

「比如」

在一个进程中,有一个全局变量 count 用来计数,现在我们声明多个线程,每个线程运行时都给 count 加 1,让我们来看看效果如何,代码实现如下:

import threading, time

count = 0

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global count
temp = count + 1
time.sleep(0.001)
count = temp
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
# print(len(threads))
print(f'Final count: {count}')

在这里,我们声明了 1000 个线程,每个线程都是现取到当前的全局变量 count 值,然后休眠一小段时间,然后对 count 赋予新的值。

那这样,按照常理来说,最终的 count 值应该为 1000。但其实不然,我们来运行一下看看。

运行结果如下:

Final count: 69

最后的结果居然只有 69,而且多次运行或者换个环境运行结果是不同的。

「这是为什么呢?」

因为 count 这个值是共享的,每个线程都可以在执行 temp = count 这行代码时拿到当前 count 的值,但是这些线程中的一些线程可能是并发或者并行执行的,这就导致不同的线程拿到的可能是同一个 count 值,最后导致有些线程的 count 的加 1 操作并没有生效,导致最后的结果偏小。

所以,如果多个线程同时对某个数据进行读取或修改,就会出现不可预料的结果。为了避免这种情况,我们需要对多个线程进行同步,要实现同步,我们可以对需要操作的数据进行加锁保护,这里就需要用到 threading.Lock 了。

「加锁保护是什么意思呢?」

就是说,某个线程在对数据进行操作前,需要先加锁,这样其他的线程发现被加锁了之后,就无法继续向下执行,会一直等待锁被释放,只有加锁的线程把锁释放了,其他的线程才能继续加锁并对数据做修改,修改完了再释放锁。这样可以确保同一时间只有一个线程操作数据,多个线程不会再同时读取和修改同一个数据,这样最后的运行结果就是对的了。

我们可以将代码修改为如下内容:

示例一的修改:

import threading
import time

lock = threading.Lock() # 创建一个最简单的 读写锁
number = 0

def addNumber():
global number
for i in range(1000000):
lock.acquire() # 先获取
number += 1
# 中间的这个过程让他强制有这个计算和赋值的过程,也就是让他执行完这两个操作,后再切换。
# 这样就不会完成计算后,还没来的及赋值就跑到下一个去了。
# 这样也就防止了线程不安全的情况
lock.release() # 再释放

def downNumber():
global number
for i in range(1000000):
lock.acquire()
number -= 1
lock.release()

print("start") # 输出一个开始
thread = threading.Thread(target = addNumber) #开启一个线程(声明)
thread2 = threading.Thread(target = downNumber) # 开启第二个线程(声明)
thread.start() # 开始
thread2.start() # 开始
thread.join()
thread2.join()
# join 阻塞在这里,直到我们得阻塞线程执行完毕才会向下执行
print("外", number)
print("stop")

# 输出
start
外 0
stop

在代码:「lock.acquire() 与 lock.release()」 中间的这个过程让它强制有这个计算和赋值的过程,也就是让他执行完这两个操作,后再切换。这样就不会完成计算后,还没来的及赋值就跑到下一个去了。这样也就防止了线程不安全的情况。

然后,就是我们第一个线程拿到这把锁的 「lock.acquire()」 了,那另一个线程就会在 「lock.acquire()」 阻塞了,直到我们另一个线程把 「lock.release()」 锁释放,然后拿到锁执行,就这样不断地切换拿锁执行。

**死锁:**就是前面的线程拿到锁之后,运行完却不释放锁,下一个线程在等待前一个线程释放锁,这种就是死锁。说的直白一点就是,相互等待。就像照镜子一样,你中有我,我中有你。也就是在没有 release 的这种情况。(你等我表白,我等你表白)

「示例二的加锁」

# -*- coding: utf-8 -*-
# @Author: clela
# @Date: 2020-04-08 22:08:33
# @Last Modified by: clela
# @Last Modified time: 2020-04-09 10:31:59
import threading, time

count = 0

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global count
lock.acquire() # 获取锁
temp = count + 1
time.sleep(0.001)
count = temp
lock.release() # 释放锁

lock = threading.Lock()
threads = []
for _ in range(1000):
thread = MyThread()
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
print(f'Final count: {count}')

在这里我们声明了一个 lock 对象,其实就是 threading.Lock 的一个实例,然后在 run 方法里面,获取 count 前先加锁,修改完 count 之后再释放锁,这样多个线程就不会同时获取和修改 count 的值了。

运行结果如下:

Final count: 1000

这样运行结果就正常了。

关于 Python 多线程的内容,这里暂且先介绍这些,关于 theading 更多的使用方法,如信号量、队列等,可以参考官方文档:https://docs.python.org/zh-cn/3.7/library/threading.html#module-threading。

5.5 递归锁 RLOCK

再次复用,一个锁可以再嵌套一个锁。向我们上面的普通锁,一个线程里面,你只能获取一次。如果获取第二次就会报错。

递归锁什么时候用呢?需要更低精度的,力度更小,为了更小的力度。

import threading
import time

class Test:
rlock = threading.RLock()
def __init__(self):
self.number = 0

def execute(self, n):
# 原本是获取锁和释放锁,那如果有时候你忘记了写 lock.release() 那就变成了死锁。
# 而 with 可以解决这个问题。
with Test.rlock:
# with 内部有个资源释放的机制
self.number += n

def add(self):
with Test.rlock:
self.execute(1)

def down(self):
with Test.rlock:
self.execute(-1)

def add(test):
for i in range(1000000):
test.add()

def down(test):
for i in range(1000000):
test.down()

if __name__ == '__main__':
thread = Test() # 实例化
t1 = threading.Thread(target=add, args=(thread,))
t2 = threading.Thread(target=down, args=(thread,))
t1.start()
t2.start()
t1.join()
t2.join()
print(t.number)

我们会发现这个递归锁是比较耗费时间的,也就死我们获取锁与释放锁都是进行上下文切换导致资源消耗的,所以说开启的锁越多,所耗费的资源也就越多,程序的运行速度也就越慢。一些大的工程很少上这么多的锁,因为这个锁的速度会拖慢你整个程序的运行速度。所以得思考好,用不用这些东西。

 

 

 

05-7 万字长文:实现多线程(下)05-7 万字长文:实现多线程(下)05-7 万字长文:实现多线程(下)

 

上一篇:并发编程 五 - 死锁


下一篇:多线程编程