• 8_14 并发编程三 线程的GIL


    一。GIL

      什么是GIL?

      GIL是一个全局排他锁,简单来说就是为了防止多线程并行操作的锁。这里有官方解释:

    In CPython, the global interpreter lock, or GIL, is a mutex 
    that prevents multiple native threads from executing Python 
    bytecodes at once. This lock is necessary mainly because 
    CPython’s memory management is not thread-safe. (However,
     since the GIL exists, other features have grown to depend on
     the guarantees that it enforces.)
    在cpython中,全局解释器锁(gil)是一个互斥体,它可以防止多个本
    机线程同时执行python字节码。这个锁是必要的,主要是因为cpython
    的内存管理不是线程安全的。(然而,由于gil存在,其他特性也越来越
    依赖于它所执行的保证。)

      GIL的存在是因为CPython解释器的内存管理不是线程安全的

      在一个进程中有多个线程,多个线程之间如果可以并行会是什么样子?以垃圾回收机制为例。

      垃圾回收机制也是一种线程,在解释器中,每创建一个程序就会加载一次解释器。
      如果python中没有GIL的话就可能出现一种情况,当线程a在运行时创建了一个名称a,而立刻运行了垃圾回收机制,垃圾回收机制就立刻把那个名称a给回收了等到再次运行线程a时,会出现名称空间丢失的情况,
      这是不被允许的。所以GIL就是为了使得所有线程都是串行运行,但是这样会大大降低运行的效率。而且没有了多核优势,但是保证了数据的安全性。

      事实上,GIL并不是python的特点,他是所有解释性语言的通病,在python的编译器中有很多有GIL的编译器。

      那么,是不是意味着有了GIL就可以不用其他锁了呢。答案显然是否定的,

    from threading import Thread
    import time
    
    n = 100
    def task():
        global n
        tmp = n
        # time.sleep(1)
        n = tmp -1
    
    t_list = []
    for i in range(100):
        t = Thread(target=task)
        t.start()
        t_list.append(t)
    
    for t in t_list:
        t.join()
    
    print(n)

       在线程中有io操作,使得其他线程有了抢锁的时间,导致最后的结果不是0,所有还是要根据不同数据添加不同的锁。

    二。多线程的作用。

      从当前来看,多进程可以支持并行操作,在当下多核处理器为主流的情况下是不是就不需要多线程了呢。

      这要从各个进程/线程的计算密集度与io密集度情况分析。

      计算密集型:

    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())  # 本机为6核
        start=time.time()
        for i in range(6):
            # p=Process(target=work) #耗时  4.732933044433594
            p=Thread(target=work) #耗时 22.83087730407715
            l.append(p)
            p.start()
        for p in l:
            p.join()
        stop=time.time()
    print('run time is %s' %(stop-start))

      io密集型

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

      从这就可以看出如果是计算密集型的就是使用进程比较有优势,因为可以多核计算,然而就io密集型的就是要线程居多,毕竟cpu利用率高还需要创建大量的进程,比较耗时间。

    二。死锁:

      死锁就是在并发进程中因为争夺系统资源而产生相互等待的现象。

      四个必要条件:

      1。互斥。

      2.占有并等待

      3.不可抢占

      4.循环等待

      例子:

    class MyThread(Thread):
        def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
            self.func1()
            self.func2()
    
        def func1(self):
            mutexA.acquire()
            print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
            mutexB.acquire()
            print('%s抢到了B锁'%self.name)
            mutexB.release()
            print('%s释放了B锁'%self.name)
            mutexA.release()
            print('%s释放了A锁'%self.name)
    
        def func2(self):
            mutexB.acquire()
            print('%s抢到了B锁'%self.name)
            time.sleep(1)
            mutexA.acquire()
            print('%s抢到了A锁' % self.name)
            mutexA.release()
            print('%s释放了A锁' % self.name)
            mutexB.release()
            print('%s释放了B锁' % self.name)
    
    for i in range(10):
        t = MyThread()
        t.start()

      定义一个自己的线程类,其中run方法在运行是,需要经过两个锁的过程,当10个线程先后执行时,第一个获取ab锁的人在获取b锁后再获取a锁,会发现锁被其他线程给站住了,双方互相站住了对方所需要的资源,而自己这边也有对方需要的资源,这样就形成了死锁。

      不要轻易的处理死锁的问题。

      递归锁。

      在python中有递归锁的概念。递归锁是一个可以连续使用的锁,Rlock可以被第一个抢到锁的人连续的acquire和release

      每acquire一次锁身上的计数加1

      每release一次锁身上的计数减1

      只要锁的计数不为0 其他人都不能抢

    mutexA = mutexB = RLock()# A B现在是同一把锁
    class MyThread(Thread):
        def run(self):  # 创建线程自动触发run方法 run方法内调用func1 func2相当于也是自动触发
            self.func1()
            self.func2()
    
        def func1(self):
            mutexA.acquire()
            print('%s抢到了A锁'%self.name)  # self.name等价于current_thread().name
            mutexB.acquire()
            print('%s抢到了B锁'%self.name)
            mutexB.release()
            print('%s释放了B锁'%self.name)
            mutexA.release()
            print('%s释放了A锁'%self.name)
    
        def func2(self):
            mutexB.acquire()
            print('%s抢到了B锁'%self.name)
            time.sleep(1)
            mutexA.acquire()
            print('%s抢到了A锁' % self.name)
            mutexA.release()
            print('%s释放了A锁' % self.name)
            mutexB.release()
            print('%s释放了B锁' % self.name)
    
    for i in range(10):
        t = MyThread()
        t.start()

    三。信号量

      信号量是一个为线程或进程服务的概念,设置一个指定数量的占位,就是最多可以容纳该数的进程或线程执行,当其被占满时,就会处于等待的状态,每个进程或线程在运行时都会执行锁操作,运行结束执行释放锁操作。例子:

    from threading import Semaphore,Thread
    import time
    import random
    
    
    sm = Semaphore(5)  # 造了一个含有五个的坑位的公共厕所
    
    def task(name):
        sm.acquire()
        print('%s占了一个坑位'%name)
        time.sleep(random.randint(1,3))
        sm.release()
    
    for i in range(40):
        t = Thread(target=task,args=(i,))
        t.start()

    四。event事件

      正常情况下使用join方法可以实现主程序等待子程序的功能,但是event可以实现子进程等子进程的功能。

    from threading import Event,Thread
    import time
    
    # 先生成一个event对象
    e = Event()
    
    
    def light():
        print('红灯正亮着')
        time.sleep(3)
        e.set()  # 发信号
        print('绿灯亮了')
    
    def car(name):
        print('%s正在等红灯'%name)
        e.wait()  # 等待信号
        print('%s加油门飙车了'%name)
    
    t = Thread(target=light)
    t.start()
    
    for i in range(10):
        t = Thread(target=car,args=('伞兵%s'%i,))
        t.start()

    五。线程q

      同一个进程下的多个线程本来就是数据共享 为什么还要用队列

      因为队列是管道+锁 使用队列你就不需要自己手动操作锁的问题.

      锁操作的不好极容易产生死锁现象.

      queue.Queue() ,先进先出的队列

      queue.LifoQueue(),先进后出的队列(栈)

      queue.PriorityQueue(),可以设置优先级的队列,优先级数字越低,就越先被拿出

    import queue
    
    # q = queue.Queue()
    # q.put('hahha')
    # print(q.get())
    
    
    # q = queue.LifoQueue()
    # q.put(1)
    # q.put(2)
    # q.put(3)
    # print(q.get())
    
    
    q = queue.PriorityQueue()
    # # 数字越小 优先级越高
    q.put((10,'haha'))
    q.put((100,'hehehe'))
    q.put((0,'xxxx'))
    q.put((-10,'yyyy'))
    print(q.get())

    补充

      不使用socketserver实现TCP并发。

      思路:使用多线程,讲链接conn当成参数传给线程,多线程可以使其链接多个客户端

    import socket
    
    client = socket.socket()
    client.connect(('127.0.0.1',8080))
    
    while True:
        msg = input('>>>').encode('utf-8')
        client.send(msg)
        recv_msg = client.recv(1024)
        print(recv_msg)
    客户端
    import socket
    from threading import Thread
    
    server = socket.socket()
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    
    def talk(conn):
        while True:
            try:
                msg = conn.recv(1024)
                return_s = msg
                conn.send(return_s)
            except ConnectionResetError:
                print('断开链接')
                break
        conn.close()
    
    while True:
        conn,addr = server.accept()
        t = Thread(target=talk,args=(conn,))
        t.start()
    服务器

    在cpython中,全局解释器锁(gil)是一个互斥体,它可以防止多个本机线程同时执行python字节码。这个锁是必要的,主要是因为cpython的内存管理不是线程安全的。(然而,由于gil存在,其他特性也越来越依赖于它所执行的保证。)

  • 相关阅读:
    关于添加“服务引用”和“添加引用”的一点总结
    nodejs+express工程 在npm install之后或使用npm install bootstrap命令安装bootstrap之后
    Mongo基础使用,以及在Express项目中使用Mongoose
    Express URL跳转(重定向)的实现
    node 开发web 登陆功能
    node js实战:带数据库,加密的注册登录表单
    nodejs 进阶:封装所有对数据库的常用操作
    nodejs进阶:密码加盐:随机盐值
    Nodejs进阶:密码加盐
    express 路由能力
  • 原文地址:https://www.cnblogs.com/LZXlzmmddtm/p/11355353.html
Copyright © 2020-2023  润新知