pythin threading 剖析

进程是正在运行的程序实体,并且包括这个运行的程序中占据的所有系统资源,一个进程可以并发多个线程,多线程可以提高执行的效率,就是说一个任务分工给多人去完成,但是这是建立在两个线程干的事情类别相差较大(就是I/O操作及cpu操作),否则有的时候效果不是很明显。

,除此之外还有就是这些线程之间的顺序又是怎么协调的呢?

本文从三大部分进行介绍,

(1)首先介绍一下,查看和当前线程有关的属性的一些方法:

##################################################################################################

#!pyana
#!coding=utf8
import threading
import time
import random
 
lock=threading.Lock()
 
def test(n):
 
    thread=threading.current_thread()
    thread.setName('This is the %d thread' %(n+1))
    print('----------------------------------')
    print('%s'%thread.getName())
    print('the PID of the thead is %s'%thread.ident)
    print('the all current threads are:%s'%threading.enumerate())
    print('the active current are:%s'%threading.active_count())
    print('----------------------------------')
 
def main():
    threads=[]
    for i in range(4):
        t=threading.Thread(target=test,args=(i,))
        t.start()
        threads.append(t)
    for thread in threads:
        thread.join()
    
    print('################# all done ##############')
    
 
if __name__=='__main__':
    main()
 
 
 

thread.ident返回的是线程PID

threading.enumerate()返回的是当前存活的线程

threading.active_count()返回的是当前存活线程的个数

分析:(1)可以看到第一个打印的并不是thread1而是thread2,这是因为线程是轮流抢占cpu,想抢上谁就运行,所以这里第一个                运行的不一定就是thread1

             (2)在四个线程中有的还没有来得及命名的时候,线程会初始化其PID和线程名,例如第一个虚线之间的

              <Thread(Thread-3,initial -1245709456)>

             (3)主线程是一样的即<_MainThread(MainThread , start -1216547136)>

 

(2)下面介绍一些可以协调线程之间关系的方法,以做一桌丰富的饭菜为类进行介绍:

##########################################################################################

现在我们先做一盘爆炒豆角:

#!pyana
#!coding=utf8
import threading
import time
 
Menu=['爆炒豆角']
 
def cooking():
    print(Menu[0]+'正在做........\n')
    time.sleep(1)
    print(Menu[0]+'做好了!!!!\n')
 
def main():
    t=threading.Thread(target=cooking)
    t.start()
    print('好了,开吃吧!!!!\n')
 
 
if __name__=='__main__':
    main()

纳尼?还没做好怎么开吃?显然这是不对的,这是因为主线程main和子线程cooking是并发的,两个线程同时抢占CPU资源,过程是这样的:t.start()后子线程抢到了cpu,运行了第一个print,在爆炒豆角的时候(time.sleep(1))cpu空闲,主线程抢到了cpu运行print,而后又是子线程运行第二个print,所以造成了上面的结果。

怎么办呢?这时候join该上场了,,它的作用就是在子线程执行完成之前,这个子线程的父线程将一直被阻塞,其官方解释是(Waits for this thread to die.)就是说的这个意思,这里的join意思就是说将子线程加入到主线程中,其实这里的join解决的就是一个同步和异步的关系,来试一下吧。

#!pyana
#!coding=utf8
import threading
import time
 
Menu=['爆炒豆角']
 
def cooking():
    print(Menu[0]+'正在做........\n')
    time.sleep(1)
    print(Menu[0]+'做好了!!!!\n')
 
def main():
    t=threading.Thread(target=cooking)
    t.start()
    t.join()
    print('好了,开吃吧!!!!\n')
 
 
if __name__=='__main__':
    main()

######################################################################################

既然是一桌菜,那我们就得多做几个吧!!!假设现在要做的菜谱有:爆炒豆角、番茄炒蛋、茄子炒肉。(假设就有一个锅可用)

flow_rate是具体做某一道菜时放的速度

sum_gram是假设没道菜的克数

is_left_gram是锅里剩下菜的克数

这里数据没有实际意义,只为说明问题的过程!!!!!!

#!pyana
#!coding=utf8
import threading
import time
 
Menu=['爆炒豆角','番茄炒蛋','茄子炒肉']
flow_rate=[0.005,0.008,0.001]
 
sum_gram =300
is_left_gram=0
 
 
def cooking(n):
    global is_left_gram
    print(Menu[n]+'正在做............\n')
    for i in range(int(sum_gram/flow_rate[n])):
        is_left_gram+=flow_rate[n]
    for i in range(int(sum_gram/flow_rate[n])):
        is_left_gram-=flow_rate[n]
    print(Menu[n]+'做完了,出锅吧!!\n')
 
def main():
    threads=[]
    for i in range(len(Menu)):
        t=threading.Thread(target=cooking,args=(i,))
        t.start()
        threads.append(t)
    for thread in threads:
        thread.join()
    print('好了,开吃吧!!!!\n')
    print('锅里剩下%d 克\n'%is_left_gram)
 
if __name__=='__main__':
    main()

    

    

运行结果:


按理论来说,锅里应该为0才对,然而这里确实-55,其实每次运行的结果这里都是不一样的,也就是说在三个线程同时操作同一个共享资源时(这里可以理解用了同一个锅),出现了混乱,导致冲突,这时候lock就起了作用,它的作用可以理解为锁住一段代码,在执行的时候其他子进程处于阻塞状态。

当然这里的逻辑也是不对的,番茄炒蛋还没有出锅,爆炒豆角怎么就做了???????

针对以上修改demo:

#!pyana
#!coding=utf8
import threading
import time
 
Menu=['爆炒豆角','番茄炒蛋','茄子炒肉']
flow_rate=[0.005,0.008,0.001]
 
sum_gram =300
is_left_gram=0
lock=threading.Lock()
 
 
def cooking(n):
    global is_left_gram,lock
    lock.acquire()
    print(Menu[n]+'正在做............\n')
    for i in range(int(sum_gram/flow_rate[n])):
        is_left_gram+=flow_rate[n]
    for i in range(int(sum_gram/flow_rate[n])):
        is_left_gram-=flow_rate[n]
    print(Menu[n]+'做完了,出锅吧!!\n')
    lock.release()
def main():
    threads=[]
    for i in range(len(Menu)):
        t=threading.Thread(target=cooking,args=(i,))
        t.start()
        threads.append(t)
    for thread in threads:
        thread.join()
    print('好了,开吃吧!!!!\n')
    print('锅里剩下%d 克\n'%is_left_gram)
 
if __name__=='__main__':
    main()

可以看到都对啦!!!!!!

但是这里可以想到的一个问题就是所谓的死锁,就是有两个进程A和B,A需要B的资源或一部分运行结果,B需要的A中的资源或一部分运行结果,但二者又彼此锁着自己的进程使得对方获取不到,就这样进入死锁状态,当然有一个rlock方法,就是可以多次acquire,但这里主要介绍一下Condiftion,它对协调有着更加重要的作用。

其有三种状态,即:

Condiftion.wait()   

当执行wait后该就暂时挂起,直到收到一个notify通知或者超时才能被唤醒继续运行

Condiftion.notify()

就是发送一个notify信号,告诉那些挂起的线程,你们继续吧

Condiftion.notifyall()

就是唤醒所有挂起的线程

下面以做一盘番茄炒蛋具体步骤为例

为了简单我们暂且从两个角度考虑:锅和人

#!pyana
#!coding=utf8
import threading
import time
 
Menu=['番茄炒蛋']
condition=threading.Condition()
def cooker():
    condition.acquire()
    print('厨师:放好了,么么哒!!\n')
    condition.notify()
    condition.wait()
    print('厨师:好的好的,我在炒....\n')
    time.sleep(2)
    print('厨师:最后5下,倒计时\n')
    for i in range(5):
        print('time:%d\n'%(5-i))
    condition.notify()
    condition.wait()
    print('厨师:OK!\n')
    condition.release()
    
    print(Menu[0]+'做好了!!!!\n')
def pot():
    condition.acquire()
    print('锅:请放油...\n')
    condition.notify()
    condition.wait()
    print('锅:感受到了,好腻呀,油开了,快放东西,炒起来\n')
    condition.notify()
    condition.wait()
    print('锅:出锅吧\n')
    condition.notify()
    condition.release()
def main():
    threads=[]
    t1=threading.Thread(target=pot)
    t2=threading.Thread(target=cooker)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('好了,开吃吧!!!!\n')
 
 
if __name__=='__main__':
    main()

################################################################################################

下面来介绍一下Semaphore和BoundedSemaphore

Semaphore 是一个内部计数器。调用 acquire() 会使这个计数器 -1,release() 则是+1(可以多次release(),所以计数器的值理论上可以无限),但是计数器为0时,再次acquire()时就导致阻塞了(必须等到其他线程release()),也就是说有下限,正因为如此semaphore 用于守护限制访问的资源,但是没有上限可以多次release(), 针对此便有了BoundedSemaphore,其有上限证即初始值,,如果超了,就会抛出一个 ValueError。

为了说明这里模拟一个场景,假设现在有一个场景:一家饭店,这次我们再顺便多加几个菜,这里有一张桌子,上面最多放2盘菜(如果多出来没地方放,视为倒掉)

#!pyana
#!coding=utf8
import threading
import time
import random
 
num_cooking=2
Menu=['番茄炒蛋','爆炒豆角','茄子炒肉','皮蛋瘦肉','红烧排骨']
semaphore=threading.BoundedSemaphore(num_cooking)
lock=threading.Lock()
#放菜
def put_cooking(i):
    with lock:
        print(Menu[i]+'正在做..........\n')
    try:
        semaphore.release()
    except ValueError:
        print(Menu[i]+'做好了但桌上放满了菜,没地方了!!!!\n')
    else:
        print(Menu[i]+'做好了,放到桌上了')
#取菜
def cater_cooking():
    with lock:
        print('小二,上菜\n')
        if semaphore.acquire(False):
            print('好的,客官您的菜来喽!!!\n')
        else:
            print('您稍等一下,马上就好!!\n')
 
#服务员
def waiter(n):
    for i in range(n):
        put_cooking(random.randint(0,len(Menu)-1))
        time.sleep(1)
 
#顾客
def consumer(n):
    for i in range(n):
        cater_cooking()
        time.sleep(4)
 
 
def main():
    print('有顾客来了!!伙计们开张!!!\n')
    print('目前桌上有%d盘菜\n'%num_cooking)
    threads=[]
    t1=threading.Thread(target=consumer,args=(random.randint(4,5+num_cooking),))
    t2=threading.Thread(target=waiter,args=(random.randint(4,5+num_cooking),))
    t1.start()
    threads.append(t1)
    t2.start()
    threads.append(t2)
    for thread in threads:
        thread.join()
    
    print('打样喽!!!!!!!')
 
if __name__=='__main__':
    main()
 

这里用到的是BoundedSemaphore,下面换成Semaphore,也就是说没有上限(桌上理论上可以放无数盘菜)

#!pyana
#!coding=utf8
import threading
import time
import random
 
num_cooking=2
Menu=['番茄炒蛋','爆炒豆角','茄子炒肉','皮蛋瘦肉','红烧排骨']
semaphore=threading.Semaphore(num_cooking)
lock=threading.Lock()
#放菜
def put_cooking(i):
    with lock:
        print(Menu[i]+'正在做..........\n')
    try:
        semaphore.release()
    except ValueError:
        print(Menu[i]+'做好了但桌上放满了菜,没地方了!!!!\n')
    else:
        print(Menu[i]+'做好了,放到桌上了')
#取菜
def cater_cooking():
    with lock:
        print('小二,上菜\n')
        if semaphore.acquire(False):
            print('好的,客官您的菜来喽!!!\n')
        else:
            print('您稍等一下,马上就好!!\n')
 
#服务员
def waiter(n):
    for i in range(n):
        put_cooking(random.randint(0,len(Menu)-1))
        time.sleep(3)
 
#顾客
def consumer(n):
    for i in range(n):
        cater_cooking()
        time.sleep(4)
 
 
def main():
    print('有顾客来了!!伙计们开张!!!\n')
    print('目前桌上有%d盘菜\n'%num_cooking)
    threads=[]
    t1=threading.Thread(target=consumer,args=(random.randint(4,5+num_cooking),))
    t2=threading.Thread(target=waiter,args=(random.randint(4,5+num_cooking),))
    t1.start()
    threads.append(t1)
    t2.start()
    threads.append(t2)
    for thread in threads:
        thread.join()
    
    print('打样喽!!!!!!!')
 
if __name__=='__main__':
    main()
 

可以看到,即使桌上的菜超过了2盘也不会提示“没地方放”的字样!!!!!!

################################################################################################

下面介绍一下threading.Event(),可以简单看做,其定义了一个bool类型,但其为True时event.wait()后该进程不受阻塞,相反为False时event.wait()便受阻塞, event.set()便是设为True    ,   event.clear()便是设为False

下面是一个简单的demo:

#!pyana
#!coding=utf8
import threading
import time
import random
 
 
Menu=['蛋炒饭']
event=threading.Event()
 
def step():
    print('--------------步骤一:炒蛋------------------\n')
    event.wait()
    print('--------------步骤二:放各种作料------------\n')
    time.sleep(2)
    event.wait()
    print('--------------步骤三:倒入米饭--------------\n')
    time.sleep(1)
    event.wait()
    print('--------------步骤四:一起大火爆炒----------\n')
    time.sleep(1)
    event.wait()
    print('--------------出锅啦-----------------------\n')
 
def cooking():
    time.sleep(1)
    print('炒好蛋了\n')
    event.set()
 
    time.sleep(1)
    event.clear()
    print('作料放好\n')
    event.set()
 
    time.sleep(1)
    event.clear()
    print('米饭倒入\n')
    event.set()
 
    time.sleep(1)
    event.clear()
    print('爆炒好了\n')
    event.set()
 
 
 
def main():
    print('下面我们来做'+Menu[0])
    t1=threading.Thread(target=step)
    t2=threading.Thread(target=cooking)
    t1.start()
    t2.start()
    t1.join()
    t2.join()
    print('开吃吧!!!!!!!!!!!!!!')
 
 
if __name__=='__main__':
    main()
 
 

##################################################################################################

(3)最后讨论一下线程的效率问题

做了这么多菜,买了多少钱总得算一下吧,下面就是通过账本计算一下总收入:这里一共有5个账本(为了方便,这里假设每个账本记得账都一样),每个账本有1000条收入记录,分别为0,1,2,3,4..........(数据没有实际意义,只为说明过程),一种是通过No_MultiAccounts方法,也就是传统方法,一个账本一个账本去算,另一个是用MultiAccounts启动了5个线程去计算

demo如下:

注意:(1)python中有深拷贝和浅拷贝之分,深拷贝是将对象本身复制给另一个对象,以后对对象的副本做改动时并不会影响                         原对象,浅拷贝就是说会影响原对象,深拷贝对应的API是deepcopy,浅拷贝对应的API是copy,这里因为五个线程是                        要 对同 一段交替操作,最后的Total要是他们操作的共同结果,故这里要用到浅拷贝。

             (2)threading中没有返回运算结果的API,故这里使用队列,可以取得每个线程运行的结果

import threading
import time
import copy
from queue  import Queue

def Accounts(account_book,q):
    Sum=sum(account_book)
    q.put(Sum)
    
def MultiAccounts(account_book):

    threads=[]
    q=Queue()
    
    for i in range(5):
        t=threading.Thread(target=Accounts,args=(copy.copy(account_book),q))
        t.start()
        threads.append(t)

    for thread in threads:
        thread.join()

    Total=0
    for _ in range(5):
        Total+=q.get()
    print('the total of MultiAccounts is %d'%Total)
        
def No_MultiAccounts(account_book):
    Total=0
    for _ in range(5):
        Total+=sum(account_book)
    print('the total of No_MultiAccounts is %d'%Total)
        
def main():
    
    account_book=list(range(1000))
    time1=time.time()
    No_MultiAccounts(account_book)
    print('the time of No_MultiAccounts is %f'%(time.time()-time1))

    time2=time.time()
    MultiAccounts(account_book)
    print('the time of MultiAccounts is %f'%(time.time()-time2))
    
              
if __name__=='__main__': 
    main()

按原理来说,这里用了5个线程,最后用时大约应该是No_MultiAccounts方法的五分之一,然而却恰恰相反,用时却大于No_MultiAccounts方法,这是怎么回事呢?

其实线程是轻量级的进程,GIL(全局解释器)确保一次执行单个线程,一个线程保存GIL,然后传递个下个线程,就这样线程轮流执行,因为执行速度过快,所以就造成线程之间并行执行的错觉,所有的传递过程都需要时间开销,所以最后的结果高于No_MultiAccounts方法。

那么Python线程到底有什么用呢?其实当程序中有很多I/O操作的时候,线程的效率就会上去,因为一般来说I/O操作很少占用cpu,大部分是交给DMA进行数据的传递,比如有两个线程,当A线程占用CPU时,B可以进行I/O操作,当轮到B占用CPU时,A又可以进行I/O操作,可是不用多线程的话,当A线程占用CPU运行时,另一个不能进行I/O操作,必须等待其完成,才能一步步按部就班的进行,所以threading节省的是交替时I/O操作所用的时间。

下面看一下demo:这里是统计了一下一个文件中有多少个英文单词,为了说明这里统计了两次

一种方法就是传统的方法,一次次统计,另一种是用2个线程去分别统计,因为这里有读取文件的操作(read_data),所以多线程效率高一点

import os
import threading
import time
import copy
from queue  import Queue
import zipfile
import tensorflow as tf
from six.moves import urllib

# (三)下载数据集
url = 'http://mattmahoney.net/dc/'

def maybe_download(filename, expected_bytes):
    """Download a file if not present, and make sure it's the right size."""
    if not os.path.exists(filename):
        filename, _ = urllib.request.urlretrieve(url + filename, filename)
    # 获取文件相关属性
    statinfo = os.stat(filename)
    # 比对文件的大小是否正确
    if statinfo.st_size == expected_bytes:
        print('Found and verified', filename)
    else:
        print(statinfo.st_size)
        raise Exception(
            'Failed to verify ' + filename + '. Can you get to it with a browser?')
    return filename


# (四)生成单词表
def read_data(filename):
  """Extract the first file enclosed in a zip file as a list of words."""
  with zipfile.ZipFile(filename) as f:
    data = tf.compat.as_str(f.read(f.namelist()[0])).split()
  return data



def statistics(filename,q):
    vocabulary = read_data(filename)
    Sum=len(vocabulary)
    q.put(Sum)

   
def Multistatistics(filename):

    threads=[]
    q=Queue()
    
    for i in range(2):
        t=threading.Thread(target=statistics,args=(filename,q))
        t.start()
        threads.append(t)

    for thread in threads:
        thread.join()

    Total=0
    for _ in range(2):
        Total+=q.get()
    print('the total of Multistatistics is %d'%Total)


def No_Multistatistics(filename):
      Total=0
      for i in range(2):
          vocabulary = read_data(filename)
          Total+=len(vocabulary)
      print('the Total of No_Multistatistics is %d'%Total)
        
def main():

    filename = maybe_download('text8.zip', 31344016)
      
    time1=time.time()
    No_Multistatistics(filename)
    print('the time of No_Multistatistics is %f'%(time.time()-time1))

    time2=time.time()
    Multistatistics(filename)
    print('the time of Multistatistics is %f'%(time.time()-time2))
    
              
if __name__=='__main__': 
    main()

可以总结的就是,首先要看程序是cpu密集型还是I/O密集型,如果是I/O密集型,那么多线程是可以起到一定作用的,但如果是cpu密集型,结果就不一定了,有时候弄不好还适得其反,毕竟线程之间的传递要需要时间开销。

那么是cpu密集型就没有办法了吗?当然有,可以采取多核呀!!!说白了就是多个cpu,再往大了说就是今天的大数据处理,可以采取Hadoop,spark等集群手段,其实可以看做就是更多的cpu参与进来!!!!!!!!!!!

#################################################################################################

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值