Python 多线程编程

一、线程和进程

线程和进程的概念:

  • 进程:是资源(CPU、内存等)分配的基本单位,是程序执行时的一个实例。进程拥有自己的地址空间、内存、数据栈以及其它用于跟踪执行的辅助数据库,进程间通过IPC(进程间通信)的方式共享信息。
  • 线程:是CPU调度和分派的基本单位,是程序执行的最小单位。线程有自己的堆栈和局部变量,线程间共享进程的所有资源。

线程和进程区别和联系:

  • 进程是资源分配的最小单位,而线程是程序执行的最小单位
  • 进程拥有独立的地址空间,开销较大,而线程共享进程的数据,使用相同地址空间,CPU创建和切换线程比进程开销小得多
  • 进程间通信需要通过IPC进行,速度较慢,而线程共享进程所有数据,通信更方便
  • 一个进程至少有一个线程。

二、Python多线程编程 

先来看看单线程和多线程的区别

采用单线程,程序运行main函数进入进程,一个进程至少有一个线程,示例如下:

from time import sleep,ctime
def loop(t=None):
    print 'Start loop %s at:'%t,ctime()
    sleep(t)
    print 'Stop loop %s at:'%t,ctime()
if __name__ == '__main__':
    loop(2)
    loop(4)
Start loop 2 at: Thu Apr 04 19:00:43 2019
Stop loop 2 at: Thu Apr 04 19:00:45 2019
Start loop 4 at: Thu Apr 04 19:00:45 2019
Stop loop 4 at: Thu Apr 04 19:00:50 2019

 采用多线程,示例代码如下:

from time import sleep,ctime
import threading
def loop(t=None):
    print 'Start loop %s at:'%t,ctime()
    sleep(t)
    print 'Stop loop %s at:'%t,ctime()
if __name__ == '__main__':
    t1=threading.Thread(target=loop,args=(2,))  #初始化子线程对象,通过Thread对象创建线程
    t2=threading.Thread(target=loop,args=(4,))
    t1.start()
    t2.start()
Start loop 2 at: Thu Apr 04 19:01:23 2019
Start loop 4 at: Thu Apr 04 19:01:23 2019
Stop loop 2 at: Thu Apr 04 19:01:26 2019
Stop loop 4 at: Thu Apr 04 19:01:28 2019

 可以看到代码运行的时间比之前宏观上少了2s,多线程最直观的理解就是“程序可以在同一时间做很多事情”

Python给多线程编程提供了多个模块儿,如thread,threading等。推荐不再使用thread,而使用更高级的threading模块儿。部分原因是:thread模块对于进程退出没有控制,当主线程结束时,所有其他线程强制结束,不会发出警告或作出处理,而threading模块儿可以保证重要的子线程在主进程结束前结束。这里只介绍threading模块儿

threading模块

threading模块中的对象:

  • Thread:表示一个执行线程的对象(较常用)
  • Lock:锁原语对象
  • RLock:可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁)
  • Condition:条件变量对象,使得一个线程等待另一个线程满足特定的“条件”,比如改变状态或某个数据值
  • Event:条件变量的通用版本,任意数量的线程等待某个事件的发生,在该时间发生后所有线程将被激活
  • Semaphore:为线程间共享的有限资源提供了一个“计数器”,如果没有可用资源时会被阻塞
  • BoundsSemaphore:与Semaphore相似,不过它不允许超过初始值
  • Timer:与Thread相似,不过它在运行前需等待一段时间
  • Barrier:创建一个“障碍”,必须达到指定数量的线程后才可以继续(Python 3.2引入)

1、Thread类

  • name:线程名
  • ident:线程标识符
  • daemon:布尔标志,表示线程是否是守护线程
  • setDaemon():True设置线程为守护线程,主线程一旦执行结束,则全部线程全部被终止,这时候子线程可能还没有执行完;False为默认情况,主线程执行结束,子线程继续执行自己任务。
  • _init_(group=None,target=None,name=None,args=(),kwargs={},verbose=None,daemon=None):实例化对象,可调用的target及其参数args或kwargs,daemon的值会被设定为thread.daemon属性
  • start():开始执行该线程
  • run():定义线程功能的方法,通常在子类中被应用开发者重写
  • join(timeout=None):直至启动的线程终止之前一直挂起,除非出超时时间,否则一直阻塞。即主线程任务结束后,进入阻塞状态,一直等待其他的子线程执行结束后,主线程再终止。
  • getName()、setName(name)

Threading创建线程有两种方式,一种是通过初始化Thread对象创建,见上面单线程多线程区别示例,另一种是通过继承Thread类来创建,代码示例如下:在run函数中重写,或者调用外部函数

import threading
from time import ctime, sleep
#继承Thread类,在子类中重写init和run方法
class mythread(threading.Thread):
    def __init__(self,name):
        super(mythread,self).__init__(name=name)
    def run(self):
        print self.name
        sleep(1)
t1=mythread('thread-1')
t2=mythread('thread-2')   
t1.start()
t2.start()
thread-1
thread-2
import threading
from time import ctime, sleep

def loop(t=None):
    print 'Start loop %s at:'%t,ctime()
    sleep(t)
    print 'Stop loop %s at:'%t,ctime()
#继承Thread类,调用外部传入函数
class mythread(threading.Thread):
    def __init__(self,name,args,target):
        super(mythread,self).__init__(name=name)
        self.target=target
        self.args=args
    def run(self):
        print self.name
        self.target(*self.args)
t1=mythread(target=loop,args=(2,),name='thread-1')
t2=mythread(target=loop,args=(4,),name='thread-2')   
t1.start()
t2.start()
thread-1
Start loop 2 at: Mon Apr 08 19:32:31 2019
thread-2

Start loop 4 at: Mon Apr 08 19:32:31 2019
Stop loop 2 at: Mon Apr 08 19:32:34 2019
Stop loop 4 at: Mon Apr 08 19:32:36 2019

2、Lock/RLock类

多线程和多进程最大的不同之处在于多进程各自有一份独立的数据,互不影响,而多线程,所有变量都有所有线程共享,这样就会造成任意变量会被任意线程操作,出现死锁,数据混乱的现象。锁是所有机制中最简单,最低级的机制。

数据混乱的例子:每次得到的结果不等(多执行几次。。。。)

import threading
#设置全局变量
num=0
def add():
    global num
    num+=1
    num-=1
def test():
    for i in range(1000000):
        add()
threading.Thread(target=test).start()
threading.Thread(target=test).start()
print num

锁一般通过lock.acquire()+lock.release()组合使用或者with lock来使用,示例代码如下:

import threading
lock = threading.RLock()
#最上层获取锁,释放锁
def locktest():
    with lock:
        print 'first layer'
        layer1()
#第一层获取锁,释放锁
def layer1():
    with lock:
        print 'second layer'
        layer2()
#第二层        
def layer2():
    print "layer2"
t=threading.Thread(target=locktest)
t.start()

这里使用threading.Lock()会造成阻塞, 因为Lock多次获取锁会发生死锁,RLock不会,因此推荐大家使用RLock,获得锁相对应一定记得释放。上面造成数据混乱的例子,只需要在把add()函数用with lock:“包裹”即可。

注:如果线程不想将变量共享出去,就需要使用局部变量,这样在函数定义局部变量会使得函数之间传递非常麻烦,ThreadLocal就是解决全局变量需要加锁和局部变量传递麻烦这两个问题。local_variable=threading.local(),或者继承类class _DbCtx(threading.local):,这样是一个全局变量,但是对于不同线程又是局部变量,线程之间不会互相影响。应用场景:例如不同数据库连接使用不同线程,local也是一个线程-属性字典。

3、Semaphore类(信号量)

Lock,RLock类只允许一个线程访问共享数据,而信号量可以允许一定数量的线程访问共享数据,示例代码如下:

import threading
from time import sleep,ctime
threadlist=[]
#设置3
semaphore=threading.BoundedSemaphore(3)
def loop(t=None):
    semaphore.acquire()
    sleep(3)
    print "thread %s is running at %s\n" % (t,ctime())
    semaphore.release()
for i in range(12):
    t=threading.Thread(target=loop,args=(i,))
    #获取线程列表
    threadlist.append(t)
for threadtemp in threadlist:
    threadtemp.start()

4、Condition类

有些线程无法直接运行,需要满足条件才可以允许它运行,Condition对象提供了这样的支持,该对象除RLock()和Lock()方法外,还有notify(),wait()以及notifyAll()方法,这里面锁是可选,默认RLock()。

  • wait():条件不满足时,线程释放锁,进入等待阻塞
  • notify():条件满足后,通知线程池激活线程
  • notifyAll():条件满足后,通知线程池激活所有线程

示例代码如下:

import threading
from time import sleep
num=0
con=threading.Condition()
def wpp():
    con.acquire()
    while True:
        print u"wpp开始训练气球兵"
        global num
        num+=1
        print u"wpp训练气球兵个数:%s"%num
        sleep(2)
        if num==3:
            print u"wpp:够了够了,给你"
            sleep(2)
            con.notify()
            con.wait()
    con.release()
def ypp():
    con.acquire()
    global num
    while True:
        print u"ypp:可以,可以,我去揍人了"
        num-=3
        sleep(2)
        if num<=0:
            print u"ypp:气球兵又没了,救!"
            sleep(2)
            con.notify()
            con.wait()
    con.release()
twpp=threading.Thread(target=wpp)
twpp.start()
typp=threading.Thread(target=ypp)
typp.start()

5、Event类 

Event和Condition差不多,Event不需要和锁关联,Event应用于不访问线程间共享资源的情景。Event事件处理机制:在全局定义一个Flag,如果Flag值为False,event.wait()方法时会阻塞,为True时不会阻塞。

  • set():将标志设为True,并通知所有处于阻塞状态的线程恢复运行状态
  • clear():标志设为False
  • wait(timeout):标志为True,不阻塞,标志为False,则阻塞
  • isSet():获取内置标志状态,返回True,False

示例代码如下:

import threading,time
event=threading.Event()
print event.isSet()
def test():
    print "start test and wait"
    #等待被唤醒从阻塞状态到运行状态
    event.wait()
    print event.isSet()
    time.sleep(2)
    print "waiting is over"
t=threading.Thread(target=test)
t.start()
time.sleep(2)
print "Main func is start running and event is setting True"
#设置为True,wait方法就不再阻塞
event.set()

三、Python多线程的局限性和意义

1、全局解释器锁(Global Interpreter Lock GIL)

对Python虚拟机的访问是由GIL控制的,这个锁保证了同时只有一个线程运行。Python设计之初,GIL是为了数据安全得出的产物,

  1. 设置GIL
  2. 切换进一个线程去运行
  3. 执行下面操作之一
    1. 指定数量的字节码指令
    2. 线程主动让出控制权
  4. 把线程设置回睡眠状态
  5. 解锁GIL
  6. 重复上述步骤

2、局限性

由上面的GIL可以看出(感兴趣可以深入了解下,我怕自己理解的不深,就不误导别人了),Python多线程在同一时刻只有一条线程跑CPU里,即使N个线程跑在N核CPU上,也只能用到1个CPU核,这样就导致CPU利用率非常低。所以如果使用多线程,想用Python就无法有效利用多核,还是用C什么的比较好。但是这个东西为什么还留着呢?因为GIL不是bug,是Python设计开发者权衡利弊最终留下的。

2、意义

那既然Python多线程无法有效利用多核,它还有什么存在的必要呢?肯定是有的呀

  • IO密集型任务:磁盘IO,网络IO占主要比例,CPU计算占比较小,eg:请求网页,读写文件等
  • 计算密集型任务:CPU计算占比很大,eg:复杂的数据计算

对于Python来说,IO密集型任务采用多线程,会有效的利用CPU;计算密集型任务采用多进程会有效利用CPU资源。

对于其它语言,针对不同场景,多线程和多进程的选择也是不同的,具体情况具体分析,所以Python多线程有用,而且用处很大,看开发者要怎么用,怎么写。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值