专栏首页python3【Python之旅】第六篇(四)

【Python之旅】第六篇(四)

在多线程程序执行过程中,为什么需要给一些线程加锁以及如何加锁,下面就来说一说。

1.给线程加锁的原因

    我们知道,不同进程之间的内存空间数据是不能够共享的,试想一下,如果可以随意共享,谈何安全?但是一个进程中的多个线程是可以共享这个进程的内存空间中的数据的,比如多个线程可以同时调用某一内存空间中的某些数据(只是调用,没有做修改)。

    试想一下,在某一进程中,内存空间中存有一个变量对象的值为num=8,假如某一时刻有多个线程需要同时使用这个对象,出于这些线程要实现不同功能的需要,线程A需要将num减1后再使用,线程B需要将num加1后再使用,而线程C则是需要使用num原来的值8。由于这三个线程都是共享存储num值的内存空间的,并且这三个线程是可以同时并发执行的,当三个线程同时对num操作时,因为num只有一个,所以肯定会存在不同的操作顺序,想象一下下面这样操作过程:

第一步:线程A修改了num的值为7
第二步:线程C不知道num的值已经发生了改变,直接调用了num的值7
第三步:线程B对num值加1,此时num值变为8
第四步:线程B使用了num值8
第五步:线程A使用了num值8

    因为num只有一个,而三个操作都针对一个num进行,所以上面的操作过程是完全有可能的,而原来线程A、B、C想要使用的num值应该分别为:7、9、8,这里却变成了:8、8、7。试想一下,如果这三个线程的操作对整个程序的执行是至关重要的,会造成什么样的后果?

    因此出于程序稳定运行的考虑,对于线程需要调用内存中的共享数据时,我们就需要为线程加锁。

2.Python多线程锁

(1)

    先看下面一个未给线程加锁的程序代码:

import threading
import time

number = 0

def run(num):
	global number
	number += 1
	print number
	time.sleep(1)
	
for i in range(20):
	t = threading.Thread(target=run, args=(i,))
	t.start()

    程序执行结果如下:

xpleaf@xpleaf-machine:/mnt/hgfs/Python/day6$ python thread_clock6.py 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

    上面是多个线程同时抢占同一内存空间的例子,但从执行结果中可以看到,程序依然顺序地输出1-19,而没有出现上面说的情况,那是仅仅是因为量少的原因,虽然执行正常,没有出错,但是并不代表不会出错。

(2)

    看下面给线程加锁的代码:

import threading
import time

number = 0

lock = threading.RLock()    #调用threading模块中的RLock()

def run(num):
	lock.acquire()      #开始给线程加锁
	global number
	number += 1
	lock.release()      #给线程解锁
	print number
	time.sleep(1)

for i in range(20):
	t = threading.Thread(target=run, args=(i,))
	t.start()

    程序执行结果如下:

xpleaf@xpleaf-machine:/mnt/hgfs/Python/day6$ python thread_clock6.py 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

    程序的执行结果肯定是会正常的,而在没有给线程加锁之前,则有可能是正常,注意这是两种完全不同的概念。

    分析一下上面的程序:在某一线程修改num的值时,即给该线程加锁,该线程加锁后,只要是该线程需要调用的代码以及涉及的内存空间,都会立即被锁上,比如这里的"number+=1",其它线程虽然也在并发同时执行,但是不能执行"number+=1"这行代码的,即不能够去访问或修改num这一个共享内存空间的数据,只能等待该线程解锁后才能执行;当该线程解锁后,另一个线程马上加锁再来修改number的值,同时也不允许其它线程占用,如此类推,直到所有线程执行完毕。

    根据上面的分析,为线程加锁就可以解决前面讲的线程安全问题。

(3)

    为了更好的理解线程加锁的一个过程,把上面的代码修改为如下:

import threading
import time

number = 0

lock = threading.RLock()

def run(num):
	lock.acquire()
	global number
	number += 1
	print number
	time.sleep(1)    #把time.sleep(1)也锁在线程中
	lock.release()
	
for i in range(20):
	t = threading.Thread(target=run, args=(i,))
	t.start()

    执行结果如下:

xpleaf@xpleaf-machine:/mnt/hgfs/Python/day6$ python thread_clock6.py 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

    程序的执行结果跟上面是完全一样,但是程序的执行过程却大不一样,这里说一下修改代码后程序的执行过程:每输出一个数字,sleep 1秒后再输出下一个数字,如此类推。

    为了更好的说明,我们可以看一下执行完此程序所花的时间:

xpleaf@xpleaf-machine:/mnt/hgfs/Python/day6$ time python thread_clock6.py | grep 'real'

real	0m20.073s
user	0m0.024s
sys	0m0.008s

    由执行时间可以更好的说明上面的执行过程,但为什么会这样呢?下面来分析一下:由(2)的分析可知,虽然20个线程都是在同时并发执行run这一个函数,这里与(2)不同在于,(2)只加锁了涉及修改number的程序代码,而这里是加锁了整一个函数!所以在20个线程同时开始并发执行这个函数时,由于每一个线程的执行都要加锁,并且加锁的是整一个执行的函数,因此其它线程就无法调用该函数中的程序代码,只能等待一个线程执行完毕后再调用该函数的程序代码,如此一来,一个线程的执行需要sleep(1)一次,则20个线程的执行就需要sleep(1)20次,并且该过程是串行的,因此我们才看到如上面所说的程序执行过程,也可以清晰的知道为什么程序的执行需要20s了。

    由上面的分析,我们不仅可以知道为什么要给线程加锁以及如何加锁,还可以比较清楚的知道线程加锁的一个过程了,以后在编写程序的时候,类似情况的,我们就应该要为线程加锁。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • python3--threading模块(线程)

    程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态...

    py3study
  • python线程入门

    正常情况下,我们在启动一个程序的时候。这个程序会先启动一个进程,启动之后这个进程会启动起来一个线程。这个线程再去处理事务。也就是说真正干活的是线程,进程这玩意只...

    py3study
  • python 线程(一)理论部分

    进程有很多优点,它提供了多道编程,可以提高计算机CPU的利用率。既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的。

    py3study
  • Java并发编程实战系列11之性能与可伸缩性Performance and Scalability

    线程可以充分发挥系统的处理能力,提高资源利用率。同时现有的线程可以提升系统响应性。 但是在安全性与极限性能上,我们首先需要保证的是安全性。 11.1 对性能的...

    JavaEdge
  • 打通 Java 任督二脉 —— 并发数据结构的基石

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantLoc...

    老钱
  • 【转】Java并发的AQS原理详解

    每一个 Java 的高级程序员在体验过多线程程序开发之后,都需要问自己一个问题,Java 内置的锁是如何实现的?最常用的最简单的锁要数 ReentrantL...

    一枝花算不算浪漫
  • 为什么局部变量是线程安全的?

    当多个线程访问add方法的时候 并操作add方法下的变量,永远都不会导致数据竞争,为什么呢? look at the next line↓:

    奕仁
  • 【原创】Java并发编程系列15 | 重入锁ReentrantLock

    AQS是java.util.concurrent包的核心基础组件,是实现Lock的基础。那么AQS是如何实现Lock的呢?

    java进阶架构师
  • 005.多线程-线程的生命周期

    新建状态: 当创建Thread类的一个实例(对象)时,此线程进入新建状态(未被启动)。 如:ThreadTest thread2 = new ThreadT...

    qubianzhong
  • IOCP异步优化

    2. IO操作: CPU会把内存中的程序委托给其他的网络、磁盘等驱动程序,让这些外部的驱动程序来进行具体的处理,处理完成以后再返回给内存程序。对于这两类操作的优...

    小蜜蜂

扫码关注云+社区

领取腾讯云代金券