《Python编程》笔记(四)

并行系统工具(一)

  • 实际上,现代CPU的绝大部分运算能力常常处于空闲状态。更快的速度有助于提高高峰时期的处理速度,但其能力中很大一部分常常得不到利用。
  • Python中,同时运行多个任务有两种基本的做法:进程分支和线程派生。
  • os.fork调用会为调用程序创建一个进程副本,会为每个副本返回不同的值:在子进程中返回0,而在父进程中返回新子进程ID。
  • Cygwin是一个免费的开源系统,为Windows提供所有类Unix功能。
  • 在类Unix平台下,分支通常是启动独立运行的程序的基础,这些独立程序与执行fork调用的程序是不同的。
  • os.execlp调用可以用一个全新的程序代替(即执行覆盖)当前进程中正在运行的程序。该调用成功后,不会返回(因为没有返回的目标),如果失败,则可以使用assert False, 'error occured'来捕获。例如os.execlp('python3', 'python3', 'spam.py')会启动spam.py程序覆盖当前进程正在运行的程序。
  • os.forkos.execlp组合意味着开始一个新进程,并在其中运行一个新的程序。即启动与原有程序并行运行的新程序。
  • Python中os.exec*调用变体:

    • os.execv(program, commandlinequence):需要传入可执行程序的名称,以及用来运行程序的命令行参数字符串元组或列表
    • os.execl(program, cmdarg1, cmdarg2, ... cmdargN):需要传入可执行程序的名称以及一个或多个单个的函数参数形式传入的命令行参数
    • os.execlp
    • os.execvp:表示使用系统的 PATH来定位可执行程序
    • os.execle
    • os.exeve:表示将在最后添加一个参数,参数是字典格式,包含的是发送给shell的环境变量
    • os.execvpe
    • os.execlpe:你懂的啦
  • Python中使用线程的一些好处:

    • 性能改善
    • 简单易用
    • 共享全局内存
    • 可移植性更好
  • Python中线程的注意点:
    • 函数调用和程序:线程通常运行程序内的函数,但也可以启动新的程序
    • 线程同步优化和队列:由于共享全局内存中的对象和名称,所以既提供了一种通讯机制,但也得注意多种操作同步优化的处理。通常可以构建一个或多个生产者线程,将数据添加到队列中,再提供消费者线程,从队列中取出数据并处理。
    • 全局解释器锁(GIL):Python中线程实现意味着在Python虚拟机中,同一时间只有一个线程运行。因此,Python线程目前还不能利用多核CPU。但是可以借助多进程来实现利用多核CPU的功能,并且不是非常复杂。
  • _thread模块:

    • _thread.start_new_thread(function, (arg1, arg2))即可启动新的线程
    • 为了使代码更健壮,带有线程的程序必须控制对共享全局对象的访问权,保证同一时间内只能有一个线程在使用它们
    • Python确保任何时间点只有一个线程持有锁,如果在持有起见其他线程请求获得锁,那么这些请求将一直被阻塞,直到释放锁。

      mutex = _thread.allocate_lock()
      
      def counter(id, count):
       for i in range(count):
           time.sleep(0.5)
           mutex.acquire()
           print('{} => {}'.format(id, i))
           mutex.release()
      
      for i in range(5):
       _thread.start_new_thread(counter, (i, 5))
      
      time.sleep(6)
    • 使用with语句使用线程锁的上下文管理器,这样即使出现异常也能保证锁得到释放

      stdoutmutex = thread.allocate_lock()
      exitmutexes = [thread.allocate_lock() for i in range(5)]
      
      def counter(mid, cnt, mutex):
       for i in range(cnt):
           time.sleep(1 / (1+mid))
           with mutex:
               print('[{}] => {}'.format(mid, i))
       exitmutexes[mid].acquire()
      
      
      for i in range(5):
       thread.start_new_thread(counter, (i, 5, stdoutmutex))
      
      while not all(mutex.locked() for mutex in exitmutexes):pass
      
      print('Main thread exiting...')
  • threading模块

    • 基于对象和类的较高层面的接口。
    • 常规的用法示例:
      class MyThread(threading.Thread):
       def __init__(self, mid, cnt, mutex):
           self.mid = mid
           self.cnt = cnt
           self.mutex = mutex
           threading.Thread.__init__(self)
      
       def run(self):
           for i in range(self.cnt):
               with self.mutex:
                   print('[{}] => {}'.format(self.mid, i))
      
      
      stdoutMutex = threading.Lock()
      threads = []
      
      for i in range(10):
       thread = MyThread(i, 100, stdoutMutex)
       thread.start()
       threads.append(thread)
      
      for thr in threads:
       thr.join()
  • 线程需要通过它们对任何可能在同一进程中的线程间共享的项(对象和命名空间)的更改进行同步化。这些对象包括:

    • 内存中的可变对象(生命周期跨越线程的持续时长)
    • 全局作用域的名称(线程函数和类之外的可更改的变量)
    • 模块中的内容(每个模块仅在系统的模块表中拥有一份共享的副本)
  • threading模块中还包括较高层面上的可以用于共享对象访问同步化的对象(比如Semaphore, Condition, Event)。

  • 使用队列进行线程间通信的程序是线程安全的,并且可以避免因为线程间传递的数据而对自身的锁进行处理。
  • threading模块中,如果任何一个派生线程仍然在运行中,程序不会退出,除非那些线程被设定为守护线程。整个程序仅在只剩下守护线程的情况下才退出。设置为daemon的线程无需join
  • 生产者消费者多线程模型:
num_consumers = 2
num_producers = 4

# 生产者可以存入的消息数量
num_messages = 4

# sys.stdout锁
safe_print = threading.Lock()
data = queue.Queue()


def producer(idnum, data):
    for num in range(num_messages):
        time.sleep(idnum)
        data.put('[producer id={}, count={}]'.format(idnum, num))


def consumer(idnum, data):
    while True:
        time.sleep(0.1)
        try:
            d = data.get(block=False)
        except queue.Empty:
            pass
        else:
            with safe_print:
                print('consumer', idnum, 'got =>', d)

threads = []
for i in range(num_consumers):
    thr = threading.Thread(target=consumer, args=(i, data))
    # 设置为daemon后才能退出
    thr.daemon = True
    thr.start()

for i in range(num_producers):
    thr = threading.Thread(target=producer, args=(i, data))
    threads.append(thr)
    thr.start()

[thr.join() for thr in threads]
  • 一般只要同时更新有可能发生,就应当使用锁来同步化线程,而不是依赖当前版本线程功能中的认为设定。
  • 可以通过sys.setcheckinterval(N)来设置解释器检查线程切换和信号处理器等的频率。设置较高的值表示切换频率低,降低因线程频繁切换而造成的开销,但是对事件的应答能力会下降;设置较低的值会使得线程对事件的应答能力提高,相应地也会增加线程切换开销。
  • 因为Python使用GIL同步化线程对虚拟机访问的方式,整条语句并非线程安全的,只有每条字节码才是。某些操作是线程安全的(原子性,免受中断),而且不需要使用锁或者队列来避免更新问题;但是为了保险起见,还是要用锁来控制全局和共享对象的所有访问相对较好。
  • 长时间运行的C扩展函数应该在其开始时释放GIL锁,而在其退出且继续运行Python代码重新获取锁。即便Python线程中的Python代码由于GIL同步化无法真正在时间上重叠,但是C语言部分可以。
  • sys.exit(N)函数调用会抛出SystemExit异常,因此可以捕获异常,来拦截程序过早退出,并执行清理活动。
  • 在Unix下的分支子进程中,通常调用os._exit函数,而非sys.exit;线程可以使用_thread.exit调用退出。
  • os._exit调用后,进程会立即退出,不会抛出可以捕获的异常;所以通常应用于子进程上,而非用于整个程序的退出。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值