线程(thread)是进程(process)中的一个实体,一个进程至少包含一个线程。
1.进程与线程的差别
功能
- 进程,能够完成多任务,比如 在一台电脑上能够同时运行多个QQ
- 线程,能够完成多任务,比如 一个QQ中的多个聊天窗口
定义的不同
- 线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源
区别
- 线程的划分尺度小于进程(资源比进程少),使得多线程程序的并发性高。
- 进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率
优缺点
线程和进程在使用上各有优缺点:线程执行开销小,但不利于资源的管理和保护;而进程正相反。
2.python中多线程
在 Python 中,进行多线程编程的模块有两个:thread 和 threading。
其中,thread 是低级模块,threading 是高级模块,
对 thread 进行了封装,一般来说,我们只需使用 threading 这个模块。
简单的例子:
from threading import Thread, current_threaddef thread_test(name): print 'thread %s is running...' % current_thread().name print 'hello', name print 'thread %s ended.' % current_thread().nameif __name__ == "__main__": print 'thread %s is running...' % current_thread().name print 'hello world!' t = Thread(target=thread_test, args=("test",), name="TestThread") t.start() t.join() print 'thread %s ended.' % current_thread().name
可以看到,创建一个新的线程,就是把一个函数和函数参数传给 Thread 实例,然后调用 start 方法开始执行。代码中的 current_thread 用于返回当前线程的实例。
执行结果如下:
thread MainThread is running...hello world!thread TestThread is running...hello testthread TestThread ended.thread MainThread ended.
3.锁
由于同一个进程之间的线程是内存共享的,所以当多个线程对同一个变量进行修改的时候,就会得到意想不到的结果。
让我们先看一个简单的例子:
from threading import Thread, current_threadnum = 0def calc(): global num print 'thread %s is running...' % current_thread().name for _ in xrange(10000): num += 1 print 'thread %s ended.' % current_thread().nameif __name__ == '__main__': print 'thread %s is running...' % current_thread().name threads = [] for i in range(5): threads.append(Thread(target=calc)) threads[i].start() for i in range(5): threads[i].join() print 'global num: %d' % num print 'thread %s ended.' % current_thread().name
在上面的代码中,我们创建了 5 个线程,每个线程对全局变量 num 进行 10000 次的 加 1 操作,这里之所以要循环 10000 次,是为了延长单个线程的执行时间,使线程执行时能出现中断切换的情况。现在问题来了,当这 5 个线程执行完毕时,全局变量的值是多少呢?是 50000 吗?
让我们看下执行结果:
thread MainThread is running...thread Thread-34 is running...thread Thread-34 ended.thread Thread-35 is running...thread Thread-36 is running...thread Thread-37 is running...thread Thread-38 is running...thread Thread-35 ended.thread Thread-38 ended.thread Thread-36 ended.thread Thread-37 ended.global num: 30668thread MainThread ended.
我们发现 num 的值是 30668,事实上,num 的值是不确定的,你再运行一遍,会发现结果变了。
原因是因为 num += 1 不是一个原子操作,也就是说它在执行时被分成若干步:
由于线程是交替运行的,线程在执行时可能中断,就会导致其他线程读到一个脏值。
为了保证计算的准确性,我们就需要给 num += 1 这个操作加上锁。当某个线程开始执行这个操作时,由于该线程获得了锁,因此其他线程不能同时执行该操作,只能等待,直到锁被释放,这样就可以避免修改的冲突。创建一个锁可以通过 threading.Lock() 来实现,代码如下:
from threading import Thread, current_thread, Locknum = 0lock = Lock()def calc(): global num print 'thread %s is running...' % current_thread().name for _ in xrange(10000): lock.acquire() # 获取锁 num += 1 lock.release() # 释放锁 print 'thread %s ended.' % current_thread().nameif __name__ == '__main__': print 'thread %s is running...' % current_thread().name threads = [] for i in range(5): threads.append(Thread(target=calc)) threads[i].start() for i in range(5): threads[i].join() print 'global num: %d' % num print 'thread %s ended.' % current_thread().name
让我们看下执行结果:
thread MainThread is running...thread Thread-44 is running...thread Thread-45 is running...thread Thread-46 is running...thread Thread-47 is running...thread Thread-48 is running...thread Thread-45 ended.thread Thread-47 ended.thread Thread-48 ended.thread Thread-46 ended.thread Thread-44 ended.global num: 50000thread MainThread ended.
4.ThreadLocal
在多线程环境下,每个线程都有自己的数据。
一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,
不会影响其他线程,而全局变量的修改必须加锁。
ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题
4.1 使用函数传参的方法
但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:
def process_student(name): std = Student(name) # std是局部变量,但是每个函数都要用它,因此必须传进去: do_task_1(std) do_task_2(std)def do_task_1(std): do_subtask_1(std) do_subtask_2(std)def do_task_2(std): do_subtask_2(std) do_subtask_2(std)
每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,
因为每个线程处理不同的Student对象,不能共享。
4.2 使用全局字典的方法
如果用一个全局dict存放所有的Student对象,
然后以thread自身作为key获得线程对应的Student对象如何?
global_dict = {}def std_thread(name): std = Student(name) # 把std放到全局变量global_dict中: global_dict[threading.current_thread()] = std do_task_1() do_task_2()def do_task_1(): # 不传入std,而是根据当前线程查找: std = global_dict[threading.current_thread()] ...def do_task_2(): # 任何函数都可以查找出当前线程的std变量: std = global_dict[threading.current_thread()] ...
这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点low。
有没有更简单的方式?
4.3 使用ThreadLocal的方法
ThreadLocal应运而生,不用查找dict,ThreadLocal帮你自动做这件事:
import threading# 创建全局ThreadLocal对象:local_school = threading.local()def process_student(): # 获取当前线程关联的student: std = local_school.student print('Hello, %s (in %s)' % (std, threading.current_thread().name))def process_thread(name): # 绑定ThreadLocal的student: local_school.student = name process_student()t1 = threading.Thread(target= process_thread, args=('dongGe',), name='Thread-A')t2 = threading.Thread(target= process_thread, args=('老王',), name='Thread-B')t1.start()t2.start()t1.join()t2.join()
执行结果:
Hello, dongGe (in Thread-A)Hello, 老王 (in Thread-B)
说明
全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。
可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。
ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
5.GIL 锁
讲到 Python 中的多线程,就不得不面对 GIL 锁,GIL 锁的存在导致 Python 不能有效地使用多线程实现多核任务,因为在同一时间,只能有一个线程在运行。
GIL 全称是 Global Interpreter Lock,译为全局解释锁。早期的 Python 为了支持多线程,引入了 GIL 锁,用于解决多线程之间数据共享和同步的问题。但这种实现方式后来被发现是非常低效的,当大家试图去除 GIL 的时候,却发现大量库代码已重度依赖 GIL,由于各种各样的历史原因,GIL 锁就一直保留到现在。