精通Python——多线程编程学习笔记

在 Python 中,你可以启动一个线程,但却无法停止它。

目录

1 介绍

2 多线程模块

        2.1 Thread                2.2 Thraading                2.3 Queue

3 多线程实践

1)同步原语              2)锁                              3)信号量

4 线程的替代方案


1 介绍

在多线程(multithreaded,MT)编程出现之前,计算机程序的执行是由单个步骤序列组成的,该序列在主机的 CPU 中按照同步顺序执行。让这些独立的任务同时运行,就是多线程编程。多线程本质上是异步的。

使用多线程编程,以及类似 Queue 的共享数据结构,这个编程任务可以规划成几个执行特定函数的线程:

       • UserRequestThread:负责读取客户端输入,该输入可能来自 I/O 通道。程序将创建多个线程,每个客户端一个,客户端的请求将会被放入队列中。
       •  RequestProcessor:该线程负责从队列中获取请求并进行处理,为第3 个线程提供输出。
       •  ReplyThread:负责向用户输出,将结果传回给用户(如果是网络应用),或者把数据写到本地文件系统或数据库中。

1)进程

计算机程序只是存储在磁盘上的可执行二进制(或其他类型)文件。只有把它们加载到内存中并被操作系统调用,才拥有其生命期。进程(有时称为重量级进程)则是一个执行中的程序。

2)线程

线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文。线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠)——这种做法叫做让步(yielding)。

一个进程中的各个线程与主线程共享同一片数据空间,因此相比于独立的进程而言,线程间的信息共享和通信更加容易。线程一般是以并发方式执行的,使得多任务间的协作成为可能。

3)Python中的线程

Python 代码的执行是由 Python 虚拟机(又名解释器主循环)进行控制的。在主循环中同时只能有一个控制线程在执行,对 Python 虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。

在多线程环境中,Python 虚拟机将按照下面所述的方式执行。

1.设置 GIL。
       2.切换进一个线程去运行。
       3.执行下面操作之一。
              a.指定数量的字节码指令。
              b.线程主动让出控制权(可以调用 time.sleep(0)来完成)。
       4.把线程设置回睡眠状态(切换出线程)。
       5.解锁 GIL。
       6.重复上述步骤。

当一个线程完成函数的执行时,它就会退出。另外,还可以通过调用诸如 thread.exit()之类的退出函数,或者 sys.exit()之类的退出 Python 进程的标准方法,亦或者抛出 SystemExit异常,来使线程退出。不过,你不能直接“终止”一个线程。

2 多线程模块

thread 模块提供了基本的线程和锁定支持;而 threading 模块提供了更高级别、功能更全面的线程管理。使用 Queue 模块,用户
可以创建一个队列数据结构,用于在多线程之间进行共享。

2.1 Thread

******************************注:避免使用 thread 模块,推荐使用更高级别的 threading 模块。******************************

 thread 模块提供了除了派生线程外,还提供了基本的同步数据结构,称为锁对象(lock object,也叫原语锁、简单锁、互斥锁、互斥和二进制信号量)。

thread 2.0 模块和锁对象:

https://docs.python.org/3.6/library/_thread.html#module-_thread 

 核心函数是 start_new_thread()。它的参数包括函数(对象)、函数的参数以及可选的关键字参数。(要执行的函数不需要参数,也需要传递一个空元组。)将专门派生新的线程来调用这个函数。

简单多线程机制:

2.2 Thraading

threading 模块的对象:

threading 模块的 Thread 类是主要的执行对象:

import threading
from time import sleep,ctime

loops =[4,2]

def loop(nloop,nsec):
    print('Start loop',nloop,' at: ',ctime())
    sleep(nsec)
    print('End loop',nloop,' at: ',ctime())
    
def main():
    print('Project starting at :',ctime())
    threads =[]
    nloops = range(len(loops))
    
    for i in nloops:
        t = threading.Thread(target=loop,args=(i,loops[i]))
        threads.append(t)
        
    for i in nloops:
        threads[i].start() #开始所有的线程
        
    for i in nloops:
        threads[i].join()  #等待所有的线程结速
        
    print('All Done at: ',ctime())
    
if __name__ == '__main__':
    main()

 实例化 Thread(调用 Thread())和调用 thread.start_new_thread()的最大区别是新线程不会立即开始执行。这是一个非常有用的同步功能。通过调用每个线程的 start()方法让它们开始执行,相比于管理一组锁(分配、获取、释放、检查锁状态等)而言,调用 join()方法即可。

子类化的 Thread:

import threading
from time import ctime,sleep

class MyThread(threading.Thread):          #子类化
    def __init__(self,func,args,name=''):
        threading.Thread.__init__(self)
        self.name = name
        self.func =func
        self.args =args
        
    def getResult(self):
        return self.res
    
    def run(self):
        print('Starting',self.name,' at: ',ctime())
        self.res = self.func(*self.args)
        print(self.name,' finished at: ',ctime())

def loop(nloop,nsec):
    print('Start loop',nloop,' at: ',ctime())
    sleep(nsec)
    print('End loop',nloop,' at: ',ctime())
    
def main():
    loops =(1,2)
    print('Project starting at :',ctime())
    threads =[]
    nloops = range(len(loops))
    
    for i in nloops:
        t = MyThread(loop,(i,loops[i]),loop.__name__)
        threads.append(t)
        
    for i in nloops:
        threads[i].start() #开始所有的线程
        
    for i in nloops:
        threads[i].join()  #等待所有的线程结速
        
    print('All Done at: ',ctime())
if __name__ == '__main__':
    main()

2.3 Queue

在生产者-消费者模型中,生产商品的时间是不确定的,同样消费者消费生产者生产的商品的时间也是不确定的。

使用 Queue 模块来提供线程间通信的机制,从而让线程之间可以互相分享数据。具体而言,就是创建一个队列,让生产者(线程)在其中放入新的商品,而消费者(线程)消费这些商品。

 

import queue 
import time,threading

q=queue.Queue(5)
 
def product(name):
    count=1
    while True:
        q.put('书籍{}'.format(count))
        print ('{}生产第{}本书籍'.format(name,count))
        print('Size now ',q.qsize())
        count+=1
        time.sleep(4)

def consume(name):
    while True:
        print ('{}购买了第{}本书籍'.format(name,q.get()))
         print('Size now ',q.qsize())
        time.sleep(2)
        q.task_done()

3 多线程实践

1)同步原语

多线程编程中一个非常重要的方面:同步。在多线程代码中,总会有一些特定的函数或代码块不希望(或不应该)被多个线程同时执行。

 当 任 意 数 量 的 线 程 可 以 访 问 临 界 区 的 代 码,但在给定的时刻只有一个线程可以通过时,就是使用同步的时候了。

其中两种类型的同步原语:锁/互斥,以及信号量

2)锁

当多线程争夺锁时,允许第一个获得锁的线程进入临界区,并执行代码。所有之后到达的线程将被阻塞,直到第一个线程执行结束,退出临界区,并释放锁。此时,其他等待的线程可以获得锁并进入临界区。

方案一:调用锁的 acquire()和 release()

def loop(nsec):
    myname = currentThread().name
    lock.acquire()
    remaining.add(myname)
    lock.release()
    sleep(nsec)
    lock.acquire()
    remaining.remove(myname)
    lock.release()

方案二:使用上下文管理

使用 with 语句,此时每个对象的上下文管理器负责在进入该套件之前调用 acquire()并在完成执行之后调用 release()。

def loop(nsec):
    myname = currentThread().name

    with lock:
        remaining.add(myname)
    sleep(nsec)

    with lock:
        remaining.remove(myname)

3)信号量

情况更加复杂时,可能需要一个更强大的同步原语来代替锁。信号量是最古老的同步原语之一。它是一个计数器,当资源消耗时递减,当资源释放时递增。

from threading import BoundedSemaphore,Lock,Thread

lock = Lock()
MAX =5
candy = BoundedSemaphore(MAX)

def buy():
    lock.acquire()
    try:
        candy.release()
    except ValueError:
        print('Full')
    else:
        print('OK')
    lock.realease()

def sell():
    lock.acquire()
    if candy.acquire(False):
        print('OK')
    else:
        print('Empty')
    lock.realease()

4 线程的替代方案

subprocess 模块
这是派生进程的主要替代方案,可以单纯地执行任务,或者通过标准文件(stdin、stdout、stderr)进行进程间通信。

multiprocessing 模块
允许为多核或多 CPU 派生进程,其接口与 threading模块非常相似。该模块同样也包括在共享任务的进程间传输数据的多种方式。

concurrent.futures 模块
这是一个新的高级库,它只在“任务”级别进行操作,也就是说,你不再需要过分关注同步和线程/进程的管理了。

参考文献:《Python核心编程》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章