Python实现多线程(二)

一. 线程间的通信问题

多个线程共用进程空间,所以进程的全局变量对进程内的线程均可见。使用全局变量通信是线程主要通信方法。线程间通信更容易产生资源争夺,往往需要同步互斥机制保证通信安全。和multiprocessing中的Event和Lock类似,threading模块也提供了Event和Lock类来实现线程的同步互斥操作,关于二者的使用总结如下:

import threading
event = threading.Event
创建线程的Event事件。

event.wait([timeout])
如果event为设置状态则不阻塞,未设置则阻塞。

event.set()
将event变为设置状态

event.clear()
将event设置去除。

import threading
lock = threading.Lock()
创建锁。

acquire(blocking=True, timeout=None)
上锁。
参数:blocking=True,默认计数器值等于0时阻塞,传入非阻塞标志Flase时,将不再阻塞,并返回False;
操作原理 : 重复上锁会阻塞

lock.release()
解锁。

示例代码:利用锁实现同步互斥

from atexit import register
from random import randrange
from threading import Thread,Lock,currentThread
from time import sleep,ctime
 
class CleanOutputSet(set):
 
    def __repr__(self):
        return ', '.join(x for x in self) 
 
lock = Lock()
loops = (randrange(2,5) for x in range(randrange(3,7)))
remaining = CleanOutputSet()
 
def mytime():
    return ctime().split(' ')[3]
 
def loop(nsec):
    lock.acquire()
    remaining.add(currentThread().name)
    print("{} Started {}".format(mytime(), currentThread().name))
    lock.release()
 
    sleep(nsec)
    
    lock.acquire()
    remaining.remove(currentThread().name)
    print("{} Completed {}({} secs)".format(mytime(), currentThread().name, nsec))
    print("Remaining thread:{}".format(remaining or 'NONE'))
    lock.release()
 
def main():
    for pause in loops:
        Thread(target=loop, args=(pause,)).start()
 
@register
def exit():
    print(mytime(), "All done!")
 
if __name__ == "__main__":
    main()

运行结果:CleanOutputSet继承自set,重写repr方法以改变print的输出样式;字符串的join方法接收的参数类型为一个可迭代对象,生成器表达式(x for x in self)返回一个生成器对象,属于可迭代对象;randrange(start, end)产生一个start~end范围内的随机整数,(randrange(2,5) for x in range(randrange(3,7))返回一个包含36个随机数的生成器对象,且随机数的范围为24;使用atexit.register()来注册exit()函数,解释器会在脚本退出前执行该函数。所有线程在操作共享资源remaining时上锁,在睡眠时解锁。

二. 线程的应用

由于线程共享进程的资源,故线程间通信可以借助全局变量实现。当然这个全局变量也可以是具有特殊数据结构的信号量和队列。下面的2个示例分别用来说明线程和信号量的应用,以及线程和队列的应用。

2.1 线程和信号量

信号量实际就是一个计数器:分配一个单位的资源时,计数器值-1;一个单位的资源返回资源池时,计数器值+1。因此可以使用信号量来跟踪有限的资源,即用信号量表示资源可用/不可用。threading模块包含2种信号量的类:Semaphore和BoundedSemaphore,其中BoundedSemaphore提供了一个额外的功能:计数器值永远不会超过它的初始值,当计数器值满时,再release会触发ValueError异常。本例模拟一个简单的糖果机:糖果机共有5个空槽,使用信号量来跟踪有限的资源,主线程创建两个线程,模拟生产者和消费者:

from atexit import register
from random import randrange
from threading import BoundedSemaphore,Lock,Thread
from time import sleep,ctime
 
lock = Lock()
MAX = 3
candytray = BoundedSemaphore(MAX)
 
def refill():
    with lock:
        print("Refilling candy...")
        try:
            candytray.release()
        except ValueError:
            print("Full! Skipping...")
        else:
            print("OK!")
 
def buy():
    with lock:
        print("Buying candy...")
        if candytray.acquire(False):
            print('OK!')
        else:
            print('Empty! Skipping...')
 
def producer(loops):
    for i in range(loops):
        refill()
        sleep(randrange(3))
 
def consumer(loops):
    for i in range(loops):
        buy()
        sleep(randrange(3))
 
@register
def atexit():
    print(ctime(), "ByeBye~")
 
nloops = randrange(2, 4)
Thread(target=consumer, args=(randrange(nloops, nloops+MAX+2),)).start()  # 模拟供不应求的情况
Thread(target=producer, args=(nloops,)).start()

运行结果:初始的BoundedSemaphore信号量为3,当信号量为3时,再执行release时,将触发valueError异常;当所有的信号量均被acquire后,再执行acquire(blocking=False)时,将立刻返回False:

2.2 线程和消息队列

模拟生产者和消费者问题:生产者向队列中投放商品,消费者从队列中获取商品:

import threading
 
from random import randint
from time import sleep, ctime
from queue import Queue
 
class MyThread(threading.Thread):
    def __init__(self, func, args, name=''):
        super().__init__()
        self.name = name
        self.func = func
        self.args = args
 
    def get_result(self):
        return self.res
 
    def run(self):
        self.res = self.func(*self.args)
 
def writeQ(queue):
    queue.put("*", 1)
    print(ctime(), "+1 queue=", queue.qsize())
 
def readQ(queue):
    val = queue.get(1)
    print(ctime(), "-1 queue=", queue.qsize())
 
def writer(queue, loops):
    for i in range(loops):
        writeQ(queue)
        sleep_time = randint(1, 2)
        print(ctime(), 'producer will sleep', sleep_time, 's')
        sleep(sleep_time)
 
def reader(queue, loops):
    for i in range(loops):
        readQ(queue)
        sleep_time = randint(4, 5)
        print(ctime(), 'consumer will sleep', sleep_time, 's')
        sleep(sleep_time)
 
funcs = [writer, reader]    
q = Queue(32)
threads = []
    
producer = MyThread(funcs[0], (q, 3), funcs[0].__name__)
consumer = MyThread(funcs[1], (q, 3), funcs[1].__name__)
threads.append(producer)
threads.append(consumer)
 
for thread in threads:
    thread.start()
 
for thread in threads:
    thread.join()
 
print("ALL DONE")

运行结果:生产者线程向队列中依次加入3个 *,消费者线程从队列中消费 *

三. 全局解释器锁

Python代码的执行,是由python虚拟机(也称为解释器主循环)控制的。虽然Python解释器中可以运行多个线程,但同一时刻只能有一个线程被解释执行。python C解释器为了保证线程安全,默认加了一把全局的锁,但对于IO操作,GIL会在IO调用前被释放,以允许其它线程在IO执行时运行,即线程遇到阻塞会让出解释器。Python线程适用于高延迟的IO操作,如网络通信;不适合cpu密集型或者传输速度很快的IO操作。

为此,作为解决方案,可以用java解释器代替C解释器,或者使用多进程程序处理CPU密集型问题。

四. 多线程和多进程

4.1 进程、线程的区别及联系

两者都是多任务编程的方式,都能够使用计算机多核资源;进程创建和删除要比线程消耗更多计算机资源;进程空间独立数据安全性好,有专门的通信方法;线程使用全局变量通信,更加简单,但是往往需要同步互斥操作;一个进程可以包含多个线程,线程共享进程资源。

4.2 应用场景

  • 如果需要创建较多的并发,而任务比较简单,线程比较适合;
  • 使用线程时需要考虑到同步互斥复杂程度;
  • python 线程要考虑到GIL问题,CPU密集型操作使用进程,而IO密集型操作使用线程;
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容