在一个进程的内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”叫做线程。
线程通常叫做轻型的进程,线程是共享内存空间的并发执行的多任务,每一个线程都共享一个进程的资源。
线程是最小的执行单元,而进程由至少一个线程组成。如何调度进程和线程完全由操作系统决定,程序不能控制。
任何进程都会启动一个线程,称为主线程,主线程可以启动新的子线程。
import threading
import time
def run():
print('子线程(%s)开始' % threading.current_thread().name)
#线程要执行的功能
print('********')
time.sleep(2)
print('子线程(%s)结束' % threading.current_thread().name)
if __name__ == '__main__':
print('主线程(%s)启动' % threading.current_thread().name)
t = threading.Thread(target = run, name = 'runThread') #创建子线程
t.start() #启动子线程
t.join() #等待子线程结束后在关闭主线程
print('主线程(%s)结束' % threading.current_thread().name)
输出结果:
C:\pyFile\module\prosess>python thread_1.py
主线程(MainThread)启动
子线程(runThread)开始
********
子线程(runThread)结束
主线程(MainThread)结束
1.线程与进程的区别
多进程中,同一个变量,各自有一份拷贝存在每一个进程中,互不影响。而多线程中,所有变量都由所有线程共享。所以,任何一个变量都可以被任意一个线程修改。因此,线程之间共享数据最大的危险在于多个线程同时修改一个变量容易把该变量该乱了。
import threading
num = 0
def run(n):
global num
for i in range(1000000):
num += n
num -= n
if __name__ == '__main__':
t1 = threading.Thread(target = run, args = (6,))
t2 = threading.Thread(target = run, args = (9,))
t1.start()
t2.start()
t1.join()
t2.join()
print('num = %d' % num)
2.使用Lock与RLock实现线程间共享数据的同步
(一)使用Lock实现线程同步
当属于并发线程的两个或多个操作尝试访问共享内存,并且至少有一个操作能够修改数据的状态时,这时如果没有恰当的同步机制,就会导致竞态条件。解决竞态条件最简单的方式是使用锁。
锁的操作:当一个线程想要访问共享内存的某一部分区域时,它必须要在使用前获取到该部分的锁。此外,在完成操作后,线程必须要释放掉之前获取的锁,这样共享内存的那部分才可以被其他想要使用的线程所用。对于线程来说,它对锁的需求要求在某个给定的时刻,只有一个线程能够使用共享内存的这部分。
死锁:死锁的出现是因为不同的线程都持有锁。由于这些锁阻止了线程访问资源,因此无法继续执行操作。
例1:
import threading, time
shared_resource_with_lock = 0
shared_resource_with_no_lock = 0
COUNT = 100000
lock = threading.Lock()
#锁管理:
def increment_with_lock():
global shared_resource_with_lock
for i in range(COUNT):
lock.acquire()
shared_resource_with_lock += 1
lock.release()
def decrement_with_lock():
global shared_resource_with_lock
for i in range(COUNT):
lock.acquire()
shared_resource_with_lock -= 1
lock.release()
#没有锁管理:
def increment_without_lock():
global shared_resource_with_no_lock
for i in range(COUNT):
shared_resource_with_no_lock += 1
def decrement_without_lock():
global shared_resource_with_no_lock
for i in range(COUNT):
shared_resource_with_no_lock -= 1
if __name__ == '__main__':
t1 = threading.Thread(target = increment_with_lock)
t2 = threading.Thread(target = decrement_with_lock)
t3 = threading.Thread(target = increment_without_lock)
t4 = threading.Thread(target = decrement_without_lock)
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()
print('锁管理:%d' % shared_resource_with_lock)
print('没有锁管理:%d' % shared_resource_with_no_lock)
例2:
import threading
num = 0
lock = threading.Lock()
def run(n):
global num
for i in range(100000000):
lock.acquire() #上锁,阻止了多线程的并发执行,效率降低
try:
num += n
num -= n
finally:
lock.release() #修改完一定要释放锁
if __name__ == '__main__':
t1 = threading.Thread(target = run, args = (6,))
t2 = threading.Thread(target = run, args = (9,))
t1.start()
t2.start()
t1.join()
t2.join()
print('num = %d' % num)
注意:由于可以存在多个锁,不同线程持有不同的锁,并试图获取其他的锁,可能造成死锁,导致多个线程挂起。
总结:
- 锁有两种状态:上锁与未上锁;
- 有两种方法可用于操纵锁:acquire()和release();
规则如下:
- 如果状态为未上锁,调用acquire()会将状态改为上锁;
- 如果状态为上锁,调用acquire()会阻塞,直到其他线程调用release()为止;
- 如果状态为未上锁,调用release()会导致RuntimeError异常;
- 如果状态为上锁,调用release()会将状态改为未上锁。
with lock: #自动上锁与释放锁
num += n
num -= n
(二)使用RLock实现行程同步
如果只想让获取锁的线程来释放锁,那就需要使用一个RLock对象。类似于Lock对象,RLock对象也有两个方法:acquire()与release()。如果希望在类外面能实现线程安全的访问,同时又使用类里面相同的方法,这时RLock就非常有用。
例:
import threading, time
class Box:
lock = threading.RLock()
def __init__(self):
self.total_items = 0
def excute(self, n):
Box.lock.acquire()
self.total_items += n
Box.lock.release()
def add(self):
Box.lock.acquire()
self.excute(1)
Box.lock.release()
def remove(self):
Box.lock.acquire()
self.excute(-1)
Box.lock.release()
def adder(box, items):
while items > 0:
print('adding 1 item in the box')
box.add()
time.sleep(2)
items -= 1
def remover(box, items):
while items > 0:
print('removing 1 item in the box')
box.remove()
time.sleep(3)
items -= 1
if __name__ == '__main__':
items = 5
box = Box()
t1 = threading.Thread(target = adder, args = (box, items))
t2 = threading.Thread(target = remover, args = (box, items))
t1.start()
t2.start()
t1.join()
t2.join()
print('total_items:%d' % box.total_items)
(三)使用信号量实现行程同步
信号量是一个由操作系统管理的抽象数据类型,用于同步多个线程对共享资源与数据的访问。本质上,信号量是由一个内部变量构成的,它标识出了对其所关联的资源的并发访问量。
在线程模块中,信号量的操作基于acquire()与release()这两个函数:
- 当一个线程想要访问与一个信号所关联的资源时,它必须调用acquire()操作,该操作会减少信号量的内部变量值,如果值为非负数,那么它就允许访问资源。如果值为负数,那么线程就会挂起,同时等待另一个线程释放该资源。
- 当一个线程使用完数据或是共享资源时,它必须通过release()操作释放资源。通过这种方式,信号量的内部变量就会增加,信号量队列中的第一个等待线程就可以访问共享资源了。
例:
import threading, time, random
#可选参数为内部变量counter赋予初始值,其默认值为1.
#如果赋的值小于0,那就会导致ValueError异常:
semaphore = threading.Semaphore(0)
def consumer():
print('consumer is waiting...')
semaphore.acquire() #获取到信号量
item -= 1
#消费者访问共享资源:
print('Consumer notify: consumed item number %s' % item)
def producer():
global item
time.sleep(3)
item = random.randint(0, 1000)
print('produced item: %s' % item)
#释放信号量,将内部的counter值加1,当其值等于0时,
#另一个线程就会再次等待它的值变为大于0,并唤醒该线程:
semaphore.release()
if __name__ == '__main__':
for i in range(0, 5):
t1 = threading.Thread(target = producer)
t2 = threading.Thread(target = consumer)
t1.start()
t2.start()
t1.join()
t2.join()
(四)使用条件实现行程同步
条件标识了应用了状态的改变。这是一种同步机制,即一个线程等待特定的条件,另一个线程通知它条件已经发生。一旦条件改变,线程就会获取到锁,从而排他性地访问共享资源。
例:
from threading import Thread, Condition
import time, random
items = []
condition = Condition()
class Consumer(Thread):
def __init__(self):
Thread.__init__(self)
def consume(self):
global condition
global items
condition.acquire()
if len(items) == 0:
condition.wait()
print('Consumer notify: no item to consume')
items.pop()
print('Consumer notify: delete 1 item...')
print('items: ' + str(len(items)))
condition.notify()
condition.release()
def run(self):
for i in range(0, 20):
time.sleep(3)
self.consume()
class Producer(Thread):
def __init__(self):
Thread.__init__(self)
def produce(self):
global condition
global items
condition.acquire()
if len(items) == 10:
condition.wait()
print('Producer notify - len(items): ' + str(len(items)))
print('Producer notify: stop item produce...')
items.append(1)
print('len(items): '+ str(len(items)))
condition.notify()
condition.release()
def run(self):
for i in range(0, 20):
time.sleep(3)
self.produce()
if __name__ == '__main__':
for i in range(0, 5):
producer = Producer()
consumer = Consumer()
producer.start()
consumer.start()
producer.join()
consumer.join()
(五)使用事件实现行程同步
事件是用于线程间通信的对象。一个线程会等待信号,同时另一个线程会发出信号。基本上,事件对象会管理一个内部的标志,可以通过set()方法将其设为true,也可以通过clear()方法将其重置为false。wait()方法会一直阻塞,直到标志变为true为止。
例:
from threading import Thread, Event
import time, random
items = []
event = Event()
class Consumer(Thread):
def __init__(self, items, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
while True:
time.sleep(2)
self.event.wait()
item = self.items.pop()
print('Consumer notify: %d popped from list by %s' % (item, self.name))
class Producer(Thread):
def __init__(self, integers, event):
Thread.__init__(self)
self.items = items
self.event = event
def run(self):
global items
for i in range(20):
time.sleep(2)
item = random.randint(0, 256)
self.items.append(item)
print('Producer notify: item %d is appended to list by %s' % (item, self.name))
print('Producer notify: event set by %s' % self.name)
self.event.set()
print('Producer notify: event cleared by %s' % self.name)
self.event.clear()
if __name__ == '__main__':
t1 = Producer(items, event)
t2 = Consumer(items, event)
t1.start()
t2.start()
t1.join()
t2.join()
(六)使用with语句
当有两个相关的操作需要对一个代码块成对执行时,with语句的作用就彰显出来了。此外,借助with语句,还可以在需要时精确地分配与释放资源。出于这个原因,with语句右叫作上下文管理器。在线程模块中,acquire()与release()方法所提供的全部对象可以用在with语句块中。
因此,如下对象可用作with语句的上下文管理器:
- Lock;
- RLock;
- 条件;
- 信号量。
例:
import threading
import logging
logging.basicConfig(level = logging.DEBUG)
def threading_with(statement):
with statement:
logging.debug('%s acquired via with' % statement)
def threading_not_with(statement):
statement.acquire()
try:
logging.debug('%s acquired directly' % statement)
finally:
statement.release()
if __name__ == '__main__':
lock = threading.Lock()
rlock = threading.RLock()
condition = threading.Condition()
mutex = threading.Semaphore(1)
threading_sync_list = [lock, rlock, condition, mutex]
for statement in threading_sync_list:
t1 = threading.Thread(target = threading_with, args = (statement, ))
t2 = threading.Thread(target = threading_not_with, args = (statement, ))
t1.start()
t2.start()
t1.join()
t2.join()
3.ThreadLocal对象
每个线程都有独立的存储空间。每个线程对ThreadLocal对象都可以读写,但是互不影响。
import threading
num = 0
local = threading.local()
def run(x, n):
x += n
x -= n
def func(n):
local.x = num #每个线程都有local.x,就是线程的局部变量
for i in range(1000000):
run(local.x, n)
print('%s - %d' % (threading.current_thread().name, local.x))
if __name__ == '__main__':
t1 = threading.Thread(target = func, args = (6,))
t2 = threading.Thread(target = func, args = (9,))
t1.start()
t2.start()
t1.join()
t2.join()
print('num = %d' % num)
作用:为每个线程绑定一个数据库、HTTP请求、用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
4.在子类中使用线程
要向通过线程模块实现新的线程,你需要这样做:
- 定义新的Thread类的子类;
- 重写__init__(self [,args])方法来添加额外的参数;
- 重写run(self, [,args])方法来实现线程启动后需要做的事。
一旦创建好新的Thread子类后,你就可以创建它的实例并通过调用start()方法来开启新的线程了,start()方法又会调用run()方法。
例:
import threading, 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:
thread.exit()
time.sleep(delay)
print('%s : %s' % (threadName, time.ctime(time.time())))
counter -= 1
if __name__ == '__main__':
t1 = myThread(1, 'No-1', 1)
t2 = myThread(2, 'No-2', 2)
t1.start()
t2.start()
print('Exiting Main Thread.')
(二)使用队列实现线程通信
队列会对单个线程对资源的所有访问进行过滤,并且支持更加整洁且可读性更棒的设计模式。
队列的方法:
- put():将一个条目添加到队列中;
- get():从队列中删除并返回一个条目;
- task_done():每次处理一个条目时都会调用该方法;
- join():这会导致阻塞,直到所有条目都被处理完为止。
例:
from threading import Thread, Event
from queue import Queue
import time
import random
class Producer(Thread):
def __init__(self, queue):
Thread.__init__(self)
self.queue = queue
def run(self):
for i in range(10):
item = random.randint(0, 256)
self.queue.put(item)
print('Producer notify: item No%d appended to queue by %s' % (item, self.name))
time.sleep(1)
class Consumer(Thread):
def __init__(self, queue):
Thread.__init__(self)
self.queue = queue
def run(self):
while True:
item = self.queue.get()
print('Consumer notify: %d popped from queue by %s' % (item, self.name))
self.queue.task_done()
if __name__ == '__main__':
queue = Queue()
t1 = Producer(queue)
t2 = Consumer(queue)
t3 = Consumer(queue)
t4 = Consumer(queue)
t1.start()
t2.start()
t3.start()
t4.start()
t1.join()
t2.join()
t3.join()
t4.join()
Producer类使用Queue.put(item [, block[, timeout)向队列中插入数据。在将数据插入到队列前它会获取到锁。
存在两种可能:
- 如果可选参数block为true,并且timeout为None,那么就需要阻塞,直到有可用的位置为止。如果timeout是一个正数,那么就会阻塞至多timeout秒,如果在这期间没有可用的位置,那就会抛出异常。
- 如果block为false,那么在有可用的位置时就会将一个条目放到队列中;否则就会抛出异常(在这种情况下会忽略掉timeout)。这里的put()会检查队列是否已满,接下来在内部调用wait(),这样生产者就会开始等待。
Consumer类使用Queue.get([block [, timeout]]),并在从队列中移除数据前获取到锁。如果队列为空,那么消费者就会进入等待状态。