线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其他线程共享进程所拥有的全部资源。

多任务可以由多进程完成,也可以由一个进程内的多线程完成。由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix(移植性操作系统接口) Thread,而不是模拟出来的线程。

python的标准库提供了两个模块:thread 和 threading

  • thread:是低级模块
  • threading:是高级模块,对thread进行了封装。绝大情况,我们只需使用threading模块。

一、创建线程:使用threading模块

单线程执行

import time

def say():
	print('hello man,')
	time.sleep(1)
if __name__ == '__main__':
	for i in range(5):
		say()

运行结果:

python线程 printf输出 python的线程_pyhton


多线程执行(函数)

import threading 
import time
def mul_thread():
	print('实现多线程')
	time.sleep(1)
if __name__=='__main__':
	for i in range(5):
		t = threading.Thread(target=mul_thread)
		t.start() #启动线程,即让线程开始执行

运行结果:

python线程 printf输出 python的线程_共享资源_02

说明:

  • 可以明显看出使用了多线程并发的操作,花费时间要短很多
  • 当调用start()时,才会真正的创建线程,并且开始执行

继承实现多线程(threading.Thread):继承Thread类,重写run()方法,使用start()启动线程

import threading
import time
class MyThread(threading.Thread):
	def run(self):
		for i in range(3):
	            time.sleep(1)
	            msg = "I'm "+self.name+' @ '+str(i) #name属性中保存的是当前线程的名字
	            print(msg)

if __name__ == '__main__':
	t = MyThread()
	t.start() #启动线程

注意:

用继承实现多线程要重写run()方法。启动线程是使用对象调用start() 方法;不能使用对象调用run()方法,对象调用run实例方法还是单线程。

小结 :
线程程序的执行顺序是不确定的。当执行到sleep语句时,线程将被阻塞(Blocked),到sleep结束后,线程进入就绪(Runnable)状态,等待调度。而线程调度将自行选择一个线程执行,当线程的run()方法结束时该线程完成。

二、共享全局变量

  1. 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享(这点要比多进程要好)
from threading import Thread
import time
g_num = 100
def work1():
    global g_num
    for i in range(3):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)
def work2():
    global g_num
    print("----in work2, g_num is %d---"%g_num)

print("---线程创建之前g_num is %d---"%g_num)
t1 = Thread(target=work1)
t1.start()
#延时一会,保证t1线程中的事情做完
time.sleep(1)
t2 = Thread(target=work2)
t2.start()

运行结果:

—线程创建之前g_num is 100—
----in work1, g_num is 103—
----in work2, g_num is 103—

  1. 共享全局的问题

假设两个线程t1和t2都要对全局变量g_num(默认是0)进行加1运算,t1和t2都各对g_num加1000000次,g_num的最终的结果应该为2000000。

但是由于是多线程同时操作,有可能出现下面情况

  • 在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0
  • 然后t2对得到的值进行加1并赋给g_num,使得g_num=1
  • 然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。
  • 这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1
import threading
import time
g_num = 0
def work1(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work1, g_num is %d---"%g_num)
def work2(num):
    global g_num
    for i in range(num):
        g_num += 1
    print("----in work2, g_num is %d---"%g_num)
print("---线程创建之前g_num is %d---"%g_num)
t1 = threading.Thread(target=work1, args=(1000000,))
t2 = threading.Thread(target=work2, args=(1000000,))
t1.start()
t2.start()
# 等待子线程执行完成 threading.enumerate()返回的是列表:当前时刻存活的线程
while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

python线程 printf输出 python的线程_多线程_03

结论:

  • 如果多个线程同时对同一个全局变量操作,会出现资源竞争问题,从而数据结果会不正确

三、线程同步,互斥锁

1、解决线程同时修改全局变量的方式
对于上一小节提出的那个计算错误的问题,可以通过线程同步来进行解决,同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

  • 系统调用t1,然后获取到g_num的值为0,此时上一把锁,即不允许其他线程操作g_num
  • t1对g_num的值进行+1
  • t1解锁,此时g_num的值为1,其他的线程就可以使用g_num了,而且是g_num的值不是0而是1
  • 同理其他线程在对g_num进行修改时,都要先上锁,处理完后再解锁,在上锁的整个过程中不允许其他线程访问,就保证了数据的正确性

2、互斥锁
某个线程要更改共享数据时,先将其锁定,此时资源的状态为“锁定”,其他线程不能更改;直到该线程释放资源,将资源的状态变成“非锁定”,其他的线程才能再次锁定该资源。互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性。使用threading模块中的Lock类

# 创建锁
mutex = threading.Lock()

# 锁定
mutex.acquire()

# 释放
mutex.release()

注意:

  • 如果这个锁之前是没有上锁的,那么acquire不会堵塞
  • 如果在调用acquire对这个锁上锁之前 它已经被 其他线程上了锁,那么此时acquire会堵塞,直到这个锁被解锁为止

解决前面2个线程对同一个全局变量各加100万次的操作

import threading
import time
g_num = 0
def work1(num):
    global g_num
    for i in range(num):
        # 上锁
        mutex.acquire()
        g_num += 1
        # 释放锁
        mutex.release()
    print("----in work1, g_num is %d---" % g_num)
def work2(num):
    global g_num
    for i in range(num):
        # 上锁
        mutex.acquire()
        g_num += 1
        # 释放锁
        mutex.release()
    print("----in work2, g_num is %d---" % g_num)
print("---线程创建之前g_num is %d---" % g_num)
# 创建互斥锁对象
mutex = threading.Lock()
t1 = threading.Thread(target=work1, args=(1000000,))
t2 = threading.Thread(target=work2, args=(1000000,))
t1.start()
t2.start()
# 等待子线程执行完成 threading.enumerate()返回的是当前时刻线程的
# print(type(threading.enumerate()))
while len(threading.enumerate()) != 1:
    time.sleep(1)

print("2个线程对同一个全局变量操作之后的最终结果是:%s" % g_num)

运行结果:

python线程 printf输出 python的线程_多线程_04

上锁解锁过程:
当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“阻塞”,直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。
3、线程死锁
在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

import threading
import time
class MyThread1(threading.Thread):
    def run(self):
        # 对mutexA上锁
        mutexA.acquire()
        # mutexA上锁后,延时1秒,等待另外那个线程 把mutexB上锁
        print(self.name+'----do1---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexB已经被另外的线程抢先上锁了
        mutexB.acquire()
        print(self.name+'----do1---down----')
        mutexB.release()
        # 对mutexA解锁
        mutexA.release()
class MyThread2(threading.Thread):
    def run(self):
        # 对mutexB上锁
        mutexB.acquire()
        # mutexB上锁后,延时1秒,等待另外那个线程 把mutexA上锁
        print(self.name+'----do2---up----')
        time.sleep(1)
        # 此时会堵塞,因为这个mutexA已经被另外的线程抢先上锁了
        mutexA.acquire()
        print(self.name+'----do2---down----')
        mutexA.release()
        # 对mutexB解锁
        mutexB.release()
mutexA = threading.Lock()
mutexB = threading.Lock()
if __name__ == '__main__':
    t1 = MyThread1()
    t2 = MyThread2()
    t1.start()
    t2.start()

python线程 printf输出 python的线程_共享资源_05


此时已经进入到了死锁状态,可以使用ctrl+c退出

避免死锁:

  • 程序设计时要尽量避免(银行家算法)
  • 添加超时时间等

总结:

锁的好处:

  • 确保了某段关键代码只能由一个线程从头到尾完整地执行

锁的坏处:

  • 阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了
  • 由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁