线程与GIL锁

什么是线程:

  线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。

  线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。

  所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。

  进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。

进程与线程的关系

(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。

多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,多个控制线程共享该进程的地址空间

  多线程指的是,在一个进程中开启多个线程,简单的讲:如果多个任务共用一块地址空间,那么必须在一个进程内开启多个线程。详细的讲分为4点:

  1. 多线程共享一个进程的地址空间

      2. 线程比进程更轻量级,线程比进程更容易创建可撤销,在许多操作系统中,创建一个线程比创建一个进程要快10-100倍,在有大量线程需要动态和快速修改时,这一特性很有用

      3. 若多个线程都是cpu密集型的,那么并不能获得性能上的增强,但是如果存在大量的计算和大量的I/O处理,拥有多个线程允许这些活动彼此重叠运行,从而会加快程序执行的速度。

      4. 在多cpu系统中,为了最大限度的利用多核,可以开启多个线程,比开进程开销要小的多。(这一条并不适用于python)

开启多线程跟开启多进程的方式一样,只是调用模块不一样

方法一:

from threading import Thread

def work(n):
    print('%s is working'%n)

if __name__ == '__main__':
    t=Thread(target=work,args=(1,))
    t.start()
    print('主线程')

方法二:

from threading import Thread

class Mythread(Thread):
    def __init__(self,x):
        super().__init__()
        self.x=x

    def run(self):
        print('%s is running' %self.x)

if __name__ == '__main__':
    t=Mythread(2)
    t.start()
    print('主线程')
‘子’线程与主线程的pid都是一样的
from threading import Thread
import os
from multiprocessing import Process
def work():
    print('%s is working' %os.getpid())

if __name__ == '__main__':
    t=Thread(target=work)   # 线程
    # t=Process(target=work)  # 进程
    t.start()
    print('主线程',os.getpid())

同一进程内的多个线程共享该进程的资源

from threading import Thread
import os
from multiprocessing import Process
n=100
def work():
    global n
    n=0
if __name__ == '__main__':
    t=Thread(target=work)  # 线程
    # t=Process(target=work)  # 进程
    t.start()
    t.join()
    print('主线程',n)

# 运行结果为0

守护进程:

 主线程从执行角度就代表了该进程,主线程会在所有非守护线程都运行完毕才结束,守护线程就在主线程结束后结束,如果还有非守护线程在执行,那么就继续执行
from threading import Thread
import time
def foo():
    print(123)
    time.sleep(1)
    print('end 123')
def bar():
    print(456)
    time.sleep(3)
    print('end 456')

if __name__ == '__main__':
    t1=Thread(target=foo)
    t2=Thread(target=bar)

    t1.daemon=True
    t1.start()
    t2.start()
    print('main--------')

线程相关的其他方法

from threading import Thread,current_thread,enumerate,activeCount

def work():
    print('%s is running'%current_thread().getName())
if __name__ == '__main__':
    t=Thread(target=work)  # 线程
    t.start()
    # t.join()
    print(t.is_alive())  # 返回线程是否活动的。
    print(t.getName())   # 返回线程名
    print(current_thread().getName())   # 当前线程名
    print(enumerate()) # 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    print(activeCount())  # 返回正在运行的线程数量
    print('主线程')

GIL(全局解释器锁):

  GIL本质就是一把互斥锁,既然是互斥锁,所有互斥锁的本质都一样,都是将并发运行变成串行,以此来控制同一时间内共享数据只能被一个任务所修改,进而保证数据安全。

可以肯定的一点是:保护不同的数据的安全,就应该加不同的锁。

  Python中的线程是操作系统的原生线程,Python虚拟机使用一个全局解释器锁(Global Interpreter Lock)来互斥线程对Python虚拟机的使用。为了支持多线程机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。
GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。
在调用任何Python C API之前,要先获得GIL
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作

GIL的影响:

无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。
所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的

GIL保护的是解释器级的数据,保护用户自己的数据则需要自己加锁处理

有了GIL的存在,同一时刻同一进程中只有一个线程被执行

from threading import Thread
import os,time
def work():
    global n  # 在每个线程中都获取这个全局变量
    temp=n
    time.sleep(0.1)    模拟io
    n=temp-1   # 对此公共变量进行-1操作
if __name__ == '__main__':
    n=100  # 设定一个全局共享变量
    l=[]
    for i in range(100):
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()

    print(n) #结果可能为99

 互斥锁(同步锁)

 锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

 锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁

from threading import Thread,Lock

import time,os
n=100
def work():
    global n

    lock.acquire()  在改变数据之前加锁
    temp=n
    time.sleep(0.1)  模拟IO,没有io因为cpu执行速度太快,结果大部分正确,但为了保证数据安全,最好加上锁
    n=temp-1
    lock.release()  解锁

  # with lock: 自动加锁解锁
      # temp=n

  # n=temp-1
if __name__ == '__main__':
  t_l
=[]
  lock
=Lock()

  for i in range(100):
    t
=Thread(target=work)
    t_l.append(t)
    t.start()

  for i in t_l:
    i.join()

  print(n)
#结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全

分析:
#1.100个线程去抢GIL锁,即抢执行权限
#2. 肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
#3. 极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
#4.直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程

#不加锁:并发执行,速度快,数据不安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    global n
    print('%s is running' %current_thread().getName())
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()

    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:0.5216062068939209 n:99
'''


#不加锁:未加锁部分并发执行,加锁部分串行执行,速度慢,数据安全
from threading import current_thread,Thread,Lock
import os,time
def task():
    #未加锁的代码并发运行
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    #加锁的代码串行运行
    lock.acquire()
    temp=n
    time.sleep(0.5)
    n=temp-1
    lock.release()

if __name__ == '__main__':
    n=100
    lock=Lock()
    threads=[]
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        threads.append(t)
        t.start()
    for t in threads:
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 is running
Thread-2 is running
......
Thread-100 is running
主:53.294203758239746 n:0
'''

#有的同学可能有疑问:既然加锁会让运行变成串行,那么我在start之后立即使用join,就不用加锁了啊,也是串行的效果啊
#没错:在start之后立刻使用jion,肯定会将100个任务的执行变成串行,毫无疑问,最终n的结果也肯定是0,是安全的,但问题是
#start后立即join:任务内的所有代码都是串行执行的,而加锁,只是加锁的部分即修改共享数据的部分是串行的
#单从保证数据安全方面,二者都可以实现,但很明显是加锁的效率更高.
from threading import current_thread,Thread,Lock
import os,time
def task():
    time.sleep(3)
    print('%s start to run' %current_thread().getName())
    global n
    temp=n
    time.sleep(0.5)
    n=temp-1


if __name__ == '__main__':
    n=100
    lock=Lock()
    start_time=time.time()
    for i in range(100):
        t=Thread(target=task)
        t.start()
        t.join()
    stop_time=time.time()
    print('主:%s n:%s' %(stop_time-start_time,n))

'''
Thread-1 start to run
Thread-2 start to run
......
Thread-100 start to run
主:350.6937336921692 n:0 #耗时是多么的恐怖
'''

互斥锁与join的区别(重点!!!)
GIL锁与互斥锁综合分析(重点!!!)
from multiprocessing import Process
from threading import Thread
import os,time
def work():
    res=0
    for i in range(100000000):
        res*=i


if __name__ == '__main__':
    l=[]
    print(os.cpu_count())
    start=time.time()
    for i in range(4):
        p=Process(target=work) 
        p=Thread(target=work)
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

计算密集型:多进程效率高
计算密集型:多进程效率高
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
    time.sleep(2)
    print('===>')

if __name__ == '__main__':
    l=[]
    print(os.cpu_count()) 
    start=time.time()
    for i in range(400):
        # p=Process(target=work) #大部分时间耗费在创建进程上
        p=Thread(target=work) #耗时2s多
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop=time.time()
    print('run time is %s' %(stop-start))

I/O密集型:多线程效率高
I/O密集型:多线程效率高

GIL友情链接:http://www.cnblogs.com/linhaifeng/articles/7449853.html

多线程链接:http://www.cnblogs.com/linhaifeng/articles/7428877.html

原文地址:https://www.cnblogs.com/sunxiansheng/p/7662763.html