python多线程之threading

多线程编程之threading

1
2
3
4
5
6
7
8
1。单进程单线程:一个人在一个桌子上吃菜。
2。单进程多线程:多个人在同一个桌子上一起吃菜。
3。多进程单线程:多个人每个人在自己的桌子上吃菜。

多线程的问题是多个人同时吃一道菜的时候容易发生争抢,例如两个人同时夹一个菜,一个人刚伸出筷
子,结果伸到的时候已经被夹走菜了。。。此时就必须等一个人夹一口之后,在还给另外一个人夹菜,
也就是说资源共享就会发生冲突争抢。
(ps:知乎上看到的感觉非常形象!)

线程启动的两种方式

  • 将函数传入Thread对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import threading
def thread_fun(num):
for n in range(0, int(num)):
print(" I come from %s, num: %s" %( threading.currentThread().getName(), n))

def main(thread_num):
thread_list = list();
# 先创建线程对象
for i in range(0, thread_num):
thread_name = "thread_%s" %i
# 线程势实例
t = threading.Thread(target = thread_fun, name = thread_name, args = (20,))
thread_list.append(t)

# 启动所有线程
for thread in thread_list:
thread.start()

# 主线程中等待所有子线程退出
for thread in thread_list:
thread.join()

if __name__ == "__main__":
main(3)
  • 继承自 threading.Thread 类
1
2
3
4
5
6
7
8
9
10
11
12
13
import threading

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self);

def run(self):
print("I am " + self.name )

if __name__ == "__main__":
for thread in range(0, 5):
t = MyThread()
t.start()

守护线程

如果在程序中将子线程设置为守护线程,则该子线程会在主线程结束时自动退出,设置方式为thread.setDaemon(True),要在thread.start()之前设置,默认是false的,也就是主线程结束时,子线程依然在执行。

  • 未设置守护线程,主线程结束,子线程依然在运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time
import threading


def fun():
print("start fun")
time.sleep(2)
print("end fun")


print("main thread")
t1 = threading.Thread(target=fun,args=())
#t1.setDaemon(True)
t1.start()
time.sleep(1)
print("main thread end")
  • 设置守护线程,主线程退出时,程序结束,子线程同时停止运行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time
import threading


def fun():
print("start fun")
time.sleep(2)
print("end fun")


print("main thread")
t1 = threading.Thread(target=fun,args=())
t1.setDaemon(True)
t1.start()
time.sleep(1)
print("main thread end")

Thread.join()

阻塞进程直到线程执行完毕。通用的做法是我们启动一批线程,最后join这些线程结束。

1
2
3
4
5
6
7
for i in range(10):
t = ThreadTest(i) # 线程实例
thread_arr.append(t)
t.start()

for thread in thread_arr:
thread.join()

死锁

所谓死锁: 是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 由于资源占用是互斥的,当某个进程提出申请资源后,使得有关进程在无外力协助下,永远分配不到必需的资源而无法继续运行,这就产生了一种特殊现象死锁。

  • 产生死锁的主要原因

    • 系统资源不足
    • 进程运行推进顺序不当
    • 资源分配不当
  • 四个必要条件

    • 互斥:一个资源一次只能被一个进程使用。
    • 请求与保持:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
    • 不可剥夺:进程已获得的资源,在末使用完之前,不能强行剥夺。
    • 循环等待:若干进程之间形成一种头尾相接的循环等待资源关系。

互斥锁与可重入锁

互斥锁

Python编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为” 互斥锁” 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。在Python中我们使用threading模块提供的Lock类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading
import time

counter = 0
mutex = threading.Lock()

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global counter, mutex
time.sleep(1);
if mutex.acquire():
counter += 1
print("I am %s, set counter:%s" % (self.name, counter))
mutex.release()

if __name__ == "__main__":
for i in range(0, 100):
my_thread = MyThread()
my_thread.start()

当一个线程调用Lock对象的acquire()方法获得锁时,这把锁就进入“locked”状态。因为每次只有一个线程1可以获得锁,所以如果此时另一个线程2试图获得这个锁,该线程2就会变为“block同步阻塞状态。直到拥有锁的线程1调用锁的release()方法释放锁之后,该锁进入“unlocked”状态。线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

可重入锁

考虑这种情况:如果一个线程遇到锁嵌套的情况该怎么办,这个嵌套是指当我一个线程在获取临界资源时,又需要再次获取。例如

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
import threading
import time

counter = 0
mutex = threading.Lock()

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)

def run(self):
global counter, mutex
time.sleep(1);
if mutex.acquire():
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
if mutex.acquire():
counter += 1
print "I am %s, set counter:%s" % (self.name, counter)
mutex.release()
mutex.release()

if __name__ == "__main__":
for i in range(0, 200):
my_thread = MyThread()
my_thread.start()

在第一个线程运行时,整个程序将直接挂起。所以引入可重入锁的概念,python提供了“可重入锁”:threading.RLock。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁。

将上述代码:

1
mutex = threading.Lock()

改成:

1
mutex = threading.RLock()

即可破坏死锁。

守护线程

守护线程,顾名思义,就是守护的意思,等待主线程结束而直接退出,一般起辅助作用。

  • 对主进程来说,运行完毕指的是主进程代码运行完毕
  • 对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import threading
import time
def foo():
print('start_1')
time.sleep(4)
print('end_1')

def bar():
print('start_2')
time.sleep(1)
print('end_2')

if __name__ == '__main__':
t1 = threading.Thread(target=foo)
t2 = threading.Thread(target=bar)

t1.setDaemon(True)
t1.start()
t2.start()

print('----------')

运行结果:

1
2
3
4
start_1
start_2
----------
end_2

如果将foo()改为time.sleep(1),bar()改为time.sleep(4), 运行结果:

1
2
3
4
5
start_1
start_2
----------
end_1
end_2

在第一次运行中,主线程等待t2结束,此时t1还在阻塞中,t2结束以后,即主线程结束,此时t1还处于阻塞中;第二次运行中,由于t2阻塞的时间更长,所以导致t1先于主线程结束。


注意

需要注意的是,Python(CPython)有一个GIL(Global Interpreter Lock)机制,任何线程在运行之前必须获取这个全局锁才能执行,每当执行完100条字节码,全局锁才会释放,切换到其他线程执行。

所以Python中的多线程不能利用多核计算机的优势,无论有多少个核,同一时间只有一个线程能得到全局锁,只有一个线程能够运行。

那么Python中的多线程有什么作用呢?为什么不直接使用Python中的多进程标准库?这里要根据程序执行的是IO密集型任务和计算密集型任务来选择。当执行IO密集型任务时,比如Python爬虫,大部分时间都用在了等待返回的socket数据上,CPU此时是完全闲置的,这种情况下采用多线程较好。当执行计算密集型任务时,比如图像处理,大部分时间CPU都在计算,这种情况下使用多进程才能真正的加速,使用多线程不仅得不到并行加速的效果,反而因为频繁切换上下文拖慢了速度。