一,前言

  • 进程:是程序,资源集合,进程控制块组成,是最小的资源单位
    • 特点:就对Python而言,可以实现真正的并行效果
    • 缺点:进程切换很容易消耗cpu资源,进程之间的通信相对线程来说比较麻烦  
  • 线程:是进程中最小的执行单位。
    • 特点无法利用多核,无法实现真正意义上是并行效果。
    • 优点:对于IO密集型的操作可以很好利用IO阻塞的时间

二,GIL(全局解释器锁)

  python目前有很多解释器,目前使用最广泛的是CPython,还有PYPY和JPython等解释器,但是使用最广泛的还是CPython解释器,而对于全局解释器锁来说,就是在CPython上面才有的,它的原理是在解释器层面加上一把大锁,保证同一时刻只能有一个python线程在解释器中执行。

  对于计算密集型的python多线程来说,无法利用到多线程带来的效果, 在2.7时计算密集型的python多线程执行效率比顺序执行的效率还低的多,在python3.5中对这种情况进行了优化,基本能实现这种多线程执行时间和顺序执行时间差不多的效果。

  对于I/O密集型的python多线程来说,GIL的影响不是很大,因为I/O密集型的python多线程进程,每个线程在等待I/O的时候,将会释放GIL资源,供别的线程来抢占。所以对于I/O密集型的python多线程进程来说,还是能比顺序执行的效率要高的。

  python的GIL这个东西。。。比较恶心,但是由于CPython解释器中的很多东西都是依赖这个东西开发的,如果改的话,将是一件浩大的工程。。。所以到现在还是存在这个问题,这也是python最为别人所诟病的地方。。。

三,多线程

  多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时

写入某个变量的时候,就需要解决同步的问题,比如多线程火车售票系统:两个指令,一个指令检查票是否卖完,另一个指令,多

个窗口同时卖票,可能出现卖出不存在的票。

  3.1 python实现多线程

  python实现多线程有两种方式,分别是直接调用和继承调用,如下实例:

  直接调用:

import threading
import time # 定义线程运行函数
def mv():
print('播放=========')
time.sleep(2)
print('ending=======') # 带参数方式
def play(name):
print("打游戏======"+name)
time.sleep(3)
print("ending======") if __name__ == '__main__':
th = threading.Thread(target=mv)
th2 = threading.Thread(target=play, args=("LOL",))
th.start()
th2.start()

  继承调用:

import threading
import time class MyThread(threading.Thread):
def __init__(self, num):
super(MyThread, self).__init__()
self.num = num def run(self):
print("play game %s======" % self.num)
time.sleep(2)
print("end play") if __name__ == '__main__':
mt = MyThread(1)
mt2 = MyThread(2)
mt.start()
mt2.start()  

  可以看到直接调用是导入threading模块并定义一个函数,之后实例化threading.Thread类的时候,将刚定义的函数名通过target参数传递进去,然后调用实例的start()方法启动一个线程。

  而继承式调用是创建一个类继承自threading.Thread类,并在构造方法中调用父类的构造方法,之后重写run方法,run方法中就是每个线程起来之后执行的内容,就类似于前面通过target参数传递进去的函数。之后以这个继承的类来创建对象,并执行对象的start()方法启动一个线程。

  从这里可以看出,其实。。。直接调用通过使用target参数把函数带进类里面之后应该是用这个函数替代了run方法。

  3.2 线程阻塞和守护线程(join和setDaemon)

  join()方法在该线程对象启动了之后调用线程的join()方法之后,那么主线程将会阻塞在当前位置直到子线程执行完成才继续往下走,如果所有子线程对象都调用了join()方法,那么主线程将会在等待所有子线程都执行完之后再往下执行。

  setDaemon(True)方法在子线程对象调用start()方法(启动该线程)之前就调用的话,将会将该子线程设置成守护模式启动,这是什么意思呢?当子线程还在运行的时候,父线程已经执行完了,如果这个子线程设置是以守护模式启动的,那么随着主线程执行完成退出时,子线程立马也退出,如果没有设置守护启动子线程(也就是正常情况下)的话,主线程执行完成之后,进程会等待所有子线程执行完成之后才退出。

  join示例:

import threading
import time # 定义线程运行函数
def mv():
print('播放=========')
time.sleep(2)
print('ending=======') # 带参数方式
def play(name):
print("打游戏======"+name)
time.sleep(3)
print("ending======") games = ["LOL", "DNF", "PUBG"] if __name__ == '__main__':
th = threading.Thread(target=mv)
th.start()
# th.join() # 带上该代码,程序(也就是主线程)不会马上往下执行,回显执行完th线程
for i in [threading.Thread(target=play, args=(i, )) for i in games]:
i.start()

  setDaemon示例:

import threading
import time # 定义线程运行函数
def mv():
print('播放=========')
time.sleep(5)
print('ending=======') if __name__ == '__main__':
th = threading.Thread(target=mv)
th.setDaemon(True) # 若有改行代码,程序执行直接退出,不打印出 ending=====,因为主线程执行完毕了直接退出。
th.start()

  3.3 线程锁

  互斥锁的产生是因为前面提到过多线程之间是共享同一块内存地址的,也就是说多个不同的线程能够访问同一个变量中的数据,那么,当多个线程要修改这个变量,会产生什么情况呢?当多个线程修改同一个数据的时候,如果操作的时间够短的话,能得到我们想要的结果,但是,如果修改数据不是原子性的(这中间的时间太长)的话。。。很有可能造成数据的错误覆盖,从而得到我们不想要的结果。例子如下:

import threading
import time count = 0 def add_num():
global count
tmp = count
time.sleep(0.001) # 若没有改代码,输出100,若有改代码输出是10,11,9等不确定是数
count = tmp + 1 def run(add_fun):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=add_fun)
t.start()
thread_list.append(t)
for j in thread_list:
j.join() print(count) if __name__ == '__main__':
run(add_num)

  出现上述情况的原因:是因为,尽管count+=1是非原子操作,但是因为CPU执行的太快了,比较难以复现出多进程的非原子操作导致的进程不安全。经过代替之后,尽管只sleep了0.001秒,但是对于CPU的时间来说是非常长的,会导致这个代码块执行到一半,GIL锁就释放了。即tmp已经获取到count的值了,但是还没有将tmp + 1赋值给count。而此时其他线程如果执行完了count = tmp + 1, 当返回到原来的线程执行时,尽管count的值已经更新了,但是count = tmp + 1是个赋值操作,赋值的结果跟count的更新的值是一样的。最终导致了我们累加的值有很多丢失。

  互斥锁

  针对上面的情况,我们就需要引入互斥锁的这一概念。某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。

import threading
import time count = 0 def add_num():
global count
if lock.acquire(): # 获得锁,并返回True
tmp = count
time.sleep(0.001)
count = tmp + 1
lock.release() # 执行完释放锁 def run(add_fun):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=add_fun)
t.start()
thread_list.append(t)
for j in thread_list:
j.join() print(count) if __name__ == '__main__':
lock = threading.Lock()
run(add_num)

  另一种获得锁的方式(with):

import threading
import time count = 0 def add_num():
global count
with lock: # 获得锁,并返回True
tmp = count
time.sleep(0.001)
count = tmp + 1 def run(add_fun):
global count
thread_list = []
for i in range(100):
t = threading.Thread(target=add_fun)
t.start()
thread_list.append(t)
for j in thread_list:
j.join() print(count) if __name__ == '__main__':
lock = threading.Lock()
run(add_num)

  迭代死锁

import threading
import time class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(): # 第一次获得锁
num = num+1
msg = self.name+' set num to '+str(num)
print(msg)
mutex.acquire() # 在锁未释放的时候第二次获得锁,需要注意的是这里的锁指的是同一个锁对象mutex
mutex.release()
mutex.release() num = 0
mutex = threading.Lock() def test():
for i in range(5):
t = MyThread()
t.start() if __name__ == '__main__':
test() # 无输出,一直阻塞,因为没有释放锁,又想再次获得锁,该锁已经被拿走了,是空的就会一直等待锁释放

  上述代码中,无输出,一直阻塞,因为没有释放锁,又想再次获得锁,该锁已经被拿走了,是空的就会一直等待锁释放使用的是同一个锁对象muex,若创建一个新的锁对象,就不会出现这些情况。

  相互调用锁

import threading
import time def fun1():
print('=====')
lock1.acquire()
time.sleep(1)
print('-------')
lock2.acquire() # lock2在另一线程使用,为释放
time.sleep(2)
lock1.release()
lock2.release() def fun2():
print("-------")
lock2.acquire()
print("locl2====")
time.sleep(0.1)
lock1.acquire() # lock1在上一线程使用未释放
print("lock3=====")
time.sleep(1)
lock1.release()
lock2.release() lock1 = threading.Lock()
lock2 = threading.Lock() def run():
th =threading.Thread(target=fun1)
th2 =threading.Thread(target=fun2)
th.start()
th2.start() if __name__ == '__main__':
run()

  像上面这种情况,锁未释放就获取另一把锁,而另一把锁已经在使用,同时另一线程反过来调用第一把锁,从而产生这种相互等待锁的阻塞情况。

  死锁解决

  为了支持在同一线程中多次请求同一资源,python提供了“可重入锁”:threading.RLock。RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。这里以例1为例,如果使用RLock代替Lock,则不会发生死锁:

import threading
import time
class MyThread(threading.Thread):
def run(self):
global num
time.sleep(1)
if mutex.acquire(1):
num = num+1
msg = self.name+' set num to '+str(num)
print msg
mutex.acquire()
mutex.release()
mutex.release()
num = 0
mutex = threading.RLock()
def test():
for i in range(5):
t = MyThread()
t.start()
if __name__ == '__main__':
test()

  3.5 信号量

  Semaphore管理一个内置的计数器

  Semaphore与进程池看起来类似,但是是完全不同的概念。

  进程池:Pool(4),最大只能产生四个进程,而且从头到尾都只是这四个进程,不会产生新的。

  信号量:信号量是产生的一堆进程/线程,即产生了多个任务都去抢那一把锁

from threading import Thread,Semaphore,currentThread
import time,random
sm = Semaphore(5) #运行的时候有5个人
def task():
sm.acquire()
print('\033[42m %s上厕所'%currentThread().getName())
time.sleep(random.randint(1,3))
print('\033[31m %s上完厕所走了'%currentThread().getName())
sm.release()
if __name__ == '__main__':
for i in range(20): #开了10个线程 ,这20人都要上厕所
t = Thread(target=task)
t.start() Semaphore举例

四,线程池

  线程池在系统启动时即创建大量空闲的线程,程序只要将一个函数提交给线程池,线程池就会启动一个空闲的线程来执行它。当该函数执行结束后,该线程并不会死亡,而是再次返回到线程池中变成空闲状态,等待执行下一个函数。
  此外,使用线程池可以有效地控制系统中并发线程的数量。当系统中包含有大量的并发线程时,会导致系统性能急剧下降,甚至导致 Python 解释器崩溃,而线程池的最大线程数参数可以控制系统中并发线程的数量不超过此数。

  4.1 介绍和基本方法

官网:https://docs.python.org/dev/library/concurrent.futures.html

concurrent.futures模块提供了高度封装的异步调用接口
ThreadPoolExecutor:线程池,提供异步调用
Both implement the same interface, which is defined by the abstract Executor class.
1、submit(fn, *args, **kwargs)
异步提交任务 2、map(func, *iterables, timeout=None, chunksize=1)
取代for循环submit的操作 3、shutdown(wait=True)
相当于进程池的pool.close()+pool.join()操作
wait=True,等待池内所有任务执行完毕回收完资源后才继续
wait=False,立即返回,并不会等待池内的任务执行完毕
但不管wait参数为何值,整个程序都会等到所有任务执行完毕
submit和map必须在shutdown之前 4、result(timeout=None)
取得结果 5、add_done_callback(fn)
回调函数

  4.2  线程池的使用

from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor

import os,time,random
def task(n):
print('%s is runing' %os.getpid())
time.sleep(random.randint(1,3))
return n**2 if __name__ == '__main__': executor=ProcessPoolExecutor(max_workers=3) futures=[]
for i in range(11):
future=executor.submit(task,i)
futures.append(future)
executor.shutdown(True)
print('+++>')
for future in futures:
print(future.result())

  4.3 异步调用和同步调用

  1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,在执行下一行代码,导致程序是串行

  2、异步调用:提交完任务后,不用原地等待任务执行完毕

  回调函数:可以为进程池或线程池内得每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接受任务的返回值当作参数,该函数成为回调函数。

#提交任务的两种方式
#1、同步调用:提交完任务后,就在原地等待任务执行完毕,拿到结果,再执行下一行代码,导致程序是串行执行
#
# from concurrent.futures import ThreadPoolExecutor
# import time
# import random
#
# def la(name):
# print('%s is laing' %name)
# time.sleep(random.randint(3,5))
# res=random.randint(7,13)*'#'
# return {'name':name,'res':res}
#
# def weigh(shit):
# name=shit['name']
# size=len(shit['res'])
# print('%s 拉了 《%s》kg' %(name,size))
#
#
# if __name__ == '__main__':
# pool=ThreadPoolExecutor(13)
#
# shit1=pool.submit(la,'alex').result()
# weigh(shit1)
#
# shit2=pool.submit(la,'wupeiqi').result()
# weigh(shit2)
#
# shit3=pool.submit(la,'yuanhao').result()
# weigh(shit3) #2、异步调用:提交完任务后,不地等待任务执行完毕, from concurrent.futures import ThreadPoolExecutor
import time
import random def la(name):
print('%s is laing' %name)
time.sleep(random.randint(3,5))
res=random.randint(7,13)*'#'
return {'name':name,'res':res} def weigh(shit):
shit=shit.result()
name=shit['name']
size=len(shit['res'])
print('%s 拉了 《%s》kg' %(name,size)) if __name__ == '__main__':
pool=ThreadPoolExecutor(13) pool.submit(la,'alex').add_done_callback(weigh) pool.submit(la,'wupeiqi').add_done_callback(weigh) pool.submit(la,'yuanhao').add_done_callback(weigh)

五,总结

  对应IO密集型任务可以通过创建线程池来提高效率,而对于计算密集型则没必要,计算密集型可以考虑分布式计算。

Python 多线程和线程池的更多相关文章

  1. Python多线程、线程池及实际运用

    我们在写python爬虫的过程中,对于大量数据的抓取总是希望能获得更高的速度和效率,但由于网络请求的延迟.IO的限制,单线程的运行总是不能让人满意.因此有了多线程.异步协程等技术. 下面介绍一下pyt ...

  2. python爬虫14 | 就这么说吧,如果你不懂python多线程和线程池,那就去河边摸鱼!

    你知道吗? 在我的心里 你是多么的重要 就像 恩 请允许我来一段 freestyle 你们准备好了妹油 你看 这个碗 它又大又圆 就像 这条面 它又长又宽 你们 在这里 看文章 觉得 很开心 就像 我 ...

  3. python爬虫之线程池和进程池

    一.需求 最近准备爬取某电商网站的数据,先不考虑代理.分布式,先说效率问题(当然你要是请求的太快就会被封掉,亲测,400个请求过去,服务器直接拒绝连接,心碎),步入正题.一般情况下小白的我们第一个想到 ...

  4. python day 20: 线程池与协程,多进程TCP服务器

    目录 python day 20: 线程池与协程 2. 线程 3. 进程 4. 协程:gevent模块,又叫微线程 5. 扩展 6. 自定义线程池 7. 实现多进程TCP服务器 8. 实现多线程TCP ...

  5. C#多线程之线程池篇3

    在上一篇C#多线程之线程池篇2中,我们主要学习了线程池和并行度以及如何实现取消选项的相关知识.在这一篇中,我们主要学习如何使用等待句柄和超时.使用计时器和使用BackgroundWorker组件的相关 ...

  6. C#多线程之线程池篇2

    在上一篇C#多线程之线程池篇1中,我们主要学习了如何在线程池中调用委托以及如何在线程池中执行异步操作,在这篇中,我们将学习线程池和并行度.实现取消选项的相关知识. 三.线程池和并行度 在这一小节中,我 ...

  7. C#多线程之线程池篇1

    在C#多线程之线程池篇中,我们将学习多线程访问共享资源的一些通用的技术,我们将学习到以下知识点: 在线程池中调用委托 在线程池中执行异步操作 线程池和并行度 实现取消选项 使用等待句柄和超时 使用计时 ...

  8. 重新想象 Windows 8 Store Apps (42) - 多线程之线程池: 延迟执行, 周期执行, 在线程池中找一个线程去执行指定的方法

    [源码下载] 重新想象 Windows 8 Store Apps (42) - 多线程之线程池: 延迟执行, 周期执行, 在线程池中找一个线程去执行指定的方法 作者:webabcd 介绍重新想象 Wi ...

  9. ExecutorService 建立一个多线程的线程池的步骤

    ExecutorService 建立一个多线程的线程池的步骤: 线程池的作用: 线程池功能是限制在系统中运行的线程数. 依据系统的环境情况,能够自己主动或手动设置线程数量.达到执行的最佳效果:少了浪费 ...

随机推荐

  1. kettle使用文件导入到Postgresql出现如下几种问题的总结

    1.kettle使用文件导入到Postgresql出现如下几种问题的总结: kettle使用文件导入到Postgresql出现如下几种问题的总结: .第一种错误,报错如ERROR: extra dat ...

  2. ansible的delegate_to、connection、和local_action

    由于工作需要,经常需要把目标节点获得的信息写入执行节点文件日志. 所以经常用到delegate_to和connection,而local_action写法难看,基本不用. delegate_to和co ...

  3. 【Android】onNewIntent调用时机

    在IntentActivity中重写下列方法:onCreate onStart onRestart onResume onPause onStop onDestroy onNewIntent一.其他应 ...

  4. WIN10在安装mysql时,出现“The security settings could not be applied to the database because the connection has failed with the following error. Error Nr. 1045

    解决方法:1, 首先卸载MySQL2, 再根据这个目录 C:\ProgramData,将MySQL删除.3, 重新安装MySQL 就好了(电脑不用重启)

  5. BZOJ-10-1176: [Balkan2007]Mokia-CDQ第二类应用

    思路 :按照操作的时间进行分治,这样转化成了 时间t ,x坐标,y坐标 经典的三维偏序. 最初时间就是递增顺序,无需排序直接进行第二维的分治,类似归并排序处理x坐标,在保证 x有序的情况下进行更新y坐 ...

  6. (转)InFluxDB数据库使用手册

    InfluxDB是一个开源的时序数据库,使用GO语言开发,特别适合用于处理和分析资源监控数据这种时序相关数据.而InfluxDB自带的各种特殊函数如求标准差,随机取样数据,统计数据变化比等,使数据统计 ...

  7. lArea.js 城市选择

    http://blog.csdn.net/libin_1/article/details/50689075 lArea.js

  8. XIV Open Cup named after E.V. Pankratiev. GP of America

    A. Ancient Diplomacy 建图,同色点间边权为$0$,异色点间边权为$1$,则等价于找一个点使得到它最短路最长的点的最短路最小,Floyd即可. 时间复杂度$O(n^3)$. #inc ...

  9. ubuntu重复登录问题

    第一次遇到: 昨天好不容易装好了驱动,紧接着装了CUDA,cuDNN,Anaconda,VSCode等等.然后安装pytorch的时候遇到了下载的问题,后来也算搞定了.但是在更换了显示器后重启(好像是 ...

  10. Vue----常见面试题

    1. 谈谈你对MVVM开发模式的理解 MVVM分为Model.View.ViewModel三者. Model 代表数据模型,数据和业务逻辑都在Model层中定义: View 代表UI视图,负责数据的展 ...