一、同步概念
同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。
"同"字从字面上容易理解为一起动作。
其实不是,在这里,"同"字应是指协同、协助、互相配合。
线程同步,可理解为线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。
之前我们遇到过,如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。
解决线程同时修改全局变量的方式
我们先把上次那个问题再看下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import threading
import time
g_num = 0
def work1(num):
global g_num
for i in range (num):
g_num + = 1
print ( "----in work1, g_num is %d---" % g_num)
def work2(num):
global g_num
for i in range (num):
g_num + = 1
print ( "----in work2, g_num is %d---" % g_num)
print ( "---线程创建之前g_num is %d---" % g_num)
t1 = threading.Thread(target = work1, args = ( 1000000 ,))
t1.start()
t2 = threading.Thread(target = work2, args = ( 1000000 ,))
t2.start()
# 确保子线程都运行结束
while len (threading. enumerate ()) ! = 1 :
time.sleep( 1 )
print ( "2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
|
运行结果:
---线程创建之前g_num is 0---
----in work2, g_num is 1048576---
----in work1, g_num is 1155200---
2个线程对同一个全局变量操作之后的最终结果是:1155200
对于这个计算错误的问题,可以通过线程同步来进行解决。
思路,如下:
系统调用 t1,然后获取到 g_num 的值为0,此时上一把锁,即不允许其他线程操作 g_num。
t1 对 g_num 的值进行+1。
t1 解锁,此时 g_num 的值为1,其他的线程就可以使用 g_num 了,而且 g_num 的值不是0而是1。
同理其他线程在对 g_num 进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性。
思路基本是这个样子,那代码怎么来实现呢?
二、互斥锁解决资源竞争的问题
当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制。
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制就是引入互斥锁。
互斥锁为资源引入一个状态:锁定/非锁定。
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。
互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。
threading 模块中定义了 Lock 类,可以方便的处理锁定:
1
2
3
4
5
6
7
8
|
# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放
mutex.release()
|
注意:
如果这个锁之前是没有上锁的,那么 acquire 不会堵塞。
如果在调用 acquire 对这个锁上锁之前,它已经被其他线程上了锁,那么此时 acquire 会堵塞,直到这个锁被解锁为止。
示例:
使用互斥锁完成2个线程对同一个全局变量各加100万次的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import threading
import time
g_num = 0
def test1(num):
global g_num
for i in range (num):
mutex.acquire() # 上锁
g_num + = 1
mutex.release() # 解锁
print ( "---test1---g_num=%d" % g_num)
def test2(num):
global g_num
for i in range (num):
mutex.acquire() # 上锁
g_num + = 1
mutex.release() # 解锁
print ( "---test2---g_num=%d" % g_num)
# 创建一个互斥锁
# 默认是未上锁的状态
mutex = threading.Lock()
# 创建2个线程,让他们各自对g_num加1000000次
p1 = threading.Thread(target = test1, args = ( 1000000 ,))
p1.start()
p2 = threading.Thread(target = test2, args = ( 1000000 ,))
p2.start()
# 等待计算完成
while len (threading. enumerate ()) ! = 1 :
time.sleep( 1 )
print ( "2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)
|
运行结果:
---test1---g_num=1989108
---test2---g_num=2000000
2个线程对同一个全局变量操作之后的最终结果是:2000000
可以看到最后的结果,加入互斥锁后,其结果与预期相符。
记住,上锁的代码范围要越小越好。在业务逻辑正确的前提下,能锁一行代码,就不要锁两行。
上锁解锁过程
当一个线程调用锁的 acquire() 方法获得锁时,锁就进入“locked”状态。
每次只有一个线程可以获得锁。
如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的 release() 方法释放锁之后,锁进入“unlocked”状态。
线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
总结
锁的好处:
确保了某段关键代码只能由一个线程从头到尾完整地执行。
锁的坏处:
阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。
由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁。
到此这篇关于互斥锁解决 Python 中多线程共享全局变量的问题的文章就介绍到这了,更多相关Python 多线程共享全局变量内容请搜索服务器之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持服务器之家!
原文链接:https://www.cnblogs.com/studyming/archive/2020/09/28/13742958.html