并发编程

336 阅读4分钟

《深入理解计算机系统(第三版)》

三种基本的构造并发程序的方法:

  • 进程。进程有独立的虚拟地址空间,想要和其他流通信,控制流必须使用某种显式的进程间通信(IPC)机制
  • I/O 多路复用。在这种形式的并发编程中,应用程序在一个进程的上下文中显式的调度它们自己的逻辑流。逻辑流被模型化为状态机,数据达到文件描述符后,主程序显式的从一个状态转换到另一个状态。因为程序是一个单独的进程,所以所有的流都共享同一个地址空间。
  • 线程。线程是一个运行在一个单一进程上下文中的逻辑流,由内核进行调度。你可以把线程看成是其他两种方式的混合体,像进程流一样由内核进行调度,而像I/O多路复用流一样共享同一个虚拟地址空间

I/O多路复用技术的优劣

I/O多路复用可以用做并发事件驱动程序的基础,在事件驱动程序中,某些时间会导致流向前推进。一般的思路是将逻辑流模型化为状态机。不严格地说,一个状态机就是一组装填,输入事件和转移,其中转移是将状态和输入事件映射到状态。事件驱动设计的一个优点是,他比基于进程的设计给了程序员更多的对程序行为的控制。

另一个优点是,一个基于I/O多路复用的事件驱动服务器是运行在单一进程上下文中的,因此每个逻辑流都能访问该进程的全部地址空间。这使得在流之间共享数据变得很容易。一个作为单个进程运行相关的优点是,你可以利用熟悉的调试工具,例如GDB,来调试你的并发服务器,就相对顺序程序那样。

最后,事件驱动设计常常比基于进程的设计要高效得多,因为他们不需要进程上下文切换来调度新的流。

事件驱动设计一个明显的缺点就是编码复杂。

基于事件的设计另一个重要的缺点是他们不能充分利用多核处理器。

基于线程的并发编程

线程就是运行在进程上下文中的逻辑流。基于线程的逻辑流结合了基于进程和基于I/O多路复用的流的特性。同进程一样,线程有内核自动调度,并且内核通过了一个整数ID来识别线程。统计与I/O多路复用的流一样,多个线程运行在单一进程的上下文中,因此共享这个进程虚拟空间的所有内容,包括他的代码、数据、堆、共享库和打开的文件。

  • 线程间通信方式:共享变量, Queue

  • 线程间同步方式:

    • Lock,Rlock,
    • 条件变量condition:用于复杂的线程间同步
    • 信号量:控制线程并发数量

条件变量示例:

import threading

class XiaoAi(threading.Thread):
    def __init__(self, cond):
        super().__init__(name='小爱')
        self.cond = cond

    def run(self):
        with self.cond:
            self.cond.wait()
            print("{} : 在 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 好啊 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 君住长江尾 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 共饮长江水 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 此恨何时已 ".format(self.name))
            self.cond.notify()

            self.cond.wait()
            print("{} : 定不负相思 ".format(self.name))
            self.cond.notify()


class TianMao(threading.Thread):
    def __init__(self, cond):
        super().__init__(name='天猫精灵')
        self.cond = cond

    def run(self):
        with self.cond:
            print("{} : 小爱同学 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 我们来对古诗吧 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 我住产长江头 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 日日思君不见君 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 此水几时休 ".format(self.name))
            self.cond.notify()
            self.cond.wait()

            print("{} : 只愿君心似我心 ".format(self.name))
            self.cond.notify()
            self.cond.wait()


if __name__ == '__main__':
    cond = threading.Condition()
    xiaoai = XiaoAi(cond)
    tianmao = TianMao(cond)

    # 启动顺序很重要
    xiaoai.start()
    tianmao.start()
天猫精灵 : 小爱同学 
小爱 : 在 
天猫精灵 : 我们来对古诗吧 
小爱 : 好啊 
天猫精灵 : 我住产长江头 
小爱 : 君住长江尾 
天猫精灵 : 日日思君不见君 
小爱 : 共饮长江水 
天猫精灵 : 此水几时休 
小爱 : 此恨何时已 
天猫精灵 : 只愿君心似我心 
小爱 : 定不负相思 

信号量(semaphore)

# Semaphore 是用于控制进入数量的锁
import threading

import time


class HtmlSpider(threading.Thread):
    def __init__(self, url, sem):
        super().__init__()
        self.url = url
        self.sem = sem

    def run(self):
        time.sleep(2)
        print('got html text success')
        self.sem.release()


class UrlProducer(threading.Thread):
    def __init__(self, sem):
        super().__init__()
        self.sem = sem

    def run(self):
        for i in range(20):
            self.sem.acquire()
            html_thread = HtmlSpider('https://baidu.com/{}'.format(i), sem)
            html_thread.start()


if __name__ == '__main__':
    # 一次允许3个并发
    sem = threading.Semaphore(3)
    url_producer = UrlProducer(sem)
    url_producer.start()

线程池 concurrent

为什么要用线程池:

  • 主线程中年可以获取某一个线程的状态或者某一个任务的状态,以及返回值。
  • 当一个线程完成的时候,主线程能立即知道
  • futures 能让多线程和多进程编码接口一致
import time
from concurrent.futures import as_completed
from concurrent.futures.thread import ThreadPoolExecutor


def get_html(times):
    time.sleep(times)
    print('get page {} success'.format(times))
    return times

excutor = ThreadPoolExecutor(max_workers=2)


# 通过submit函数提交执行的函数到线程池中,submit是非阻塞的,会立即返回
task1=excutor.submit(get_html,(3))
task2=excutor.submit(get_html,(2))

# done 用于判定某个任务是否完成
print(task1.done())
time.sleep(3)
print(task1.done())
# result 是阻塞的方法,得到执行的返回结果
print(task1.result())

结果

False
get page 2 success
get page 3 success
True
3