1. 线程
首先区分线程和进程两个概念:
- 进程是资源(CPU、内存等)分配的基本单位,它是程序执行时的一个实例。程序运行时系统就会创建一个进程,并为它分配资源,然后把该进程放入进程就绪队列,进程调度器选中它的时候就会为它分配CPU时间,程序开始真正运行。
- 线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变量。线程由CPU独立调度执行,在多CPU环境下就允许多个线程同时运行。同样多线程也可以实现并发操作,每个请求分配一个线程来处理。
线程与进程是有区别的,每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
比如谷歌浏览器本身是一个进程,在浏览器中打开一个网页,这就是一个线程,同时打开多个网页,分别用来看资料,玩游戏,翻译文档等,这就是多线程。
线程的优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理。
- 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度
- 程序的运行速度可能加快
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
2.线程函数及模块
Python中使用线程有两种方式:函数或者用类来包装线程对象。
函数式:调用thread模块中的start_new_thread()函数来产生新线程。
语法如下:
thread.start_new_thread ( function, args[, kwargs] )
python3略有不同
_thread.start_new_thread ( function, args[, kwargs] )
也可以这样写:
start_new(function, args, kwargs=None)
参数说明:
- function - 线程函数。
- args - 传递给线程函数的参数,他必须是个tuple类型(不可更改)。
- kwargs - 可选参数。
线程的结束可以调用thread.exit();
例如:
import _thread
import time
def print_time(threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print("%s: %s" % (threadName, time.ctime(time.time())))
try:
_thread.start_new_thread(print_time, ("Thread-1", 2,))
_thread.start_new_thread(print_time, ("Thread-2", 4,))
except:
print("Error: unable to start thread")
while 1:
pass
输出:
Thread-1: Sun Aug 2 09:26:05 2020
Thread-1: Sun Aug 2 09:26:07 2020
Thread-2: Sun Aug 2 09:26:07 2020
Thread-1: Sun Aug 2 09:26:09 2020
Thread-1: Sun Aug 2 09:26:11 2020
Thread-2: Sun Aug 2 09:26:11 2020
Thread-1: Sun Aug 2 09:26:13 2020
Thread-2: Sun Aug 2 09:26:15 2020
Thread-2: Sun Aug 2 09:26:19 2020
Thread-2: Sun Aug 2 09:26:23 2020
运行过程:一共创建了两个线程,线程1和线程2,线程1每次休眠2s,线程2每次休眠4s,开始先创建线程1,打印时间05,经过2s,打印线程1时间,并且现在经过了两个线程1,也就是4s,创建线程2并打印时间09,接下来的4s内继续打印两个线程1的时间,然后过了8s,第二次打印线程2的时间,再到10s时打印线程1的最后一次时间,经过2s打印线程2的第三次时间,最后每隔4s打印一次线程2的时间,最后5次线程1和线程2的时间都打印了出来。
Python通过两个标准库thread和threading提供对线程的支持。thread提供了低级别的、原始的线程以及一个简单的锁。
threading 模块提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 用以表示线程活动的方法。
- start():启动线程活动。
- join([time]): 等待至线程中止。这阻塞调用线程直至线程的join() 方法被调用中止-正常退出或者抛出未处理的异常-或者是可选的超时发生。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
import threading
import time
exitFlag = 0
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print("Starting " + self.name)
print_time(self.name, self.counter, 5)
print("Exiting " + self.name)
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
(threading.Thread).exit()
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
thread1.start()
thread2.start()
print("Exiting Main Thread")
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Sun Aug 2 15:14:36 2020
Thread-2: Sun Aug 2 15:14:37 2020
Thread-1: Sun Aug 2 15:14:37 2020
Thread-1: Sun Aug 2 15:14:38 2020
Thread-1: Sun Aug 2 15:14:39 2020
Thread-2: Sun Aug 2 15:14:39 2020
Thread-1: Sun Aug 2 15:14:40 2020
Exiting Thread-1
Thread-2: Sun Aug 2 15:14:41 2020
Thread-2: Sun Aug 2 15:14:43 2020
Thread-2: Sun Aug 2 15:14:45 2020
Exiting Thread-2
Process finished with exit code 0
开始定义了一个myThread类继承于threading.Thread,并进行初始化,在run函数中先输出一个开始的提示语句,然后调用输出时间函数,并传入参数。时间函数作用是打印当前进程的名称和时间。首先实例化了2个对象thread1,thread2,然后调用start函数开启线程,先开启的是thread1,因此先打印的是thread1的线程开始提示语句,然后是thread2的,接下来输出Exit Main Thread,结束主线程,然后每隔1秒打印一次thread1,每隔2s打印一次thread2,打印完5次后输出Exiting提示语句表明线程完成,当前线程完成。
3. 线程同步
一个进程中往往会有多个线程,当多个进程同时对某一个数据进行修改时就会发生错误,Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法和release方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。
import threading
import time
class myThread(threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print("Starting " + self.name)
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print("%s: %s" % (threadName, time.ctime(time.time())))
counter -= 1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()
print("Exiting Main Thread")
输出:
Starting Thread-1
Starting Thread-2
Thread-1: Sun Aug 2 16:25:55 2020
Thread-1: Sun Aug 2 16:25:56 2020
Thread-1: Sun Aug 2 16:25:57 2020
Thread-2: Sun Aug 2 16:25:59 2020
Thread-2: Sun Aug 2 16:26:01 2020
Thread-2: Sun Aug 2 16:26:03 2020
Exiting Main Thread
可以看见,通过lock锁让一个线程能够使一个进程先运行完再执行另外一个线程,保证在一个线程的执行过程中不会被打扰。
4.线程优先级队列
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
Queue模块中的常用方法:
- Queue.qsize() 返回队列的大小
- Queue.empty() 如果队列为空,返回True,反之False
- Queue.full() 如果队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
- Queue.get_nowait() 相当Queue.get(False)
- Queue.put(item) 写入队列,timeout等待时间
- Queue.put_nowait(item) 相当Queue.put(item, False)
- Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- Queue.join() 实际上意味着等到队列为空,再执行别的操作