什么是线程呢?
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。如下图所示:
每个进程至少有一个线程,即进程本身。进程可以启动多个线程。操作系统像并行“进程”一样执行这些线程。
线程和进程各自有的区别和优劣:
线程分类:
内核线程是操作系统的一部分,而内核中没有实现用户空间线程。
python的thread模块是⽐较底层的模块,python的threading 模块是对thread做了⼀些包装的,可以更加⽅便的被使⽤。下面通过即行代码加深一下对线程的理解:
import threading
if __name__ == '__main__':
# 一个进程里面一定有一个线程, 叫主线程.
print("当前线程个数:", threading.active_count())
print("当前线程信息:", threading.current_thread())
输出结果为:
多线程编程的实现也是有两个方法,实例化对象的方法和创建子类的方法。
通过实例化对象的方法创建线程,具体代码如下:
"""
通过实例化对象的方式实现多线程
"""
import time
import threading
def task():
"""当前要执行的任务"""
print("听音乐........")
time.sleep(1)
if __name__ == '__main__':
start_time = time.time()
threads = []
for count in range(5):
t = threading.Thread(target=task)
# 让线程开始执行任务
t.start()
threads.append(t)
# 等待所有的子线程执行结束, 再执行主线程;
[thread.join() for thread in threads]
end_time = time.time()
print(end_time-start_time)
输出结果为:
方法一分析:
可以看到线程有以下⼏种状态:
下面我们将通过一个实际案例来看一下多线程编程的第二个方法:通过创建子类,重写run方法,实现多线程编程。
首先看一下实际项目案例介绍:基于多线程的批量主机存活探测
项目描述: 如果要在本地网络中确定哪些地址处于活动状态或哪些计算机处于活动状态,则可以使用此脚本。我们将依次ping地址, 每次都要等几秒钟才能返回值。这可以在Python中编程,在IP地址的地址范围内有一个for循环和一个os.popen(“ping -q -c2”+ ip)。
Python中os模块中有一个system,它的作用就是可以让linux中的命令在Python中执行。具体代码如下:
from threading import Thread
class GetHostAliveThread(Thread):
"""
创建子线程, 执行的任务:判断指定的IP是否存活
"""
def __init__(self, ip):
super(GetHostAliveThread, self).__init__()
self.ip = ip
def run(self):
# 重写run方法: 判断指定的IP是否存活
import os
# 需要执行的shell命令
cmd = 'ping -c1 -w1 %s &> /dev/null' %(self.ip)
result = os.system(cmd)
# 返回值如果为0, 代表命令正确执行,没有报错; 如果不为0, 执行报错;
if result != 0:
print("%s主机没有ping通" %(self.ip))
if __name__ == '__main__':
print("打印172.25.254.0网段没有使用的IP地址".center(50, '*'))
for i in range(1, 255):
ip = '172.25.254.' + str(i)
thread = GetHostAliveThread(ip)
thread.start()
部分输出结果如下:
优点:在⼀个进程内的所有线程共享全局变量,能够在不使⽤其他⽅式的前提下完成多线程之间的数据共享(这点要⽐多进程要好)
缺点:线程对全局变量随意修改可能造成多线程之间对全局变量的混乱(即线程⾮安全)
那么如何解决线程不安全问题呢?
GIL(global interpreter lock): python解释器中任意时刻都只有一个线程在执行。
Python代码的执行由Python 虚拟机(也叫解释器主循环,CPython版本)来控制,Python 在设计之初就考虑到要在解释器的主循环中,同时只有一个线程在执行,即在任意时刻,只有一个线程在解释器中运行。对Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。
原理如下图:
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作。
同步就是协同步调,按预定的先后次序进⾏运⾏。如:你说完,我再说。 "同"字从字⾯上容易理解为⼀起动作,其实不是, "同"字应是指协同、协助、互相配合。
threading模块里面有一个Lock类,即线程锁,它就很好的解决了这个问题。
下面看两个问题:
1.为什么需要线程锁?
多个线程对同一个数据进行修改时,可能会出现不可预料的情况。
2.如何实现线程锁?
(1) 实例化一个锁对象 lock = threading.Lock( )
(2) 操作变量之前进行加锁 lock.acquire( )
(3) 操作变量之后进行解锁 lock.release( )
下面通过代码实现多线程对全局变量进行修改,共享数据:
money = 0
def add():
for i in range(1000000):
global money
lock.acquire()
money += 1
lock.release()
def reduce():
for i in range(1000000):
global money
lock.acquire()
money -= 1
lock.release()
if __name__ == '__main__':
from threading import Thread, Lock
# 创建线程锁
lock = Lock()
t1 = Thread(target=add)
t2 = Thread(target=reduce)
t1.start()
t2.start()
t1.join()
t2.join()
print(money)
输出结果如下:
死锁问题
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时等待对⽅的资源,就会造成死锁。如下图:
下面看一个造成死锁的例子代码:
"""
在线程间共享多个资源的时候,如果两个线程分别占有⼀部分资源并且同时 等待对⽅的资源,就会造成死锁。
"""
import time
import threading
class Account(object):
def __init__(self, id, money, lock):
self.id = id
self.money = money
self.lock = lock
def reduce(self, money):
self.money -= money
def add(self, money):
self.money += money
def transfer(_from, to, money):
if _from.lock.acquire():
_from.reduce(money)
time.sleep(1)
if to.lock.acquire():
to.add(money)
to.lock.release()
_from.lock.release()
if __name__ == '__main__':
a = Account('a', 1000, threading.Lock()) # 900
b = Account('b', 1000, threading.Lock()) # 1100
t1 = threading.Thread(target=transfer, args=(a, b, 200))
t2 = threading.Thread(target=transfer, args=(b, a, 100))
t1.start()
t2.start()
print(a.money)
print(b.money)
运行结果之后会发现和我们预想的结果不太一样,因为造成了死锁:
多任务分类分为IO密集型的任务和计算密集型的任务,且对于处理这两个不同的任务时所使用的方法也不同,可以总结如下: