1. 线程与进程的区别?
(1)进程:进程是程序的一次执行过程,是一个动态概念,是程序在执行过程中分配和管理资源的基本单位,每一个进程都有一个自己的地址空间,至少有 5 种基本状态,它们是:初始态,执行态,等待状态,就绪状态,终止状态。
(2)线程:线程是CPU调度和分派的基本单位,它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
(3)联系与区别:
难点:处理好同步与互斥。
2. 进程间内存是否共享?如何实现通讯?
进程间内存是共享的:
进程间实现通讯的方法有:
但主要有:共享内存、信号、管道、消息队列四种,其通信特点:
通信方法 | 无法介于内核态与用户态的原因 |
管道(不包括命名管道) | 局限于父子进程间的通信。 |
消息队列 | 在硬、软中断中无法无阻塞地接收数据。 |
信号量 | 无法介于内核态和用户态使用。 |
共享内存 | 需要信号量辅助,而信号量又无法使用。 |
4. 多线程有几种实现方法,都是什么?
方法一:python的threading 模块。
例如:
方法二:重写run方法。
例如:
5.GIL锁是怎么回事?
GIL即全局解释器锁,简单来说就是一个互斥体(或者说锁),这种机制的存在只允许一个线程来控制Python解释器。
这就意味着在任何一个时间点只能有一个线程处于执行状态。GIL对执行单线程任务的程序员们来说并没什么显著影响,但是它对计算密集型(CPU-bound)和多线程任务的影响是比较大的。 由于GIL即使在拥有多个CPU核的多线程框架下都只允许一次运行一个线程,因此成为众多python功能的一大问题。但为什么开发人员还是选择了GIL呢?
1)GIL解决了Python中的问题:
Python利用引用计数来进行内存管理,这就意味着在Python中创建的对象都有一个引用计数变量来追踪指向该对象的引用数量。当数量为0时,该对象占用的内存即被释放。这个引用计数变量需要在两个线程同时增加或减少时从竞争条件中得到保护。如果发生了这种情况,可能会导致泄露的内存永远不会被释放,抑或更严重的是当一个对象的引用仍然存在的情况下错误地释放内存。这可能会导致Python程序崩溃或带来各种诡异的bug。通过对跨线程分享的数据结构添加锁定以至于数据不会不一致地被修改,这样做可以很好的保证引用计数变量的安全。
2)选取GIL作为解决方案的原因:
3)GIL对多线程Python程序的影响:
4)处理Python中的GIL的方法:
多进程vs多线程:最流行的方法是应用多进程方法,在这个方法中你使用多个进程而不是多个线程。每一个Python进程都有自己的Python解释器和内存空间,因此GIL不会成为问题。
注:如果想了解GIL的低层次内部运行,建议选择观看David Beazley的Understanding the Python GIL。
6. python中是否线程安全?如何解决线程安全?
线程优点: 在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提 下完成多线程之间的数据共享(这点要⽐多进程要好) 线程缺点: 线程是对全局变量随意遂改可能造成多线程之间对全局变量 的混乱(即线程⾮安全)
解决方法:
使用全局解释器锁(GIL):Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
7. 什么叫死锁?
在多道程序系统中,由于多个进程的并发执行,改善了系统资源的利用率并提高了系统的处理能力。然而,多个进程的并发执行也带来了新的问题——死锁。所谓死锁是指多个进程因竞争资源而造成的一种僵局,若无外力作用,这些进程都将无法向前推进。(或者说:在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。)
避免死锁的几种办法:
8. 什么是协程?常用的协程模块有哪些?
1)定义:
协程与子例程一样,协程(coroutine)也是一种程序组件。又称微线程、纤程。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。相对子例程而言,协程更为一般和灵活,但在实践中使用没有子例程那样广泛。
2)协程优势:
3)协程缺点:
3)常用的协程模块:
9. 协程中的join是用来做什么用的?它是如何发挥作用的?
1.协程执行时要想使任务执行则必须对协程对象调用join函数
2.有多个任务时,随便调用哪一个的join都会并发的执行所有任务,但是需要注意如果一个存在io的任务没有被join该任务将无法正常执行完毕
join函数源码在greenlet.py中的Greenlet类的join():
ef join(self, timeout=None):
if self.ready(): #检测是否执行完成
return
else:
switch = getcurrent().switch #获得当前greenlet的switch回调函数
self.rawlink(switch)
try:
t = Timeout.start_new(timeout)
try:
result = self.parent.switch()
assert result is self, 'Invalid switch into Greenlet.join(): %r' % (result, )
finally:
t.cancel()
except Timeout as ex:
self.unlink(switch)
if ex is not t:
raise
except:
self.unlink(switch)
raise
从join的源码第六行,跟踪到rawlink
函数:
def rawlink(self, callback):
if not callable(callback):
raise TypeError('Expected callable: %r' % (callback, ))
self._links.append(callback)
if self.ready() and self._links and not self._notifier:
self._notifier = self.parent.loop.run_callback(self._notify_links)
可以看出,这个rawlink
函数的目的只有一个:注册当前greenlet的回调函数(第四行), 当主协程hub还没有run的时候,这个时候的greenlet可以理解为一个上下文。
回到join函数。在注册了当前greenlet的回调函数后,主要干的事是第10行:切换到主协程hub
它会执行greenlet.switch(self)
,由于当前协程为hub,并且没有运行过run,所以会执行hub.run
函数,源码在hub.py下的Hub类里面。在这个函数里面就会执行gevent的一般流程。
以下函数的输出:
def test1(id):
print(id)
gevent.sleep(0)
print(id, 'is done!')
t = gevent.spawn(test1, 't')
gevent.sleep(0)
输出(为什么没有继续输出t is done!
?):
t
因此join()的作用是:
loop.run_callback(waiter.switch)
保存的是当前协程(可以理解为上下文)的回调函数waiter.get()
函数waiter.get()
函数调用hub.switch()
,切换到主协程hubhub.run()
函数_run()
函数,即子协程的任务:我们定义的test1函数sleep(0)
的时候,会回到主协程hub,hub会执行之前保存的回调函数,即回到了上下文,不会再回到主协程hub,所以不会输出t is done!