列表与队列——谈谈线程安全

关键词:线程安全、GIL、原子操作(atomic operation)、存储数据类型(List、Queue.Queue、collections.deque)

当多个线程同时进行,且共同修改同一个资源时,我们必须保证修改不会发生冲突,数据修改不会发生错误,也就是说,我们必须保证线程安全。

同时我们知道,python中由于GIL的存在,即使开了多线程,同一个时间也只有一个线程在执行。

那么这是否就说明python中多个线程执行时,不会发生冲突呢?答案是否定的。

GIL下的线程不安全

来看下面这段代码

import threading
import time
zero = 0
def change_zero():global zerofor i in range(3000000):zero += 1zero -= 1

th1 = threading.Thread(target = change_zero)
th2 = threading.Thread(target = change_zero)
th1.start()
th2.start()
th1.join()
th2.join()
print(zero) 

两个线程共同修改zero变量,每次对变量的操作都是先加1再减1,按理说执行3000000次,zero结果应该还是0,但是运行过这段代码发现,结果经常不是0,而且每次运行结果都不一样,这就是数据修改之间发生冲突的结果。

其根本原因在于,zero += 1这一步操作,并不是简单的一步,而可以看做两步的结合,如下

x = zero + 1
zero = x 

所以可能在一个线程执行时,两步只执行了一步x = zero + 1(即还没来得及对zero进行修改),GIL锁就给了另一个线程(不熟悉锁的概念以及GIL的可以先看这篇文章),等到GIL锁回到第一个线程,zero已经发生了改变,这时再执行接下来的zero = x,结果就不是对zero加1了。一次出错的完整的模拟过程如下

初始:zero = 0
th1: x1 = zero + 1# x1 = 1
th2: x2 = zero + 1# x2 = 1
th2: zero = x2# zero = 1
th1: zero = x1# zero = 1问题出在这里,两次赋值,本来应该加2变成了加1
th1: x1 = zero - 1# x1 = 0
th1: zero = x1# zero = 0
th2: x2 = zero - 1# x2 = -1
th2: zero = x2# zero = -1
结果:zero = -1 

为了更好地说明python在GIL下仍存在线程不安全的原因,这里需要引入一个概念:原子操作(atomic operation)

原子操作

原子操作,指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。

zero += 1这种一步可以被拆成多步的程序,就不是一个原子操作。不是原子操作的直接后果就是它没有完全执行结束,就有可能切换到其他线程,此时如果其他线程修改的是同一个变量,就有可能发生资源修改冲突。

一个解决办法是通过加锁(Lock),将上面change_zero函数的定义改为

def change_zero():global zerofor i in range(1000000):with lock:zero += 1zero -= 1 

加锁后,锁内部的程序要么不执行,要执行就会执行结束才会切换到其他线程,这其实相当于实现了一种“人工原子操作”,一整块代码当成一个整体运行,不会被打断。这样做就可以防止资源修改的冲突。读者可以试着加锁后重新运行程序,会发现结果zero变量始终输出为0。

下面我们来考虑一个问题:如果程序本身就是原子操作,是不是就自动实现了线程安全,就不需要加锁了呢?答案是肯定的。

举一个例子,现在我们要维护一个队列,开启多个线程,一部分线程负责向队列中添加元素,另一部分线程负责提取元素进行后续处理。这时,这个队列就是在多个线程之间共享的一个对象,我们必须保证在添加和提取的过程中,不会发生冲突,也就是说要保证线程安全。如果操作过程比较复杂,我们可以通过加锁来使多个操作中间不会中断。但是如果这个程序本身就是原子操作,则不需要添加额外的保护措施。

比如我们用queue模块中的Queue对象来维护队列,通过Queue.put填入元素,通过Queue.get提取元素,因为Queue.putQueue.get都是原子操作,所以要么执行,要么不执行,不会存在被中断的问题,所以这里就不需要添加多余的保护措施。

那么这里自然就会产生一个问题:我们怎么知道哪些操作是原子操作,哪些不是呢?官网上列了一个表

下面这些都是原子操作

L.append(x)
L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys() 

下面这些则不是原子操作

i = i+1
L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1 

这里要注意的一点是,我们有时会听到说python中的list对象不是线程安全的,这个说法是不严谨的,因为线程是否安全,针对的不是对象,而是操作。如果我们指这样的操作L[0] = L[0] + 1,它当然不是一个原子操作,不加以保护就会导致线程不安全,而L.append(i)这样的操作则是线程安全的。

因此列表是可以用作多线程中的存储对象的。但是我们一般不用列表,而使用queue.Queue,是因为后者内部实现了Condition锁的通信机制,详情请看这篇文章

下面我们回到原子操作,虽然官网上列出了一些常见的操作,但是有时我们还需要自己能够判断的方法,可以使用dis模块的dis函数,举例如下所示

from dis import dis
a = 0
def fun():global aa = a + 1dis(fun) 

结果如下

 5 0 LOAD_GLOBAL 0 (a)3 LOAD_CONST1 (1)6 BINARY_ADD7 STORE_GLOBAL0 (a) 10 LOAD_CONST0 (None) 13 RETURN_VALUE 

我们只要关注每一行即可,每一行表示执行这个fun函数的过程,可以被拆分成这些步骤,导入全局变量–>导入常数–>执行加法–>存储变量……,这里每一个步骤都是指令字节码,可以看做原子操作。这里列出的是fun函数执行的过程,而我们要关心的是a = a + 1这个过程包含了几个指令,可以看到它包含了两个,即BINARY_ADDSTORE_GLOBAL,如果在前者(运算加和)执行后,后者(赋值)还没开始时,切换了线程,就会出现我们上文例子中的修改资源冲突。

下面我们来看看L.append(i)过程的字节码

from dis import dis
l = []
def fun():global ll.append(1)dis(fun) 

得到结果

 50 LOAD_GLOBAL 0 (l) 3 LOAD_ATTR 1 (append) 6 LOAD_CONST1 (1) 9 CALL_FUNCTION 1 (1 positional, 0 keyword pair)12 POP_TOP13 LOAD_CONST0 (None)16 RETURN_VALUE 

可以看到append其实只有POP_TOP这一步,要么执行,要么不执行,不会出现被中断的问题。

原子操作我们就讲到这里,接下来,对比一下python中常用的队列数据结构

python中常见队列对比

List VS Queue.Queue VS collections.deque

首先,我们需要将Queue.Queue和其他两者区分开来,因为它的出现主要用于线程之间的通信,而其他二者主要用作存储数据的工具。当我们需要实现Condition锁时,就要用Queue.Queue,单纯存储数据则用后两者。

collections.dequelist的区别主要在于数据的插入与提取上。如果要将数据插入列表头部,或者从头部提取数据,则前者的效率远远高于后者,这是前者的双向队列特性,优势毋庸置疑。如果对提取的顺序无所谓,则没有必要一定要用collections.deque

本节主要参考下面两个回答

欢迎关注我的知乎专栏

专栏主页:python编程

专栏目录:目录

版本说明:软件及包版本说明接下来我将给各位同学划分一张学习计划表!

学习计划

那么问题又来了,作为萌新小白,我应该先学什么,再学什么?
既然你都问的这么直白了,我就告诉你,零基础应该从什么开始学起:

阶段一:初级网络安全工程师

接下来我将给大家安排一个为期1个月的网络安全初级计划,当你学完后,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web渗透、安全服务、安全分析等岗位;其中,如果你等保模块学的好,还可以从事等保工程师。

综合薪资区间6k~15k

1、网络安全理论知识(2天)
①了解行业相关背景,前景,确定发展方向。
②学习网络安全相关法律法规。
③网络安全运营的概念。
④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(1周)
①渗透测试的流程、分类、标准
②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking
③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察
④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(1周)
①Windows系统常见功能和命令
②Kali Linux系统常见功能和命令
③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(1周)
①计算机网络基础、协议和架构
②网络通信原理、OSI模型、数据转发流程
③常见协议解析(HTTP、TCP/IP、ARP等)
④网络攻击技术与网络安全防御技术
⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)
①数据库基础
②SQL语言基础
③数据库安全加固

6、Web渗透(1周)
①HTML、CSS和JavaScript简介
②OWASP Top10
③Web漏洞扫描工具
④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)

那么,到此为止,已经耗时1个月左右。你已经成功成为了一名“脚本小子”。那么你还想接着往下探索吗?

阶段二:中级or高级网络安全工程师(看自己能力)

综合薪资区间15k~30k

7、脚本编程学习(4周)
在网络安全领域。是否具备编程能力是“脚本小子”和真正网络安全工程师的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。

零基础入门的同学,我建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习
搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP,IDE强烈推荐Sublime;

Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,没必要看完

用Python编写漏洞的exp,然后写一个简单的网络爬虫

PHP基本语法学习并书写一个简单的博客系统

熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选)

了解Bootstrap的布局或者CSS。

阶段三:顶级网络安全工程师

如果你对网络安全入门感兴趣,那么你需要的话可以点击这里👉网络安全重磅福利:入门&进阶全套282G学习资源包免费分享!

学习资料分享

当然,只给予计划不给予学习资料的行为无异于耍流氓,这里给大家整理了一份【282G】的网络安全工程师从入门到精通的学习资料包,可点击下方二维码链接领取哦。

  • 21
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值