day27のスレッド同期

day27のスレッド同期

今日エグゼクティブサマリー

  1. ミューテックス(ミューテックス)
  2. デッドロック
  3. セマフォ(セマフォ)
  4. グローバルインタプリタロック(GIL)
  5. 同期および非同期
  6. コルーチン

昨日のレビュー

  1. プロセスプール
  2. マルチスレッド

今日の詳細な内容

ミューテックス(ミューテックス)

複数のプロセスが同時にあなたが何も操作を行わない場合は、同じ共有データ(例えば百万エラーとして)時間を変更する混乱データ処理の原因となります場合我々は、過去に見てきました。同様のミスを避けるために、我々は、同期制御を行います。

スレッドの同期は、リソースの競合、最も簡単なミューテックス同期メカニズムを使用することで、複数のスレッドへの安全なアクセスを保証します。

ミューテックス複数のスレッド(原子)のデータの正確さを保証するように、唯一のスレッドのデータを変更することを確実にします。

原子性は、スレッドのいずれかが、すべてのコード実行、または単純に実行しないことを意味します。各スレッドは不可分であると次のスレッドの実装に、このスレッドが実行されていない実行することはできません。

1571358424076

状態の集合を紹介するリソースとしてミューテックス:ロックとアンロック。

スレッドが公開データを変更するときは、最初にロックされました。以下のためのリソースの状態ロックは、他のスレッドは、それを変更することはできません。などのリソース、状態のリソースを解放するために、このスレッドを知っているロック解除、他のスレッドが再びリソースをロックすることができます。ミューテックスは、データの複数のスレッドの安全性と正確性を確保するためにデータを変更する唯一のスレッドの動作を保証します。

1571312775660

スレッドモジュールが定義されてロッククラスを、あなたは簡単にロックを処理することができます。

ミューテックスは、主に3つの方法で使用されます。

# 创建锁
mutex = threading.Lock()
# 锁定
mutex.acquire()
# 释放,解锁
mutex.release()

私たちは、百万疑問を解決するためにミューテックスを使用することができます。

import threading
import time
num = 0
def add_num1():
    global num
    mutex.acquire()
    for i in range(1000000):
        num += 1
    mutex.release()
    print('num in add_num1', num)
def add_num2():
    global num
    mutex.acquire()
    for j in range(1000000):
        num += 1
    mutex.release()
    print('num in add_num2', num)
mutex = threading.Lock()
th1 = threading.Thread(target=add_num1)
th2 = threading.Thread(target=add_num2)
th1.start()
th2.start()
time.sleep(1)
print(num)

上記のコードの結果が出力されます。

num in add_num1 1000000
num in add_num2 2000000
2000000

スレッドの呼び出しがオブジェクトをロックするacquire()方法がロックを取得するときに、ロックを入力するロック状態。一つだけのスレッドがロックを取得することができますので。この時点でロックを取得するために別のスレッド2回の試行を中に達成されない場合、同期ブロッキング状態。ブロックされた状態は、スレッドがロックを呼び出したときまで継続するrelease()にロック後の方法ロック解除状態。スレッドスケジューラからなる同期ブロッキングロックを取得する状態での選択スレッド、スレッドは(実行)状態に走るようになります。場合は、スレッドロック、他のスレッドは外側だけ待つことができます。

1571314495634

デッドロック

デッドロックは、私たちが将来的に回避しようとしなければならない、エラーです。

原因デッドロックはよく理解されている:私たちは部屋に私のキーをロック想像してみてください。私たちは家に入るためのキーを取得する必要がありますが、私たちだけが家を取得するためのキーを取得します。2つのプロセスがデッドロックを構成すると、家の中にこのようにキーを取得するには、二つのことを行うことはできません。

別の例は、ジョー・スミスジョン・ドウは、ジョー・スミスジョン・ドウを予約したい描きたいです。ジョー・スミスとジョン・ドウが過去にお互いを知らない、お互いを信頼していない。しかし、彼らは彼女が出て支払うことではない他のを望んでいるものを手に入れるために、他の後に自分のことを心配しています。だから、ジョー・スミスは彼らの手に本を手渡すためにジョン・ドウの絵画を取得するには、要求された、とジョン・ドウの要件、彼だけに絵にジョンの数を取得します。このように、彼らはそこで硬い、取引を進めることができません。

1571315317783

私たちは今、デュアルスレッドのデッドロックの例を記述することができます。

import threading
import time

class MyThread1(threading.Thread):
    def run(self):
        mutexA.acquire()
        time.sleep(1)
        print(self.name + '---do1---up---')
        mutexB.acquire()
        print(self.name + '---do1---down---')
        mutexA.release()

class MyThread2(threading.Thread):
    def run(self):
        mutexB.acquire()
        time.sleep(1)
        print(self.name + '---do2---up---')
        mutexA.acquire()
        print(self.name + '---do2---down---')
        mutexB.release()

mutexA = threading.Lock()
mutexB = threading.Lock()
th1 = MyThread1()
th2 = MyThread2()
th1.start()
th2.start()

次のように続行不可能であろう2行のコードの上記のコードを出力した後に、その出力の内容は次のとおりです。

Thread-2---do2---up---
Thread-1---do1---up---

マルチスレッドでは、問題の大部分は同時に生じ、複数のロックを取得デッドロックスレッドによるものです。スレッドは、最初に取得し、その後、第2ロックが発生し得る場合に遮断するときは、スレッドが実行する他のスレッドを詰まらせる可能性がある、プログラム全体で得られるアニメーションを停止しました。

1571317154981

セマフォ(セマフォ)

アクションミューテックスセマフォなど。違いは、データを使用するミューテックス一つだけスレッドを制限しながら、信号は同時に実行するスレッドの数の量を制御することができる、ということです。

セマフォは、時間ロック内のスレッドの数を制御するために使用されています。セマフォは、同時の数を制御するために使用されます。

使用场景举例:在读写文件的时候,一般只有一个线程再写,而读可以有多个线程同时进行。如果需要限制同时读文件的线程数目,就要用到信号量。因为如果使用互斥锁,同一时刻将只能有一个线程读取文件。

信号量用来将信号量限制在能够最大化效率的同时,又不会让计算机因过载而崩溃的数目下。

如果我们不做任何限制,下面代码中的100个线程将同时运行:

import time
import threading

def foo():
    time.sleep(1)
    print('ok', time.ctime())
    
for i in range(100):
    th = threading.Thread(target=foo)
    th.start()

上面的代码在等待大约1秒后几乎同时打印出来。100个程序虽然没有搞垮计算机,但可想而知,这个过程耗费了大量的内存。如果我们的任务更多,程序更复杂,计算机就未必吃得消了。

这时,我们就可以通过信号量来控制同时运行的线程数目:

import time
import threading

def foo():
    sem.acquire()
    time.sleep(1)
    print('ok', time.ctime())
    sem.release()
    
sem = threading.Semaphore(5)
for i in range(100):
    th = threading.Thread(target=foo)
    th.start()

上面的代码每隔一秒会打印出五条内容,明显降低了计算机的压力。

全局解释器锁(GIL)

全局解释器锁(Global Interpreter Lock,GIL)是CPython独有的锁。引入它的初衷是为了保证数据安全而牺牲效率。但GIL是一柄双刃剑,它带来优势的同时也带来很多问题。

GIL应该是Python最为人诟病的一个特性了:正是因为GIL的存在,Python中的单线程只能使用CPU的一个核心,即便使用的是多核处理器。这极大限制了Python多线程的应用场景。很多计算集中的代码只能用多进程来实现。而多进程因为数据不共享,还要用到队列来实现进程间的通信,这又增加了代码的复杂度。

要了解Python的全局解释器锁,我们首先要了解Python文件的执行过程:

  1. 操作系统将应用程序从硬盘加载到内存。Python文件运行时,会在内存开辟一个进程空间,将Python解释器以及py文件加载进去,解释器运行py文件
  2. Python解释器编译Python代码一共分成两个步骤:首先将代码编译成C的字节码,然后虚拟机将C的字节码编译成机器码。随后,操作系统会将机器码交给CPU去执行
  3. py文件中有一个主线程,主线程做的就是这个过程。如果开多线程,每个线程也都要进行上述的过程(Python --> C --> 机器码)

假设我们现在有三个线程,那么这三个线程在计算机中理想的,效率最高的运行情况应该是这样的:

我们有三个线程,就得到三组机器码。把他们交给操作系统。操作系统分配三个CPU分别执行这三组机器码。它们同时执行,最大限度地提高效率。

1571358777319

然而,CPython却不是这样执行的。CPython的多线程不能使用多核

CPython在所有线程进入解释器前加了一个全局解释器锁(Global Interpreter Lock,GIL)。这个锁是互斥锁,是加在解释器上的。这就导致同一时间只有一个线程在执行,也就无法使用多核。而且我们没有任何办法能解掉这个锁。

1571361144138

既然GIL锁的负面效应这么大,为什么还要在Python的解释器上加一个GIL锁呢?

这是因为Python最开始编写的时候,CPU还只有一个核心。那时的人们没能预料到未来会有多核CPU出线的可能,也就没有专门针对多核预留优化的空间。对于单核CPU而言,加一个锁既保证了数据安全,又让编写Python解释器更加容易。

而现在,虽然人们意识到多线程使用多核处理器的重要性,但是因为解释器内部的管理全部是针对单线程写的。牵一发而动全身,要想去掉GIL锁,就要从根本上修改Python解释器的代码。

然而,经过这么多年的发展,Python已经是一个非常庞大的系统工程,修改代码谈何容易。再加上Python是开源免费的编程语言,Python组织的盈利能力十分有限。而修改源代码又是一项需要消耗巨大资源的事情,所以这么多年来,GIL一直没能被取消掉。

读者或许还会问,既然只有CPython有GIL锁,我们可不可以换一种解释器,比如不使用CPython,而是使用PyPy呢?

答案也是不合适的。首先,官方更推荐使用CPython。因为Python本就是个面向对象的编程语言,效率相对不高。不管是使用Java编写的JPython,还是使用Python编写的PyPy,其运行效率都要低于使用C语言编写的CPython。而且,其他解释器,比如PyPy,规则和漏洞都很多,转成PyPy还要重写代码。我们为了追求提高多核效率,却牺牲了运行效率并且带来了漏洞,还要重写代码,似乎有些得不偿失了。

那么Python就不能使用多核了吗?

也不是的。虽然多线程无法是用多核,多进程还是能用到多核的。如果我们进行一些计算密集型的任务,就可以使用多进程,使用多个CPU来提高运算效率。不过多进程的缺点是会开辟较多的内存空间,开销比较大。

还有一个问题,Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么还要用到互斥锁呢?

我们已经知道,使用锁的目的是为了保护共享的数据,确保同一时刻,只能有一个线程修改共享的数据,避免发生混乱。

但是我们会有不同类型的数据,这样我们就要使用不同的锁来分别对它们进行保护。

GIL和互斥锁是两把锁,保护的数据是不一样的。GIL是解释器级别的,保护的是解释器级别的数据,比如垃圾回收的数据;互斥锁则是保护程序员自己开发的应用程序数据。GIL负责保护宏观数据,互斥锁负责保护微观具体的数据。

这就好比一间屋子。屋子除了有一个正门锁,在屋子内部还有很多小房间也有各自的门和锁。正门锁,只要是我们的家人都可以进来,而坏人却都进不来(理想中的门锁)。但是对于家人来说,也会有一些私密的空间。比如我们自己的房间是不想让爸妈进来看的,于是我们给自己的房间也上了一把锁。正门锁是为了防止坏人进来,我们房间的门锁是为了保留一些小秘密。我们不能说,既然有了正门锁,就没有必要给小房间上锁了。正门锁和小房间的锁保护的东西不同,但都有必要。

在这个例子里面,正门锁就是GIL,小房间的锁就是我们创建的互斥锁。

1571365477590

同步和异步

同步调用:确定调用顺序。多个任务依此按照顺序执行,每次只执行一个任务,直到该任务结束,才会开始下一个任务。如果任务未结束,后面的任务将处于等待状态,而不会提前执行。

异步调用:不确定调用顺序。一次提交多个任务,这些任务同时处于就绪状态,共同执行。如果有一个任务没有结束,并不会影响其他任务的进展。

例如,我们现在要让三个人分配任务,让他们每人写一本书:

  • 同步:先告知第一个人写书,等他写完之后,告知第二个人。然后等第二个人写完之后,在告知第三个人。等第三个人写完,任务结束。如果前面有一个人耽误了日期,后面的人也会一直等待下去。
  • 异步:把三个任务告知三个人,让他们每人写一本书。三个人同时开始写书。即便他们中有一个人没能按时完成,另两个人的进度不会受影响。

1571367702812

再如,烧水泡茶的例子。我们要请客人喝茶,首先一共要分洗水壶、烧开水、洗茶壶、洗茶杯、拿茶叶和泡茶等六个步骤。其中,洗水壶要1分钟;烧开水要15分钟;洗茶壶要1分钟;洗茶杯要2分钟;拿茶叶要1分钟;泡茶要3分钟。

  • 同步:按照顺序,先洗水壶,然后烧开水。水烧开之后洗茶壶,再洗茶杯,拿茶叶,最后泡茶。总共花费了23分钟
  • 异步:先洗水壶。然后在烧开水的同时洗茶壶,洗茶杯,拿茶叶。最后等水烧开,泡茶。总共要花费19分钟

1571368852354

同步(sync)

同步意味着各个任务有序执行,它们处在统一的时间轴中。有两个典型的场景可能会用到同步:

  1. 任务的逻辑十分清晰,必须要先执行某一个任务。如果第一个任务发生了阻塞,会一直等待,直到这个任务完成,再执行第二个任务。这样才能协同步调,按照预定逻辑的先后次序进行,避免因果倒置,逻辑混乱,产生异常
  2. 一个任务的完成需要依赖另外一个任务。只有等待被依赖的任务完成后,依赖的任务才能开始执行。这时一种可靠的任务序列

我们可以通过设置互斥锁的方式来让多个线程同步有序执行:

import threading
import time

class Task1(threading.Thread):
    def run(self):
        while Ture:
            mutex1.acquire()
            print('Task1')
            time.sleep(1)
            mutex2.release()
            
class Task2(threading.Thread):
    def run(self):
        while True:
            mutex2.acquire()
            print('Task2')
            time.sleep(1)
            mutex3.release()
        
class Task3(threading.Thread):
    def run(self):
        while True:
            mutex3.acquire()
            print('Task3')
            time.sleep(1)
            mutex1.release()

th1 = Task1()
th2 = Task2()
th3 = Task3()

mutex1 = threading.Lock()
mutex2 = threading.Lock()
mutex3 = threading.Lock()

mutex2.acquire()
mutex3.acquire()

th1.start()
th2.start()
th3.start()

上述代码会依此循环输出Task1,Task2,Task3,每秒输出一个。

上面的例子中,三个线程之间相互关联,耦合性极强。这就会带来一个问题:如果我们修改其中一个任务函数,就有可能会影响到另外两个函数的顺利运行。这给日后的开发扩展带来很多不确定,埋下了隐患。

这时,我们就需要用到生产者消费者模式来解决这个问题。

在线程世界中,生产者就是生产数据的线程,消费者就是消费数据的线程(就好比做包子和吃包子)。经常会出现生产数据的速度大于消费数据的速度,或者生产速度跟不上消费速度的情况。

这就需要我们在生产者和消费者之间放一个容器,也就是缓冲区,来解决生产者和消费者强耦合的问题。生产者统一往容器中放入数据,消费者统一从容器中提取数据。但是生产者和消费者之间没有直接的数据交换。

1571382317973

生产者和消费者彼此间不直接通讯,他们之间通过容器进行数据交互。这个容器,就可以是阻塞队列。

Python的quene模块实现三种类型的队列可以实现线程同步

  • FIFO(先入先出)队列,Queue
  • LIFO(后入先出)栈,LifoQueue
  • 优先级队列,PriorityQueue

这三种队列的区别在于检索条目的顺序不同:

  • Queue类会按照先进先出的顺序检索条目
  • LifoQueue按照后进先出的顺序检索,类似于一个栈
  • PriorityQueue则是通过使用heapq模块使得条目有序,最小值的条目最先被检索到

这些队列都实现了锁原语(可以理解为原子操作,要么不做,要么做完),能够在多线程中直接使用。

有了queue模块的Queue类,我们就可以构造一个队列对象。其用法为:

q = queque.Queue(maxsize=0)

其参数可以不写,默认为0。其参数表示队列的最大条目,可以用来限制内存的使用。当参数为0或负数时,队列大小为无穷大。

一旦队列被占满,插入操作将被阻塞,直到队列中存在空闲空间。

这样我们就可以根据生产者消费者模式写出以下代码:

import threading
import queue
import time

class Pro(threading.Thread):
    def run(self):
        count = 0
        while True:
            if q.qsize() < 100:
                for i in range(3):
                    count += 1
                    msg = f'生产商品{count}'
                    q.put(count)
                    print(msg)
            time.sleep(2)
            
class Con(threading.Thread):
    def run(self):
        while True:
            if q.qsize() > 2:
                for i in range(2):
                    msg = f'消费商品{q.get()}'
                    print(msg)
            time.sleep(3)
            
q = queue.Queue()
for i in range(2):
    pr = Pro()
    pr.start()
for j in range(3):
    co = Con()
    co.start()

上面的代码运行过后,生产与消费交替进行。虽然生产者与消费者的进程数目并不相同,每个进程的生产速度与消费速度也不一致,但程序还是有序地运转着。这就是通过队列容器实现了生产者与消费者间的间接通信,从而降低了耦合性,提高了代码的灵活性。

异步(async)

异步意味着代码执行无序,各个线程拥有独立的时间轴,其目的是追求效率。

处理和调用一个任务之后,不会等待这个任务的处理结果,而是同时处理其他更多的任务。这些任务通过状态、回调等方式将处理结果反馈给调用者。

对于I/O相关的程序来说,异步编程可以大幅度提高任务的执行效率。因为在某个I/O操作的读写过程中,系统可以先去处理其他操作(通常是其他的I/O操作)。

用我们之前的烧水泡茶的例子来讲就是,在烧水的同时,我们可以洗茶壶、洗茶具、找茶叶,而不是等水少开。

异步执行任务是不确定执行顺序的,所以要注意判断当前任务是否适用于异步执行。

我们从前学的多线程或多进程,都是异步的例子:

import threading
import time

def th(num):
    print('线程%s开始执行' % num)
    time.sleep(1)
    print('线程%s执行完毕' % num)
    
for i in range(0, 3)
    th = threading.Thread(target=th, args=(i,))
    th.start()

print('主方法执行完毕')

上面的代码执行的结果为:

线程0开始执行
线程1开始执行
线程2开始执行
主方法执行完毕
线程0执行完毕
线程1执行完毕
线程2执行完毕

前面四行先被打印出来,后面三行在一秒后才被打印。

但是我们发现,主方法要在每个线程结束前就已经结束。而且,线程间没有任何的数据交换,对于复杂些的任务,处理起来就不太适用了。

如果我们使用条件控制,就可以实现进程间的交互。避免出错的同时,最大限度提高效率:

import threading
import time

num = 0

def num_add_1():
    global num
    # 当参数为False时,如果锁不可得,不会阻塞,而是返回False,如果锁可得,返回True
    while True:
        if mutex.acquire(False):
            for i in range(1000000):
                num += 1
            print('线程一执行完毕,此时num的值为:', num)
            mutex.release()
            break
        else:
            print('线程一该干嘛干嘛')
            time.sleep(1)

def num_add_2():
    global num
    while True:
        if mutex.acquire(False):
            for i in range(1000000):
                num += 1
            print('线程二执行完毕,此时num的值为:', num)
            mutex.release()
            break
        else:
            print('线程二该干嘛干嘛')
            time.sleep(1)

mutex = threading.Lock()
th1 = threading.Thread(target=num_add_1)
th2 = threading.Thread(target=num_add_2)
th1.start()
th2.start()

上面的代码运行结果为:

线程二该干嘛干嘛
线程一执行完毕,此时num的值为: 1000000
线程二执行完毕,此时num的值为: 2000000

通过条件控制线程的执行,虽然实现了线程间的通信,但是实现的方式比较复杂,也会增加线程间的耦合性,并不是一种理想的解决方案。

这就要用到回调机制来实现进程将执行结果反馈给调用者:

from multiprocessing import Pool
import random
import time

def download(name):
    for i in range(1, 4):
        print(f'{name}正在下载文件{i}')
        time.sleep(random.randint(1, 3))
    return f'{name}下载完成'

def alarm_user(msg):
    print(msg)

if __name__ == '__main__':
    p = Pool(3)
    for i in range(1, 4):
        p.apply_async(func=download, args=(f'线程{i}',), callback=alarm_user)
    p.close()
    p.join()
    print('程序执行完毕')

上面的代码运行结果为:

线程1正在下载文件1
线程2正在下载文件1
线程3正在下载文件1
线程1正在下载文件2
线程3正在下载文件2
线程1正在下载文件3
线程2正在下载文件2
线程1下载完成
线程3正在下载文件3
线程2正在下载文件3
线程3下载完成
线程2下载完成
程序执行完毕

因为有随机数,所以每次运行的结果可能会不同。

当func执行完毕后,其返回值会传递给回调函数callback。

协程

协程时比线程更小的执行单元,也被称作微线程。

一个线程作为容器,里面可以放置多个协程。

只切换函数调用即可完成协程,从而减少CPU的切换,利于计算密集型的任务

协程自己主动让出CPU。

进程与线程的任务是由操作系统自行切换的,程序员无法控制

プログラム(コード)コルーチンプログラマは、プログラマのスイッチング制御を行うように書くことができるが、製剤化することができると

greenlet

私たちは、コルーチンを実装するためにgreenletモジュールを使用しています。Pythonはこのgreenletを構築していませんが、我々は自分自身をインストールする必要があります。

インストールするには、多くの方法があります。

  1. PyCharmターミナル、次のコマンドを入力します。

    pip install greenlet
    

    1571391393618

  2. メニューバーで発見file- >settings

    1571391468136

    インタプリタのオプションでは、インストールされているすべてのパッケージが表示されます。私たちは、何greenletがない、見ることができます。私たちは、パッケージを追加、右にプラス記号をクリックすることができます

    1571391721143

    Greenletはgreenletパッケージの次の検索を見つけるために検索ボックスに入力します。以下は、ユーザーのパッケージパスにパッケージをチェックして、パッケージをインストールすることに注意してください。

    1571391662391

  3. もちろん、端末が直接導入greenletパッケージを使用することができます。

    1571391895332

パッケージがインストールされているgreenlet後、我々はコルーチンを達成するためにそれを使用することができます。

from greenlet import greenlet
import time
def func_a():
    while True:
        print('---A---')
        gr2.switch()
        time.sleep(1)
    
def func_b():
    while True:
        print('---B---')
        gr1.switch()    # 会从上次暂停的西方开始执行
        time.sleep(1)
    
gr1 = greenlet(func_a)
gr2 = greenlet(func_b)
gr1.switch()    # 执行协程gr1

違いは、タスクコルーチンに切り替えスレッドは、手動で、代わりに自動的にオペレーティングシステムに割り当てられたCPU命令によって実行されることです。

行商

Pythonはまたgreenletモジュール、geventよりも強力です。これは、タスクを自動的に切り替えることができます。

原理はgreenletは、I / O(入力/出力への入力を参照)、このようなネットワークへのアクセスなどの操作を、遭遇したとき、それは自動的に他のgreenletに切り替わることです。I / O操作が完了した後、適切な時期に戻すまで、それは続いています。

gevent出会いモジュールは、I / O操作になることができた場合のみ、プログラムは並行処理の効果を達成するために、スイッチングをタスクします。すべてのプログラムが何のI / O操作がない場合、それは基本的に逐次実行です。

同様に、我々はまた、gevent geventパッケージをインストールする必要があります。

geventパッケージをインストールした後、使用することができます。

import gevent

def func_a():
    while True:
        print('---A---')
        gevent.sleep(1)  # 模拟一个耗时操作
    # gevent中,当一个协程遇到耗时的I/O操作时,会自动将CPU的使用权交给其他协程

def func_b():
    while True:
        print('---B---')
        gevent.sleep(1)  # 遇到耗时操作,转交CPU使用权

gt1 = gevent.spawn(func_a)  # 创建gevent协程对象,此时协程已经开始执行
gt2 = gevent.spawn(func_b)
gt1.join()  # 等待协程执行结束
gt2.join()

AとBが同時に出力され、その後、毎秒、AとBの組の出力されます

おすすめ

転載: www.cnblogs.com/shuoliuchina/p/11700101.html