Python多线程适用于I/O密集型任务,Python多进程适用于计算密集型任务。
线程包括:主线程、子线程、守护线程(后台线程)和前台线程。
GIL(全局解释器锁)让Python的多线程并非真正的多线程,解释器解释执行任何Python代码时,都需要先获得这个锁,在遇到 I/O 操作时会释放这把锁。Python的进程作为一个整体,解释器进程内只有1个线程在执行,其它线程都处于等待状态等着GIL的释放。
通俗来说,GIL的存在让同一个时刻只能有1个线程在执行,多线程并非真正的并发,只是交替地执行。Python的多线程并发,效果可能还不如单线程(因为线程切换会耗时间),想要利用多核CPU,可以考虑多进程。
import time
import threading
class MyThread(threading.Thread):
def run(self):
for i in range(4):
print('thread {}, @number: {}'.format(self.name, i))
time.sleep(2)
def main():
print('Start main threading')
threads = [MyThread() for i in range(3)]
for t in threads:
t.start()
print('End main threading')
if __name__ == '__main__':
main()
#代码输出:
Start main threading #主线程开始
thread Thread-13, @number: 0 #这三行表示:3个线程几乎同时运行了
thread Thread-14, @number: 0
thread Thread-15, @number: 0
End main threading #主线程结束
thread Thread-13, @number: 1 #后面的3个3行结果块表示:3个线程在主线程结束后继续交互/同时运行着
thread Thread-14, @number: 1
thread Thread-15, @number: 1
thread Thread-15, @number: 2
thread Thread-14, @number: 2
thread Thread-13, @number: 2
thread Thread-15, @number: 3
thread Thread-14, @number: 3
thread Thread-13, @number: 3
join 方法:阻塞当前父线程,直到被调用join 方法的子线程运行结束。通俗来说就是线程A正在运行,这时线程B(和C)插队,则A被阻塞从而暂停运行,A等待B(和C)运行结束后才继续运行。需要强调的是,若有C,则B和C之间还是并行交互同时运行的,没有谁阻塞谁,因为B和C之间并不是父/子关系。
join 方法设置参数,则表示子线程阻塞父线程的时间,过了这时间,父线程继续运行,不管子线程是个啥状态(亲测:此时父线程并不kill子线程,而有些博客说法是会kill子线程)。
#1.2.1 修改1.1中代码的main函数,添加2行
def main():
print('Start main threading')
threads = [MyThread() for i in range(3)]
for t in threads: #在主线程里,3个子线程开始运行,主线程也在运行
t.start()
for t in threads: #子线程插队,主线程暂停运行,3个子线程同时交互运行
t.join()
print('Next FOR operation')
print('End main threading')
#代码输出:
Start main threading
thread Thread-20, @number: 0
thread Thread-21, @number: 0
thread Thread-22, @number: 0
thread Thread-20, @number: 1
thread Thread-22, @number: 1
thread Thread-21, @number: 1
thread Thread-20, @number: 2
thread Thread-21, @number: 2
thread Thread-22, @number: 2
thread Thread-20, @number: 3
thread Thread-21, @number: 3
thread Thread-22, @number: 3
Next FOR operation #可以看到,第1个子线程就阻塞了主线程,其他2个子线程还没来得及阻塞,本身结束了
Next FOR operation
Next FOR operation
End main threading #只有当3个子线程都结束了,主线程才继续运行
#1.2.2 修改main函数如下,结果与1.2.1一样,但本质不一样:
#在1.2.1中,是3个子线程都会阻塞主线程,第1个子线程阻塞完了之后,若第2个子线程还没运行完,则再次阻塞主线程,只要当3个子线程都结束后,主线程才会继续运行;
#而在这里,只有第2个子线程阻塞主线程,当第2个子线程结束后,不管第1、3子线程是否结束,主线程都会继续运行。结果一样,只是因为3个子线程几乎是同时结束。
def main():
print('Start main threading')
threads = [MyThread() for i in range(3)]
for t in threads:
t.start()
threads[1].join() #第2个子线程阻塞主线程,此处也可以是threads[0]或threads[2]
print('End main threading')
#1.2.3 修改main函数如下,体会与1.2.1和1.2.2中输出的差别
def main():
print('Start main threading')
threads = [MyThread() for i in range(3)]
#每个子线程开始运行后紧接着就插队,则阻塞主线程(包括这个for循环,导致后面子线程并没有开始运行)
for t in threads:
t.start()
t.join()
print('Next FOR operation')
print('End main threading')
#代码输出:如下所示,主线程main里的for循环被当前子线程阻塞后,后面的子线程也被阻塞,所以3个子线程是一个结束后才运行下一个
Start main threading
thread Thread-23, @number: 0
thread Thread-23, @number: 1
thread Thread-23, @number: 2
thread Thread-23, @number: 3
Next FOR operation
thread Thread-24, @number: 0
thread Thread-24, @number: 1
thread Thread-24, @number: 2
thread Thread-24, @number: 3
Next FOR operation
thread Thread-25, @number: 0
thread Thread-25, @number: 1
thread Thread-25, @number: 2
thread Thread-25, @number: 3
Next FOR operation
End main threading
线程比进程轻量,一个原因就是它们共享内存,即各线程可以平等的访问内存数据,如果短时间内“同时并行”读取修改内存的数据,很可能造成数据不同步。因此,同时并行多线程时要小心!特别是各子线程要同时修改内存数据时。
为了避免线程不同步造成是数据不同步,可以对资源进行加锁,即线程需要先获得锁来锁住资源,才能访问资源。方法为:在访问资源之前判断是否获得了锁,访问资源之后解除锁。可以借助 threading 模块的 Lock 功能。
#创建锁
mutex = threading.Lock()
#获取锁,锁住资源,访问资源
if mutex.acquire():
#访问资源的操作……
#释放锁,不再锁住资源
mutex.release()
有时候,会出现死锁现象,即多个子线程都需要先获得锁才能运行,但获得锁需要其他线程释放锁,而其他线程并没运行,无法释放锁。通俗来说,此时陷入死胡同,即为死锁。
另外一种说法:有2个global资源a和b,有2个线程t1和t2,若t1占用a,想访问b,但此时t2占用b,想访问a,2个线程都不释放此时拥有的资源,即为死锁。
import time
import threading
mutex_a = threading.Lock()
mutex_b = threading.Lock()
class MyThread(threading.Thread):
def task_a(self):
if mutex_a.acquire():
……
if mutex_b.acquire():
……
mutex_b.release()
mutex_a.release()
def task_b(self):
if mutex_b.acquire():
……
if mutex_a.acquire():
……
mutex_a.release()
mutex_b.release()
def run(self):
self.task_a()
self.task_b()
def main():
print "Start main threading"
threads = [MyThread() for i in range(2)]
for t in threads:
t.start()
print "End Main threading"
以上代码输出结果为空!
可重入锁:为了在同一线程中多次请求同一资源,Python提供了可重入锁(RLock)。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,使得资源可以被多次require,直到一个线程所有的acquire都被release,其他线程才能获得资源。详细内容省略。
条件变量锁:一些复杂的情况下,需要针对锁进行一些条件判断。Python提供了Condition对象,除acquire和release方法之外,它还提供wait和notify方法。线程首先acquire一个条件变量锁。如果条件不成立,则该线程wait;如果成立就运行该线程,也可以notify其他线程。其他处于wait状态的线程接到通知后会重新判断条件。条件变量锁可以看成是:不同的线程先后acquire获得锁,如果条件不成立,则被扔到一个Lock/RLock的waiting池,直达其他线程notify之后再重新判断条件。该模式常用于生成者消费者模式。
import time
import threading
import random
queue = []
con = threading.Condition()
class Producer(threading.Thread):
def run(self):
while True:
#第1个con.aqcuire()总为True,此时当某线程con.acquire()为True后,其他线程con.acquire()为False,无法运行
if con.acquire():
if len(queue) > 5: #若已经生成5个产品了,则暂停生产
con.wait()
else:
elem = random.randrange(5)
queue.append(elem)
print("Produce elem {}, size is {}".format(elem, len(queue)))
time.sleep(0.5)
con.notify() #生产者每生产1个产品,就通知消息者来消费,此时并没释放锁
con.release() #显式地释放锁(以让消费者线程可以运行,即消费产品)
time.sleep(2) #暂停2秒,以让消费者线程可以抢到刚才释放的锁
class Consumer(threading.Thread):
def run(self):
while True:
if con.acquire():
if len(queue) < 0: #若没有剩余产品,则消费者等待
con.wait()
else:
elem = queue.pop()
print("Consume elem {}, size is {}".format(elem, len(queue)))
time.sleep(0.5)
con.notify() #消费者每消费1个产品,就通知生产者去生产,此时并没有释放锁
con.release() #显式地释放锁(以让生产者线程可以运行,即生产产品)
time.sleep(2) #暂停2秒,以让生产者线程可以抢到刚才释放的锁
def main():
for i in range(3):
Producer().start()
for i in range(2):
Consumer().start()
if __name__ == '__main__':
main()
#代码输出(一直运行下去,不会停止!):
Produce elem 3, size is 1
Consume elem 3, size is 0
Produce elem 2, size is 1
Produce elem 3, size is 2
Consume elem 3, size is 1
Produce elem 2, size is 2
Produce elem 0, size is 3
Consume elem 0, size is 2
Produce elem 0, size is 3
Produce elem 3, size is 4
Consume elem 3, size is 3
Produce elem 1, size is 4
Produce elem 1, size is 5
Consume elem 1, size is 4
Produce elem 1, size is 5
Produce elem 4, size is 6
Consume elem 4, size is 5
Produce elem 4, size is 6
……
以上生产者消费者模型是对队列进行操作,在Python中有队列结构Queue,其内部实现了锁的相关设置。用Queue重写生产者消费者模型如下:
import queue #Python2中是Queue,Python3中改成queue
import threading
import time
import random
q5 = queue.Queue(5) #队列容量只有5,多于5,暂停生产
class Producer(threading.Thread):
def run(self):
while True:
elem = random.randrange(5)
q5.put(elem) #当队列为满时,Queue.put会被阻塞,释放锁,直到消费者消费后队列不满
print("Produce elem {}, size is {}".format(elem, queue.qsize()))
time.sleep(2)
class Consumer(threading.Thread):
def run(self):
while True:
elem = q5.get() #当队列为空时,Queue.get会被阻塞,释放锁,直到生产者生产后队列不空
q5.task_done() #当队列进行get后会fetch一个task时,task_done告诉队列该task完成
print("Consume elem {}, size is {}".format(elem, queue.qsize()))
time.sleep(2)
def main():
for i in range(3):
Producer().start()
for i in range(2):
Consumer().start()
if __name__ == '__main__':
main()
#代码输出:类似1.3.3中结果
Produce elem 4, size is 1
Produce elem 1, size is 2
Produce elem 1, size is 3
Consume elem 4, size is 2
Consume elem 1, size is 1
Produce elem 4, size is 2
Produce elem 1, size is 3
Produce elem 3, size is 4
Consume elem 1, size is 3
Consume elem 4, size is 2
Consume elem 1, size is 1
……
Consume elem 1, size is 3
Produce elem 2, size is 4
Produce elem 4, size is 5
……
当不想与其他线程共享某变量时,可以定义为local变量,但在函数中定义的local变量在函数之间传递特别麻烦。此时可以使用ThreadLocal,它定义时为global变量,各子线程都可使用它,但使用它时该global变量会变成子线程内部的局部变量,其他线程不可理性。
import threading
def test(name):
print('Current thread: {}'.format(threading.currentThread().name))
local.name = name #某线程在使用别的线程创建的ThreadLocal变量时,会变成当前线程的局部变量
print("{} in {}".format(local.name, threading.currentThread().name))
local = threading.local() #声明local为ThreadLocal类型,在父线程中是全局变量,各子线程可以使用
t1 = threading.Thread(target=test, args=('Tom',))
t2 = threading.Thread(target=test, args=('Lina',))
t1.start()
t2.start()
#代码输出:
Current thread: Thread-31
Tom in Thread-31
Current thread: Thread-32
Lina in Thread-32
threading 模块的 Event 接口的 wait 和 set 方法分别用于阻塞和唤醒线程,起到控制线程的作用。
import threading
import time
class MyThread(threading.Thread):
def __init__(self, event):
super(MyThread, self).__init__()
self.event = event
def run(self):
print("thread {} is ready ".format(self.name))
self.event.wait() #线程被阻塞(等待event.set方法来唤醒)
print("thread {} run".format(self.name))
signal = threading.Event() #声明一个Event类实例signal,用于控制所有用signal进行初始化操作的线程
def main():
start = time.time()
for i in range(3):
t = MyThread(signal) #用Event类实例signal来初始化3个线程
t.start()
time.sleep(3)
print("after {}s".format(time.time() - start))
signal.set() #3个线程一直处于wait状态被阻塞,set方法唤醒所有相关线程
if __name__ == '__main__':
main()
#代码输出:
thread Thread-11 is ready
thread Thread-12 is ready
thread Thread-13 is ready
after 3.003596544265747s
thread Thread-12 run
thread Thread-13 run
thread Thread-11 run
在C/C++中,主线程结束后,其子线程会默认被主线程kill掉;而在Python中会默认等待子线程结束后,主线程才退出。若想让主线程一结束,子线程不管是否运行结束,都会被kill掉,可以让其daemon属性为True使子线程变成主线程的后台线程,从而跟随主线程一起结束。
#代码有问题,输出结果不对,daemon属性不好使了?……
import threading
import time
class MyThread(threading.Thread):
def run(self):
print('thread {} will wait {}s'.format(self.name, 2))
time.sleep(2)
print("thread {} finished".format(self.name))
def main():
print("Start main threading")
for i in range(3):
t = MyThread()
t.daemon = True #让子线程设置为主线程的后台线程
t.start()
print("End Main threading")
if __name__ == '__main__':
main()
#代码输出:上面代码有问题
Start main threading
thread Thread-85 will wait 2s
thread Thread-86 will wait 2s
thread Thread-87 will wait 2s
End Main threading
thread Thread-85 finished
thread Thread-87 finished
thread Thread-86 finished
Python 中有2个库包含 map 函数: multiprocessing 和其子库 multiprocessing.dummy.dummy,后者是前者的完整克隆,唯一的不同在于 multiprocessing 用于进程,而 dummy 用于线程。
map使用形式为map(func, [arg1,arg2,…,argn]),map负责将每个参数argi传给函数func,可以把线程分给不同的CPU,完成序列操作、参数传递和结果保存等一系列的操作。
from urllib.request import urlopen
from multiprocessing.dummy import Pool
pool = Pool() #创建线程池,线程个数默认为机器CPU核数,也可指定(但个数太多,线程之间的切换会很消耗资源的)
urls = ['http://www.zhihu.com', 'http://www.126.com', 'http://cn.bing.com']
results = pool.map(urlopen, urls) #将不同的url传给各自线程,并返回各自执行结果给列表results
print(results)
pool.close()
print('main ended')
#代码输出:
[.client.HTTPResponse object at 0x7f909000e518>, .client.HTTPResponse object at 0x7f909000e748>, .client.HTTPResponse object at 0x7f909068dc88>]
main ended
多进程:进程包multiprocessing,只需定义一个函数即可,支持子进程、通信和共享数据、执行不同形式的同步,提供了Process、Queue、Pipe、Lock等组件。
创建进程的类:Process([group [, target [, name [, args [, kwargs]]]]]),target表示调用对象,name为别名,args表示调用对象的位置参数元组,kwargs表示调用对象的字典,group实质上不使用。
使用Process构造函数的target和args参数:target=一个函数名,args=该函数要用的参数元组
#创建多个函数,并作为多个进程
import multiprocessing
import time
def worker_1(interval):
print("worker_1")
time.sleep(interval)
print("end worker_1")
def worker_2(interval):
print("worker_2")
time.sleep(interval)
print("end worker_2")
def worker_3(interval):
print("worker_3")
time.sleep(interval)
print("end worker_3")
if __name__ == "__main__":
p1 = multiprocessing.Process(target = worker_1, args = (1,))
p2 = multiprocessing.Process(target = worker_2, args = (2,))
p3 = multiprocessing.Process(target = worker_3, args = (3,))
p1.start() #3个进程开始并行执行
p2.start()
p3.start()
print("The number of CPU is:" + str(multiprocessing.cpu_count()))
for p in multiprocessing.active_children():
print("child p.name: " + p.name + "\tp.id:" + str(p.pid))
print("END!!!!")
#代码输出:
worker_1 #第1个进程开始执行
The number of CPU is:8
child p.name: Process-5 p.id:29126
child p.name: Process-3 p.id:29122
child p.name: Process-4 p.id:29123
END!!!!
worker_2 #第2个进程开始执行,启动进程稍微花点时间,导致主进程的代码先执行了
worker_3
end worker_1
end worker_2
end worker_3
在代码
p3.start()
后面加上一行代码:time.sleep(0.5)
#代码输出:
worker_1
worker_2
worker_3 #0.5秒的时间足够进程2和3也启动了,然后再执行主进程代码
The number of CPU is:8
child p.name:Process-12 p.id3610
child p.name:Process-14 p.id3622
child p.name:Process-13 p.id3611
END!!!!
end worker_1
end worker_2
end worker_3
在类的
__init__
函数里实现Process.__init__(self)
,在run
函数里实现具体功能。
import multiprocessing
import time
class ClockProcess(multiprocessing.Process):
def __init__(self, interval):
multiprocessing.Process.__init__(self)
self.interval = interval
def run(self):
for i in range(5):
print("time is {0}".format(time.ctime()))
time.sleep(self.interval)
if __name__ == '__main__':
p = ClockProcess(2)
p.start()
#代码输出:
time is Mon Jun 20 18:58:52 2016
time is Mon Jun 20 18:58:54 2016
time is Mon Jun 20 18:58:56 2016
time is Mon Jun 20 18:58:58 2016
time is Mon Jun 20 18:59:00 2016
daemon属性决定进程是否为后台进程,后台进程会随主进程的结束而结束(但在实际操作中,并没有这样,为什么?)
#daemon属性为默认聚会False
import multiprocessing
import time
def worker(interval):
print("work start: {0}".format(time.ctime()));
time.sleep(interval)
print("work end: {0}".format(time.ctime()));
if __name__ == "__main__":
p = multiprocessing.Process(target=worker, args=(2,))
#p.daemon = True
p.start()
print("end!")
#代码输出:
end!
work start: Mon Jun 20 19:10:41 2016
work end: Mon Jun
注意,当加上
p.daemon = True
后,输出仍然不变!?原因还在调查中……设置后台进程执行完的方法是在主进程代码中加上一行
p.join()
Lock 可以限制当前资源只给一个进程使用,当多个进程需要访问共享资源时,可用来避免访问冲突。
import multiprocessing
def worker_with(lock):
with lock: #有lock,一直自己使用,直到该进程运行结束
for i in range(5):
print("Lock acquired via with")
def worker_no_with(lock):
lock.acquire()
for i in range(5):
print("Lock acquired directly")
lock.release()
if __name__ == "__main__":
lock = multiprocessing.Lock()
w = multiprocessing.Process(target=worker_with, args=(lock,))
nw = multiprocessing.Process(target=worker_no_with, args=(lock,))
w.start()
nw.start()
print("end")
#代码输出:
end
Lock acquired via with
Lock acquired via with
Lock acquired via with
Lock acquired via with
Lock acquired via with
Lock acquired directly #lock一直被worker_with使用,直到它使用完了,worker_no_with才运行
Lock acquired directly
Lock acquired directly
Lock acquired directly
Lock acquired directly
Semaphore用来控制对共享资源的访问数量,如池的最大连接数
说实话,没看懂。。。
Event用来实现进程间的同步通信
待续……
Queue是多进程安全队列,可以实现多进程间的数据传递,主要有put和get方法,分别用于插入数据到队列中和从队列中读取并删除一个元素。
待续……
Pipe方法返回(conn1,conn2)表示一个管道的两端
待续……
Pool可以提供指定数量的进程供用户调用。当新请求提交到pool时,若没满,则创建一个新的进程执行该请求;若已满,则请求会等待,直到pool中有进程结束,才会创建新的进程来执行该请求。
待续……
参考:http://python.jobbole.com/85050/
参考:http://python.jobbole.com/85177/
参考:http://www.cnblogs.com/kaituorensheng/p/4445418.html