并发编程之线程

Posted shenjianping

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了并发编程之线程相关的知识,希望对你有一定的参考价值。

一、线程概述

1、什么是线程?

线程依附进程而存在的,一个进程至少有一个线程,线程相当于微进程,多进程能实现并发,多线程也同样可以。线程是最小的执行单元。

技术图片

 

2、线程的特点

  • 线程依附进程存在
  • 同一个进程中的线程数据是互相共享的
  • 一个进程可以开启多个线程
  • 进程相当于容器,线程相当于容器中的实体
  • 无论启动多少个线程,电脑有多少个cpu,Python在某一时刻只允许一个线程运行

3、为什么需要线程?

进程的开销过大,因为进程在创建过程中,每一个进程都需要各自持有一份数据,而线程因为是在同一个进程中,它们可以共享一份数据,这样减少了并发性能方面的损耗。

二、threading基于线程的并行

(一)直接调用

from threading import Thread
import time

def task(url):
    time.sleep(1)
    print(url,time.ctime())

url_list=[
    https://www.baidu.com,
    https://www.zhihu.com,
    https://www.163.com,
]

if __name__ == __main__:
    p_list = []
    for url in url_list:
        p = Thread(target=task,args=(url,))#实例化3个线程
        p.start() #启动每一个线程
        print(p.getName()) #获取每一个线程的名字

"""
输出
Thread-1
Thread-2
Thread-3
https://www.baidu.com Fri Sep 27 22:05:22 2019
https://www.zhihu.com Fri Sep 27 22:05:22 2019
https://www.163.com Fri Sep 27 22:05:22 2019
"""

(二)继承式调用

from threading import Thread
import time

class MyThread(Thread):

    def __init__(self,url):
        self.url = url
        super(MyThread,self).__init__()

    def run(self):
        """
        必须实现线程运行的run方法
        :return: 
        """
        time.sleep(1)
        print(url, time.ctime())

url_list=[
    https://www.baidu.com,
    https://www.zhihu.com,
    https://www.163.com,
]

if __name__ == __main__:
    p_list = []
    for url in url_list:
        p = MyThread(url)#实例化3个线程
        p.start() #启动每一个线程
        print(p.getName()) #获取每一个线程的名字
"""
输出
Thread-1
Thread-2
Thread-3
https://www.163.com Fri Sep 27 22:13:27 2019
https://www.163.com Fri Sep 27 22:13:27 2019
https://www.163.com Fri Sep 27 22:13:27 2019
"""

(三)threading.Thread类的属性和方法

1、构造方法

 def __init__(self, group=None, target=None, name=None,
                 args=(), kwargs=None, *, daemon=None):
        """This constructor should always be called with keyword arguments. Arguments are:

        *group*应该是None; 当ThreadGroup实现一个类时保留给以后的扩展。
        
        *target* run()方法要调用的可调用对象。默认为None,表示什么都不会被调用。

        *name*是线程名称。默认情况下,唯一名称以“ Thread- N ” 的形式构造,其中N是一个小十进制数字。

        *args*是目标调用的参数元组。默认为()。

        *kwargs*是用于目标调用的关键字参数的字典。默认为{}。

        """

2、方法

  • start()

启动线程的活动。每个线程对象最多只能调用一次。使run()方法在单独的控制线程中调用对象的方法。

  • run()

表示线程活动的方法。可以在子类中重写此方法。标准run()方法调用传递给对象构造函数的可调用对象作为目标参数(如果有),并分别argskwargs参数中获取顺序和关键字参数

  • join(timeout=None)

等待线程终止。这将阻塞调用线程,直到join()被调用方法的线程终止(正常或通过未处理的异常终止),或者直到发生可选的超时。

  • getName()

获取正在运行的线程的名称

  • is_alive()

返回线程是否处于活动状态。该方法返回Truerun()方法开始之前返回直到run()方法终止之后。模块函数enumerate()返回所有活动线程的列表。

  • setDaemon(True)

将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。

         当我们 在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成

         想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程

         完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦

3、属性

  • daemon

一个布尔值,指示此线程是否是守护线程(真)(假)。必须在start()调用之前设置此参数,否则RuntimeError引发该参数。它的初始值是从创建线程继承的;主线程不是守护程序线程,因此在主线程中创建的所有线程默认为 daemonFalse

当没有活动的非守护线程时,整个Python程序将退出。

  • name

仅用于标识,没有语义。多个线程可以被赋予相同的名称。初始名称由构造函数设置。

三、concurrent.futures模块

  • 该模块为了并行任务提高更高级别的接口
  • 为了执行异步调用该模块既可以实现进程池也可以实现线程池
from concurrent.futures import ThreadPoolExecutor
import requests
import time


def task(url):

    response=requests.get(url)
    print(response,time.ctime())


pool=ThreadPoolExecutor(5)

url_list=[
    https://www.baidu.com,
    https://www.zhihu.com,
    https://www.163.com,
]
for url in url_list:
    pool.submit(task,url) #异步提交任务

pool.shutdown() #相当于进程池的pool.close()+pool.join()操作
"""
输出:
<Response [200]> Fri Sep 27 23:05:16 2019
<Response [200]> Fri Sep 27 23:05:16 2019
<Response [400]> Fri Sep 27 23:05:21 2019
"""

另外,还有一种写法就是加上回调函数,将结果返回给future,之后使用future.result()将结果接收,在回调函数中单独处理。

技术图片
from concurrent.futures import ThreadPoolExecutor
import requests
import time

def task(url):
    response=requests.get(url)
    return response

# ########d###
def done(future,*args,**kwargs):
    """
    done为回调函数,task执行的结果返回给future,将结果与之后的动作分离开来
    
    :param future: 
    :param args: 
    :param kwargs: 
    :return: 
    """
    response=future.result()
    print(response)

pool=ThreadPoolExecutor(5)

url_list=[
    https://www.baidu.com,
    https://www.zhihu.com,
    https://www.163.com,
]
for url in url_list:
    res=pool.submit(task,url)
    res.add_done_callback(done)

pool.shutdown()
"""
输出:
<Response [200]>
<Response [200]>
<Response [400]>
"""
回调函数

四、threading.local模块

(一)使用

可以创建一个全局对象,各个线程可以用这个全局对象保存各自的局部变量,而在使用时不受其他线程的影响。

from threading import local,Thread,current_thread

data = local() #定义全局local对象

def handle():
    data.x = 1
    for i in range(50):
        data.x += 1
    print(current_thread(),data.x)
if __name__ == __main__:

    for i in range(5):
        t = Thread(target=handle)
        t.start()
"""
输出:
<Thread(Thread-1, started 3968)> 51
<Thread(Thread-2, started 5936)> 51
<Thread(Thread-3, started 8808)> 51
<Thread(Thread-4, started 7960)> 51
<Thread(Thread-5, started 7144)> 51
"""

可以看到每一个线程输出的值都是一样的,虽然定义了全局对象local,但是定义的data.x属性是每一个线程独有的。本质是不同的线程使用同一个local对象创建不同的数据字典。

(二)全局变量和局部变量

1、global

全局变量使用global关键字

from threading import local,Thread,current_thread

x=1
def handle():
    global x
    for i in range(50):
        x += 1
    print(current_thread(),x)
if __name__ == __main__:

    for i in range(5):
        t = Thread(target=handle)
        t.start()
"""
输出:
<Thread(Thread-1, started 8384)> 51
<Thread(Thread-2, started 1212)> 101
<Thread(Thread-3, started 7508)> 151
<Thread(Thread-4, started 8904)> 201
<Thread(Thread-5, started 8024)> 251
"""

定义全局变量x,这样每一个线程都更改同一个变量,导致计算杂乱无章。

2、局部变量

from threading import local,Thread,current_thread

def handle():
    x =1
    for i in range(50):
        x += 1
    print(current_thread(),x)
if __name__ == __main__:

    for i in range(5):
        t = Thread(target=handle)
        t.start()
"""
输出:
<Thread(Thread-1, started 7244)> 51
<Thread(Thread-2, started 6704)> 51
<Thread(Thread-3, started 1488)> 51
<Thread(Thread-4, started 844)> 51
<Thread(Thread-5, started 8600)> 51
"""

每一个线程使用自己的x属性,所以输出的值都是一定的。

(三)总结

threading.local模块可以对线程的数据进行管理:

  • local模块实例化全局对象
  • 每一个线程在使用这个对象都将创建自己的一个字典,类似于局部变量
  • 每一个线程数据字典是独立的,互不干扰,试图去读取其它线程的数据会导致错误

五、线程通信

(一)queue模块

1、队列使用的必要性

当必须在多个线程之间安全地交换信息时,它就显得尤为重要了,因为内置了很多锁,保证了数据的安全性。

2、queue模块中的类

  • queue.Queue(maxsize = 0)

  FIFO(先进先出)队列的构造函数。 maxsize是一个整数,用于设置可以放入队列中的项目数的上限。一旦达到此大小,插入将被阻塞,直到消耗队列项目为止。如果 maxsize小于或等于零,则队列大小为无限。

  • queue.LifoQueue(maxsize = 0)

  LIFO(后进先出)队列的构造函数。 maxsize是一个整数,用于设置可以放入队列中的项目数的上限。一旦达到此大小,插入将被阻塞,直到消耗队列项目为止。如果 maxsize小于或等于零,则队列大小为无限。

  • queue.PriorityQueue(maxsize = 0)

  优先级队列的构造函数。 maxsize是一个整数,用于设置可以放入队列中的项目数的上限。一旦达到此大小,插入将被阻塞,直到消耗队列项目为止。如果 maxsize小于或等于零,则队列大小为无限。
最低值的条目将首先被检索(最低值的条目是由返回的条目sorted(list(entries))[0])。条目的典型模式是形式为元组(priority_number, data)。

  • 异常queue.Empty

空对象上调用非阻塞get()(或 get_nowait()时引发异常Queue。

  • 异常queue.Full

在已满的队列对象上调用非阻塞put()(或 put_nowait())时引发异常Queue。

  该模块实现了三种类型的队列,它们的区别仅在于检索条目的顺序不同。在FIFO 队列中,首先检索到添加的第一个任务。在 LIFO队列中,最近添加的条目是第一个检索到的条目(操作类似于堆栈)。使用优先级队列,条目将保持排序(使用heapq模块),并且最先检索值最低的条目。

  在内部,该模块使用锁来临时阻止竞争线程。

3、队列对象

队列对象(Queue,LifoQueue或PriorityQueue)提供的公共方法:

  • Queue.qsize()

返回队列的大小。注意,qsize()> 0不能保证后续的get()不会阻塞,qsize()<maxsize也不能保证put()不会阻塞。

  • Queue.empty()

如果返回True,队列为空。注意,如果返回True,不能保证后续put()的调用都不会阻塞。同样,如果empty()返回False,也不能保证get()的后续调用不会阻塞。

  • Queue.full()

如果队列已满,返回True,否则返回False。注意,如果full()返回True,不能保证get()的后续调用不会阻塞。同样,如果full()返回False,也不能保证后续put()的调用都不会阻塞。

  • Queue.put(item,block = True,timeout = None )

  将item放入队列。如果可选的args、block为True且timeout为 None(默认),则在必要时阻塞,直到有可用插槽可用。如果 超时为正数,则它最多会阻塞超时的时间,Full如果在该时间内没有空闲插槽可用,则会引发异常。否则(block为False),如果在该时间内没有空闲插槽可用,则会引发异常。如果有空闲插槽立即可用,则将item放在队列中。否则引发Full异常(在这种情况下将忽略超时)。

  • Queue.put_nowait(item)

等同于put(item, False)

  • Queue.get(block = True,timeout = None )

  从队列中删除并返回一个item。如果可选的args block为true,并且 timeout为None默认值,则在必要时阻塞,直到有可用的item为止。如果超时为正数,则它最多会阻塞超时的时间,Empty如果在该时间内没有可用的item,则会引发异常。否则(block为false),如果有立即可用的item,则返回一个item,否则引发Empty异常(在这种情况下,超时将被忽略)。

  • Queue.get_nowait()

等同于get(False)。

  • Queue.task_done()

在完成一项工作之后,Queue.task_done() 函数向任务已经完成的队列发送一个信号

  • Queue.join()

阻塞直到队列中的所有item都已获得并处理。实际上意味着等到队列为空,再执行别的操作。
每当将item添加到队列时,未完成任务的数量就会增加。每当使用者线程调用task_done()以指示已检索到该item并且该item的所有工作完成时,该计数就会减少。当未完成的任务数降至零时,join()取消阻止。

(二)使用

1、先进先出队列

mport queue
import threading
import time

def do_work(item):
    print("%s已经完成"%item,time.ctime())
    time.sleep(1)

def worker():
    while True:
        item = q.get() #取出队列中的任务
        if item is None:
            break
        do_work(item) #进行任务
        q.task_done() #任务完成后向队列发送一个讯号通知一下

if __name__ == __main__:

    q = queue.Queue()
    t_list = []
    num_worker_threads = 5

    # 5个工人线程完成任务
    for i in range(num_worker_threads):
        t = threading.Thread(target=worker)
        t.start()

    # 将10个任务放入队列中
    for item in range(10):
        q.put(item)

    # 等队列中的任务全部取出并且处理
    q.join()

    #结束所有工人线程
    for i in range(num_worker_threads):
        q.put(None)

    for t in t_list:
        t.join()
"""
输出:
0已经完成 Sat Sep 28 15:51:41 2019
1已经完成 Sat Sep 28 15:51:41 2019
2已经完成 Sat Sep 28 15:51:41 2019
3已经完成 Sat Sep 28 15:51:41 2019
4已经完成 Sat Sep 28 15:51:41 2019
5已经完成 Sat Sep 28 15:51:42 2019
6已经完成 Sat Sep 28 15:51:42 2019
8已经完成 Sat Sep 28 15:51:42 2019
9已经完成 Sat Sep 28 15:51:42 2019
7已经完成 Sat Sep 28 15:51:42 2019
"""

注意:

#put、put_nowait和get、get_nowait方法的区别

q.put() #不会报错,阻塞等待
q.put_nowait() #如果队列是有长度的,那么放满了此时不会阻塞,而是直接报错
q.get() #不会报错,阻塞等待
q.get_nowait() #如果队列已经空了,那么继续get不会阻塞,而是直接报错

#使用用异常处理,来让队列不阻塞也不报错
#     try:
#         q.get_nowait() #为空会报错
#     except:
#         print(‘队列已空‘)

2、其它队列

import queue

#LifoQueue后进先出
q = queue.LifoQueue()

q.put(1)
q.put(2)
print(q.get()) #输出为2,后进先出,类似栈

# PriorityQueue 优先队列,put参数传入为一个元组 (优先级,要传入得值)
#数字越小,代表优先级越高。当优先级一样的时候,根据传入的值的 ASCII码值的顺序,进行排列
q = queue.PriorityQueue() q.put((11,zhangsan)) q.put((2,lisi)) q.put((6,wangwu)) print(q.get()) #输出(2, ‘lisi‘)

六、线程同步

(一)threading.Lock 同步锁/互斥锁

  当遇到多个线程操作同一个资源时,会引发资源安全或者错乱的情况,此时可以使用同步锁/互斥锁进行解决,在某一时刻只允许一个线程来操作该资源。比较形象的比喻就是房子外面挂着一把钥匙,谁拿着钥匙就进去,直到钥匙被归还,另一个线程再拿着钥匙进去。

from threading import Lock, Thread

data = 100  # 设置一个共享的全局变量


def handle(lock):
    global data
    lock.acquire()  # 拿到钥匙
    data = data - 1
    lock.release()  # 归还钥匙


if __name__ == __main__:
    lock = Lock()

    t_list = []
    for i in range(5):
        t = Thread(target=handle, args=(lock,))
        t_list.append(t)
        t.start()

    [t.join() for t in t_list]  # 等待所有的线程执行完毕

    print(data)

死锁:当多个进程抢夺同一个资源而造成互相等待的现象,若无外力作用将会一直持续下去,此时系统处于死锁状态。

(二)threading.RLock 递归锁

递归锁就是用来解决在使用同步锁情况下出现的死锁情况。下面是一个死锁的情况:

import threading
import time


def eat1(name):
    noodle_lock.acquire()
    time.sleep(3)
    print("%s获取面条" % name)
    fork_lock.acquire()
    print("%s获取叉子" % name)
    print("%s吃面了" % name)
    fork_lock.release()
    noodle_lock.release()


def eat2(name):
    fork_lock.acquire()
    time.sleep(5)
    print("%s获取叉子" % name)
    noodle_lock.acquire()
    print("%s获取面条" % name)
    print("%s吃面了" % name)
    noodle_lock.release()
    fork_lock.release()


if __name__ == __main__:
    noodle_lock = threading.Lock()
    fork_lock = threading.Lock()
    threading.Thread(target=eat1, args=(zhangsan,)).start()
    threading.Thread(target=eat2, args=(lisi,)).start()

"""
输出:
zhangsan获取面条
lisi获取叉子
"""

上面的当zhangsan获取面条这个锁时,lisi获取了叉子这个锁,当zhangsan再获取叉子这个锁时已经无法获取,同样的lisi再获取面条这个锁时也是无法获取,这样就造成了死锁。

使用递归锁可以解决这个问题,递归锁相当于有一串钥匙。

import threading
import time


def eat1(name):
    noodle_lock.acquire()
    time.sleep(3)
    print("%s获取面条" % name,time.ctime())
    fork_lock.acquire()
    print("%s获取叉子" % name,time.ctime())
    print("%s吃面了" % name,time.ctime())
    fork_lock.release()
    noodle_lock.release()


def eat2(name):
    fork_lock.acquire()
    time.sleep(5)
    print("%s获取叉子" % name,time.ctime())
    noodle_lock.acquire()
    print("%s获取面条" % name,time.ctime())
    print("%s吃面了" % name,time.ctime())
    noodle_lock.release()
    fork_lock.release()


if __name__ == __main__:
    noodle_lock = fork_lock = threading.RLock() #递归锁,相当于拥有一串钥匙,其它的线程拿不到了,相当于将拿到面条、拿到叉子然后吃面,之后下一个线程也是如此
    threading.Thread(target=eat1, args=(zhangsan,)).start()
    threading.Thread(target=eat2, args=(lisi,)).start()
"""
输出:
zhangsan获取面条 Sat Sep 28 12:34:28 2019
zhangsan获取叉子 Sat Sep 28 12:34:28 2019
zhangsan吃面了 Sat Sep 28 12:34:28 2019
lisi获取叉子 Sat Sep 28 12:34:33 2019
lisi获取面条 Sat Sep 28 12:34:33 2019
lisi吃面了 Sat Sep 28 12:34:33 2019
"""

递归锁就是用来解决死锁问题的,注意的是:

  • 在只有一个线程时,递归锁不起作用
  • 对个线程,如果一个线程拿到锁了(acquire),其它的就拿不到了

  RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次acquire。直到一个线程所有的acquire都被release,其他的线程才能获得资源。

(三)threading.Semaphore 信号量

同步锁允许在某一时刻一个线程来操作资源,但是信号量在某一时刻允许一定数量的线程操作资源。

信号量同步是基于内部计数器,每调用一次acquire(),计数器减1;每调用一次release(),计数器加1;当计数器为0时,acquire()调用被阻塞,直到有线程调用release()。

值得注意的是信号量也是锁,只是在内部加了一个计算器。

在火车站内需要对顾客进行检查,假设每次只能检查4个人,然后检查完毕的人再换下个顾客来检查,这里每次检查4个人就是信号量可以并发处理4个线程。

import threading
import time

def check_person(i,sem):
    sem.acquire()
    time.sleep(1)
    print(i+1,time.ctime())
    sem.release()


if __name__ == __main__:

    sem = threading.Semaphore(4) #同时四个人进行检查

    for i in range(50):
        t = threading.Thread(target=check_person,args=(i,sem))
        t.start()
"""
输出:
1 Sat Sep 28 12:55:01 2019
2 Sat Sep 28 12:55:01 2019
3 Sat Sep 28 12:55:01 2019
4 Sat Sep 28 12:55:01 2019
5 Sat Sep 28 12:55:02 2019
6 Sat Sep 28 12:55:02 2019
8 Sat Sep 28 12:55:02 2019
7 Sat Sep 28 12:55:02 2019
...
"""

可以明显看出,每次都是4个线程一起并发,相当于4个人检查完毕。

(四)threading.Event 事件

1、Event事件概述

  • 这是线程之间通信的最简单机制之一:一个线程发出事件信号,其他线程等待事件。
  • 事件对象管理一个内部标志,该标志可以通过方法设置为true,并通过 set()方法设置为false clear() 。wait()方法将阻塞直到标志为真。
  • 所以事件对象的机制就是:全局定义了一个Flag,如果Flag值为 False,当程序执行event.wait()方法时就会阻塞,如果Flag值为True时,程序执行event.wait()方法时不会阻塞继续执行。

2、常用属性、方法

  • is_set

当且仅当内部标志Flag为True时,才返回True。

  • set

将内部标志Flag设置为True。唤醒所有等待变为真的线程。wait()一旦标志为True的线程将根本不会阻塞。

  • clear

将内部标志Flag重置为False。随后,线程调用 wait()将阻塞,直到set()被调用以再次将内部标志设置为True为止。

  • wait(timeout = None 

阻塞直到内部标志Flag为真。如果内部标志在输入时为True,立即返回。否则,阻塞直到另一个线程调用set()将该标志设置为True,或者直到发生可选的超时为止。

如果存在timeout参数而不是timeout参数None,则它应该是一个浮点数,以秒为单位(或几分之一)指定操作的超时时间。

当且仅当内部标志在等待调用之前或等待开始之后设置为True时,此方法才返回True,因此它将始终返回True,除非给出了超时且操作超时。

3、事件的使用

以汽车过红绿灯为例,当为红灯时,汽车不能通过十字路,当为绿灯时,汽车是可以通过十字路:

import threading
import time

def cars(e,i):
    if not e.is_set():
        print("%s车等待在十字路口"%i)
        e.wait()  #阻塞,直到flag变成True
    else:
        print("%s车通过了十字路口"%i)

def light(e):
    while True:
        if e.is_set():#绿灯
            e.clear() #将flag改为False,进程阻塞,
            print("红灯亮了")
        else: #默认走else,因为默认flag是False
            e.set() #将flag改为True,此时执行car进程,绿灯车通过
            print("绿灯亮了")
            time.sleep(3) #wait没有阻塞,汽车通行

if __name__ == __main__:
    e = threading.Event() #默认为False,红灯亮
    t = threading.Thread(target=light,args=(e,)) #红绿灯线程
    t.start() #启动红绿灯线程

    car_list = []
    for i in range(10): #10辆车过红绿灯
        time.sleep(1)
        car = threading.Thread(target=cars,args=(e,i,))
        car.start()
        car_list.append(car)
    [car.join() for car in car_list]
"""
输出:
绿灯亮了
0车通过了十字路口
1车通过了十字路口
红灯亮了
绿灯亮了
2车通过了十字路口
3车通过了十字路口
4车通过了十字路口
红灯亮了
绿灯亮了
5车通过了十字路口
6车通过了十字路口
7车通过了十字路口
红灯亮了
绿灯亮了
8车通过了十字路口
9车通过了十字路口
...
"""

思路:

  • 红绿灯线程控制Flag的状态
  • 汽车线程,模拟出10辆汽车,也就是10个线程
  • 判断e.is_set()的状态,如果为True,红绿灯线程进行改变,汽车线程根据e.is_set()的状态判断是否阻塞(e.wait())

七、全局解释器锁-GIL

(一)GIL概述

1、为什么会有GIL?

  现在电脑大部分都是多核cpu,python为了高效的利用多核这个特性,就开始使用多线程,虽然这样是很美好的,但是这样难免就会造成数据完整性以及状态同步的问题,比如同一时刻多个线程向进程获取全局资源并操作,这样使得全局资源结果混乱。为了解决数据完整性以及状态同步的问题,就出现了GIL。

2、GIL有什么样的影响?

  首先必须明确GIL是作用在线程上的,也就是说在同一个进程中的多个线程在某一时刻只允许一个线程获取到GIL,此时这个线程才能工作,其它线程处于等待中。这样明显会降低效率。

  但是有一点值得注意的,那就是计算密集型和I/O密集型操作,如果多线程进行的是I/O密集型操作的话,当碰到I/O操作时,该线程就释放GIL,其它线程进行操作。这样也能缓和GIL带来的负面影响。如果是计算密集型任务就不太适合多线程操作了。

(二)如何避免GIL的影响?

1、GIL运行的步骤

因为GIL是针对线程的,所以在每一个进程外都会有一个GIL锁,那么当一个进程内是多线程时,GIL是如何工作的:

  • python代码执行就需要交给解释器,此时解释器就会加锁,当执行多线程时会抢占式向进程获取锁
  • 当该线程遇到I/O操作或者当前线程运行时间超过预定时间就会释放锁,并且自己处于睡眠状态
  • 其它的线程会再次向进程抢占式申请锁,重复上次线程的行为

2、如何避免GIL的影响?

  既然GIL每次只能允许一个线程进行操作,那么可以开启多个进程这样每一个进程都有一把GIL,也就是每一个进程都会有一个线程进行工作,也是可以弥补一下GIL带来的不足,虽然不错但是进程的开销太大,最重要的是线程之间的通讯也是一个很大的问题,不同进程之间线程的通讯只能依靠进程通讯,这当然比不上同一个进程中多线程之间共享数据来的简单。

  其次,协程是微线程,它将线程的作用又加强了,协程的切换时间比线程又有所改善,这在一定程度上也是可以弥补GIL的不足。

所以,总结一下:

  • 针对计算密集型任务使用多进程较好
  • 针对I/o密集型任务使用多线程较好(当有I/o时释放GIL锁)

参考:

https://docs.python.org/3.6/library/threading.html#thread-objects

https://docs.python.org/3.6/library/queue.html

https://www.cnblogs.com/yuanchenqi/articles/6248025.html

 

以上是关于并发编程之线程的主要内容,如果未能解决你的问题,请参考以下文章

转:Java并发编程之十九:并发新特性—Executor框架与线程池(含代码)

并发编程之线程

Java并发编程之美之并发编程线程基础

转:Java并发编程之二:线程中断(含代码)

并发编程补充知识之标准线程池

Java并发编程之线程安全线程通信