大雾!可以说很大意啦!
上周五终于改好bug,想着把代码同步到个人GitHub上面,就随手 git push
同步到远程。然而万万没想到,之前 touch
过一个 information.txt
文件,里面记录着一些服务器信息,然后也跟着同步到远程仓库去啦。等到周一,敏感信息被公司检测出来,通知删除。
可以说,真的是很大意了。在通知我之前,我还不记得有提交过什么敏感信息。直到看见具体文件信息,才知道自己犯了错误啊,吓得直接选择删除项目仓库,真是抱歉。当作教训了,以后一定要注意,不论做什么事情,都要细心点。
Ok,认真学习啦。以前在学校的时候,推导过挺多算法,也总结到个人公众号上面去啦。在实习工作之中,也能用到部分算法,使用起来也是很得心应手。但对于刚实习或工作的同学来说(就是我),吓人的技术可能略懂,但工程方面的知识却是很浅薄。就拿最简单的进程、线程问题来说,代码实现过程中也会遇到很多问题,所以在这儿总结一下,加深自己理解。重点:基础真的很重要。操作系统、网络原理、数据结构,这些知识要认真学习呢。
首先通俗例子解释下什么是进程和线程的关系。比如你开启一个QQ,就开启了一个进程。开启了微信,就开启了另外一个进程。在QQ这个进程里,传输文字是一个线程、传输语音是一个线程、弹出对话框是一个线程。也就是说,进程可以包含多个线程。
进程:进程是正在执行程序的实例,是资源分配最小的单位,每个进程都有自己单独的资源区域。进程在一定的环境下,把静态的程序代码运行起来,通过使用不同的资源,来完成一定的任务。进程的环境包括环境变量,进程所掌控的资源,有中央处理器,有内存,打开的文件,映射的网络端口等。
守护进程:运行在后台的进程,用于执行特定的系统任务。
进程的状态:只介绍进程基本状态。
进程状态的转换:进程在运行期间,不断地从一种状态转换到另一种状态,它可以多次处于就绪状态和运行状态,也可以多次处于阻塞状态。
线程:cpu调度的最小单位。线程共享进程的资源,多个线程可以共享同一地址空间和其他资源,比如共享全局变量。线程作为进程的一部分,扮演的角色就是怎么利用中央处理器去运行代码。线程关注的是中央处理器的运行,而不是内存等资源的管理。同一时刻只有一个线程占用cpu,但高速切换给人带来并行的假象。
线程状态及转换:只介绍线程基本状态。
为什么多线程?
调度:在引入线程的操作系统中,线程是调度和分配的基本单位 ,进程是资源拥有的基本单位 。把传统进程的两个属性分开,线程便能轻装运行,从而可显著地提高系统的并发程度。 在同一进程中,线程的切换不会引起进程的切换,在由一个进程中的线程切换到另一个进程中的线程时,才会引起进程的切换。
并发性:在引入线程的操作系统中,不仅进程之间可以并发执行,而且在一个进程中的多个线程之间亦可并发执行,因而使操作系统具有更好的并发性,从而能更有效地使用系统资源和提高系统吞吐量。
拥有资源:进程是资源分配的最小单位,线程是cpu调度的最小单位。进程更倾向于内存管理的概念,进程在自己的区域掌控自己的资源,也不越界。线程更倾向于cpu的运行。进程在执行过程中拥有独立的内存单元,而多个线程共享内存, 从而极大的提高了程序的运行效率。
系统开销:由于在创建或撤消进程时,系统都要为之分配或回收资源,因此,操作系统所付出的开销将显著地大于在创建或撤消线程时的开销。 进程切换的开销也远大于线程切换的开销。
健壮性:进程更为健壮。一个进程之间的某个线程死掉,整个进程就死掉了。一个进程死掉对其他进程没有影响。另外一个线程可以创建和撤销另一个线程。同一个进程中的多个线程之间可以并发执行。
通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。
所谓的锁,可以理解为内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功。如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。死锁的原因包括系统资源不足、进程运行推进顺序不合适、资源分配不当等。
比如两只羊过独木桥。进程比作羊,资源比作桥。若两只羊互不相让,争着过桥,就产生死锁。
产生死锁的四个必要条件:
多线程开发过程中,任何一个线程都可对变量进行修改,如果关键代码部分没有进行加锁,那么因此而产生bug,可能是我们不能理解的。比如我们定义了一个共享变量 balance,初始值为 0,并且启动两个线程,先存后取,理论上结果应该为 0。但是,由于线程的调度是由操作系统决定的,当 t1、t2 交替执行时,只要循环次数足够多,balance 的结果就不一定是 0 了。
import time, threading
balance = 0 # 假定这是你的银行存款:
# 先存后取,结果应该为0
def change_it(n):
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
change_it(n)
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
# 26
另外,针对加锁的代码,我们也要考虑锁的开销,尽可能只对关键代码进行加锁。写代码之前,可以先大概构思好,如何实现,考虑好数据结构等的应用,然后再去coding。
import time, threading
balance = 0
lock = threading.Lock()
def change_it(n):
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(100000):
# 先要获取锁:
lock.acquire()
try:
# 放心地改吧:
change_it(n)
finally:
# 改完了一定要释放锁:
lock.release()
t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
# 0
Python 的线程虽然是真正的线程,但解释器执行代码时,有一个 GIL 锁(Global Interpreter Lock),任何 Python 线程执行前,必须先获得 GIL 锁。每执行 100 条字节码,解释器就自动释放 GIL 锁,让别的线程有机会执行。这个 GIL 全局锁实际上把所有线程的执行代码都给上了锁。所以,多线程在 Python 中只能交替执行,即使 100 个线程跑在 100 核 CPU 上,也只能用到 1 个核。
GIL 是 Python 解释器设计的历史遗留问题,通常我们用的解释器是官方实现的 CPython,要真正利用多核,除非重写一个不带 GIL 的解释器。所以,在 Python 如果一定要通过多线程利用多核,那只能通过 C 扩展来实现。因而,多线程的并发在 Python 中就是一个美梦,如果想真正实现多核任务,还是通过多进程来实现吧。
篇幅有限,本文不再针对进程(线程)之间的通信进行介绍,有兴趣的可以直接去这儿https://www.jianshu.com/p/c1015f5ffa74。