Python-多线程threading

Python多线程

  线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

进程是资源分配的最小单位,线程是CPU调度的最小单位,每一个进程中至少有一个线程。

线程的特点

  在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。

  • 轻型实体;
      线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
      线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。
  • 独立调度和分派的基本单位;
      在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
  • 可并发执行;
      在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
  • 共享进程资源;
      在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。

创建线程

python的thread模块是比较底层的模块,python的threading模块是对thread做了一些包装的,可以更加方便的被使用

1
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

参数:

  • group:目前此参数为None,在实现ThreadGroup类时为将来的扩展保留。
  • target:target接收的是一个函数的地址,由run()方法调用执行函数中的内容。默认为无,表示未调用任何内容。
  • name :线程名,可自行定义。
  • args:target接收的是函数名,此函数的位置参数以元组的形式存放在args中,用于执行函数时调用。
  • kwargs :target接收的是函数名,此函数的关键字参数以字典的形式存放在kwargs中,用于执行函数时调用。
  • daemon:如果为True表示该线程为守护线程。
    方法:
  • threading模块的方法
    • threading.currentThread(): 返回当前的线程变量。
    • threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
    • threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
  • Thread类方法
    • start():开启线程,一个Thread对象只能调用一次start()方法,如果在同一线程对象上多次调用此方法,则会引发RuntimeError。
    • run():执行start()方法会调用run(),该方将创建Thread对象时传递给target的函数名,和传递给args、kwargs的参数组合成一个完整的函数,并执行该函数。run()方法一般在自定义Thead类时会用到。
    • join(timeout=None):join会阻塞、等待线程,timeout单位为秒,因为join()总是返回none,所以在设置timeout调用join(timeout)之后,需要使用isalive()判断线程是否执行完成,如果isalive为True表示线程在规定时间内没有执行完,线程超时。如果join(timeout=None)则会等待线程执行完毕后才会执行join()后面的代码,一般用于等待线程结束。
    • name:获取线程名。
    • getName():获取线程名。
    • setName(name):设置线程名。
    • ident:“线程标识符”,如果线程尚未启动,则为None。如果线程启动是一个非零整数。
    • is_alive():判断线程的存活状态,在run()方法开始之前,直到run()方法终止之后。如果线程存活返回True,否则返回False。
    • daemon:如果thread.daemon=True表示该线程为守护线程,必须在调用Start()之前设置此项,否则将引发RuntimeError。默认为False
    • isDaemon():判断一个线程是否是守护线程。
    • setDaemon(daemonic):设置线程为守护线程。

thread创建线程

1
2
3
4
5
6
7
8
9
10
11
12
import threading
import time
import os

def test(num):
print("线程--- %d" % num)
time.sleep(2)

if __name__ == '__main__':
for i in range(5):
t = threading.Thread(target=test,args=(i,))
t.start()

执行结果:

1
2
3
4
5
线程--- 0
线程--- 1
线程--- 2
线程--- 3
线程--- 4

thread子类创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import threading
import time

class MyThread(threading.Thread):
# 重写run方法
def run(self):
for i in range(3):
time.sleep(1)
print(self.name + "\t" + str(i))

if __name__ == '__main__':
for i in range(5): # 创建5个线程
t = MyThread()
t.start()

执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Thread-1	0
Thread-2 0
Thread-3 0
Thread-5 0
Thread-4 0
Thread-2 1
Thread-1 1
Thread-3 1
Thread-5 1
Thread-4 1
Thread-2 2
Thread-1 2
Thread-4 2
Thread-3 2
Thread-5 2

共享全局变量

利用线程对变量num分别加1000000次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from threading import Thread
import time
# 定义一个全局变量
num = 0

def test1():
global num
for i in range(1000000):
num += 1
print("当前值--num:%d"%num)

def test2():
global num
for i in range(1000000):
num += 1
print("当前值--num:%d"%num)

if __name__ == '__main__':
t1 = Thread(target=test1) # 线程1
t2 = Thread(target=test2) # 线程2
t1.start()
# time.sleep(2) # 可通过延时保证test1执行完毕
t2.start()

执行结果:

1
2
当前值--num:1196015
当前值--num:1458177 # 执行结果不为2000000,且每次结果不一样

python之间线程是可以共享同一进程中的所有资源,包括变量。但为什么上述代码执行结果不是2000000,这和操作系统的cpu处理速度和调度算法有关,“ num += 1 ” 可以看成 “ num = num + 1 ”,分为两部,先加法,再赋值;例如当num=1000,线程1的num加法执行完,要进行赋值“num = 1001”时,cpu切换了线程,执行线程2,执行了一个时间片后,再回到线程1,尽管这时num的值叠加了好几轮,执行了“num = 1001”,num的值就又回到原点了;同时执行两个线程没有达到预期效果。
总结:

  • 在一个进程内的所有线程共享全局变量,能够在不适用其他方式的前提下完成多线程之间的数据共享
  • 缺点就是,线程是对全局变量随意遂改可能造成多线程之间对全局变量的混乱(即线程非安全)

互斥锁

当多个线程几乎同时修改某一个共享数据的时候,需要进行同步控制
线程同步能够保证多个线程安全访问竞争资源,最简单的同步机制是引入互斥锁。

  • 线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态,实现线程同步的方法有很多,临界区对象就是其中一种。

互斥锁为资源引入一个状态:锁定/非锁定。

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

threading模块中定义了Lock类,可以方便的处理锁定:

1
2
3
4
5
6
#创建锁
mutex = threading.Lock()
#锁定
mutex.acquire([blocking])
#释放
mutex.release()

其中,锁定方法acquire可以有一个blocking参数。

  • 如果设定blocking为True,则当前线程会堵塞,直到获取到这个锁为止(如果没有指定,那么默认为True)
  • 如果设定blocking为False,则当前线程不会堵塞

使用互斥锁实现上面的例子的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from threading import Thread, Lock

# 定义一个全局变量
num = 0
# 创建锁,默认没有上锁
mutex = Lock()

def test1():
global num
# 上锁
mutex.acquire() # 如果有一方上锁,则另一方则会进入堵塞状态
for i in range(1000000):
num += 1
# 解锁
mutex.release()
print("当前值--num:%d"%num)

def test2():
global num
mutex.acquire()
for i in range(1000000):
num += 1
mutex.release()
print("当前值--num:%d"%num)

if __name__ == '__main__':
t1 = Thread(target=test1) # 线程1
t2 = Thread(target=test2) # 线程2
t1.start()
t2.start()

执行结果:

1
2
当前值--num:1000000
当前值--num:2000000

死锁

在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁。

尽管死锁很少发生,但一旦发生就会造成应用的停止响应。下面看一个死锁的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import threading
import time

mutexA = threading.Lock()
mutexB = threading.Lock()

class Thread1(threading.Thread):
# 重写run方法
def run(self):
if mutexA.acquire(): # mutexA上锁
print(self.name + "-------acquire mutexA")
time.sleep(1) # 保证mutexA、mutexB都堵塞

if mutexB.acquire(): # 等待Thread2释放mutexB
print(self.name + "-------acquire mutexB")
mutexB.release()
print(self.name + "-------release mutexB")

mutexA.release()
print(self.name + "-------release mutexA")

class Thread2(threading.Thread):
# 重写run方法
def run(self):
if mutexB.acquire(): # mutexB上锁
print(self.name + "-------acquire mutexB")
time.sleep(1)

if mutexA.acquire(): # 等待Thread1释放mutexA
print(self.name + "-------acquire mutexA")
mutexA.release()
print(self.name + "-------release mutexA")

mutexB.release()
print(self.name + "-------release mutexB")

if __name__ == '__main__':
t1 = Thread1()
t2 = Thread2()
t1.start()
t2.start()

注:time.sleep(1)的作用是保证两个线程都抢占到资源;由于代码比较少,cpu运行速度快,延时的作用防止线程1直接执行完,而看不到结果
线程1和线程2各自等待对方资源释放,这样就陷入死锁了。
执行结果:

1
2
3
Thread-1-------acquire mutexA
Thread-2-------acquire mutexB
(...死锁)

避免死锁的方法

  • 增加超时时间
  • 银行家算法

百度百科
参考博客