Python小白带小白初涉多线程

650 查看

Python 2.7
IDE Pycharm 5.0.3


首先

解释一下线程:简单来说,一个进程中包含多个线程,比如打开一个qq(进程),然后你一边聊qq(一个线程),一边用qq传送文件(一个线程),等等,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)

再者

引用廖雪峰大大的话是酱紫的:Python解释器由于设计时有GIL全局锁,导致了多线程无法利用多核。多线程的并发在Python中就是一个美丽的梦。
But--Python的线程是真正的Posix Thread,而不是模拟出来的线程。


好了

了解其中的大概,就可以用程序来验证自己的想法了!!
不要方,直接上栗子(好饿~)

我们知道线程并发(虽然python不支持),但是大概是这么个意思,对于进程来说,同一变量,都有备份(大家都有面包),所以不用抢,但是,线程的世界中,变量只有一个,共享!所以他们得互相抢占资源,抢着执行程序,这就导致,任何一个共享变量都可以被任何一个线程修改,看图看图。来人,上栗子~

# -*- coding: utf-8 -*-
import threading,time,os,random
balance = 0

def change_it(n):
    global balance
    balance = balance +n
    balance = balance -n

def run_thread(n):
    for i in range(200):
            start = time.time()
            change_it(n)
            #time.sleep(random.random())
            end = time.time()
    print '%s 线程结束...'%threading.current_thread().name


print '开启当前线程...当前线程为%s'%threading.current_thread().name

print '实例化抢占模式多线程...'
t1 = threading.Thread(target=run_thread,args=(1,))
t2 = threading.Thread(target=run_thread,args=(4,))
t3 = threading.Thread(target=run_thread,args=(7,))

print '实例化抢占模式多线程完成...'
print '开启抢占模式多线程...'
t1.start()
t2.start()
t3.start()

print '开启抢占模式多线程完成...'
t1.join()
t2.join()
t3.join()

print '多线程结束,输出balance :',balance
开启当前线程...当前线程为MainThread
实例化抢占模式多线程...
实例化抢占模式多线程完成...
开启抢占模式多线程...
开启抢占模式多线程完成...
Thread-1 线程结束...
Thread-2 线程结束...
Thread-3 线程结束...
多线程结束,输出balance : 6

从结果可以看出,三个线程来回抢资源,导致变量被修改的面目全非,如果不用抢占,那么balance变量会一直保持0值!
这里可见其多线程的“霸道”
BTW任何进程默认就会启动一个线程,我们把该线程称为主线程,名字是MainThread主线程又可以启动新的线程,获取当前线程名字就用threading.current_thread().name简单快捷,啦啦啦啦


但是!

如果让线程那么霸道下去可不行,无规矩不成方圆,所以得给它上把锁,让它老实点的一个个排队来,(这样不就丧失了多线程并发的优势了咩,算了,python本来就不支持并发多线程0.0),而这个锁,线程需要向组织要(acquire),用完之后再还给组织(release),这样,就像关起门来干些不可名状的事,别人在门口待着,排队来,然后一个线程完事,再下一个线程来(捂脸)。不说了,上栗子!

# -*- coding: utf-8 -*-
import threading,time,os,random
lock = threading.Lock()
balance = 0

def change_it(n):
    global balance
    balance = balance +n
    balance = balance -n

def run_thread(n):
    for i in range(3):
        lock.acquire()
        try:
            start = time.time()
            change_it(n)
            time.sleep(random.random())
            end = time.time()
            print '%s线程在进行调用此函数! 耗时%.2f秒'%(threading.current_thread().name,end-start)
        finally:
            lock.release()
    print '%s 线程结束...'%threading.current_thread().name


print '开启当前线程...当前线程为%s'%threading.current_thread().name

print '实例化Lock模式多线程...'
t1 = threading.Thread(target=run_thread,args=(1,))
t2 = threading.Thread(target=run_thread,args=(2,))
t3 = threading.Thread(target=run_thread,args=(3,))

print '实例化Lock模式多线程完成...'
print '开启Lock模式多线程...'
t1.start()
t2.start()
t3.start()

print '开启Lock模式多线程完成...'
t1.join()
t2.join()
t3.join()

print '多线程结束,输出balance :',balance
开启当前线程...当前线程为MainThread
实例化Lock模式多线程...
实例化Lock模式多线程完成...
开启Lock模式多线程...
开启Lock模式多线程完成...
Thread-1线程在进行调用此函数! 耗时0.22秒
Thread-2线程在进行调用此函数! 耗时0.54秒
Thread-3线程在进行调用此函数! 耗时0.42秒
Thread-1线程在进行调用此函数! 耗时0.63秒
Thread-2线程在进行调用此函数! 耗时0.33秒
Thread-3线程在进行调用此函数! 耗时0.78秒
Thread-1线程在进行调用此函数! 耗时0.11秒
Thread-1 线程结束...
Thread-2线程在进行调用此函数! 耗时0.25秒
Thread-2 线程结束...
Thread-3线程在进行调用此函数! 耗时0.09秒
Thread-3 线程结束...
多线程结束,输出balance : 0

结果显示,当线程达到循环执行次数,会结束自己调用的程序,这里我把它打印出来了,会更利于理解。
不清楚的话我再贴一下lock定义:简单说,就是一个线程得到锁后,别的线程不能用!

threading.Lock() 
A factory function that returns a new primitive lock object. Once a thread has acquired it, subsequent attempts to acquire it block, until it is released; any thread may release it.

接下里的是各自为战local,不是lock锁门干事!
在多线程的环境下,每个线程都有自己的数据,为了使线程之间互不干扰,而且只能自己可见,那就要使用threading.local()了,老样子,给波简单官腔;

class threading.local 
A class that represents thread-local data. Thread-local data are data whose values are thread specific. To manage thread-local data, just create an instance of local (or a subclass) and store attributes on it:
mydata = threading.local()
mydata.x = 1

好啦好啦,就这个,然后用起来就知道具体的操作了,举栗子咯

# -*- coding: utf-8 -*-
import threading
local_name = threading.local()
local_mission = threading.local()
#创建全局ThreadLocal对象

def Task():
    print 'please read your misiion:'
    print 'Hello,%s,your mission:%s(from%s)'%(local_name.name,local_mission.mission,threading.current_thread().name)
    print '%s accept the challenge'%local_name.name
    #先后进入Task

def process_thread(name,mission):
    local_name.name = name
    local_mission.mission = mission
    #注意参数的传递
    Task()

t1 = threading.Thread(target=process_thread,args=('BOb','kill Alex'),name='专线1')
t2 = threading.Thread(target=process_thread,args=('Alex','kill Bob'),name='专线2')
t1.start()
t2.start()
t1.join()
t2.join()
print 'game over'
please read your misiion:
Hello,BOb,your mission:kill Alex(from专线1)
BOb accept the challenge
please read your misiion:
Hello,Alex,your mission:kill Bob(from专线2)
Alex accept the challenge
game over

这里的local_name(同理local_mission)就是一个ThreadLocal对象,每个Thread对它都可以读写name属性,但互不影响。你可以把local_name看成全局变量,但每个属性如local_name.name都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。


最后

进程VS线程啦,墙裂推荐进程VS线程,秒懂
懒得看我就总结下:
关于进程:
多进程模式最大的优点就是稳定性高,因为一个子进程崩溃了,不会影响主进程和其他子进程。(当然主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)著名的Apache最早就是采用多进程模式。
多进程模式的缺点是创建进程的代价大,在内存和CPU的限制下,如果有几千个进程同时运行,操作系统连调度都会成问题。


关于线程:
多线程模式通常比多进程快一点,但是也快不到哪去,而且,多线程模式致命的缺点就是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。
在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。


最后的最后

Python适合IO密集型任务,如Web应用,让Python处理计算密集型任务你咋不上天呢!那么优美的语言干那么底层的事,甩锅给C,它行它上!


9.25补充

如何避免受到GIL的影响。
使用multiprocessing代替Thread,有人会说,诶,这个不是用在多进程上么?对的,它完整的复制了一套thread所提供的接口方便迁移,使每个进程都有自己独立的GIL,这样就不会出现进程之间的GIL争抢。
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。