抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

线程,是操作系统调度的单位

一个程序写完了,放在硬盘里不运行,叫做程序
一个程序写完了,放到内存里跑起来,叫做进程
一个进程中至少有一个线程,称为主线程
一个线程要干很多事情,有些事情很耗时,就创建个子线程,让子线程去做,主线程继续下一步

当有多个线程的时候,CPU会轮流切换执行,每个线程执行多长时间叫做时间片
比如 t1 分到了0.001秒的时间片,t2 分到了0.002秒的时间片
显然这么短时间是不够执行的,所以CPU会快速的切来切去,这样就看起来好像能执行多任务
多任务就是你可以同时听歌、写文档、挂QQ等等,但其实同一时刻CPU只执行一个线程
只不过切换的快,时间片也很小,所以人感觉不出来。

线程的执行顺序是由操作系统决定的,开发者左右不了。

子线程由主线程创建,子线程关闭后留下的辣鸡由主线程清理
一旦主线程挂了,子线程全部挂掉。
主线程会等待所有子线程结束后才结束

单线程操作

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


def say_hi():
for i in range(5):
print('Hi!')
time.sleep(1) # 耗时操作


def say_yes():
for i in range(5):
print('Yes!')
time.sleep(1) # 耗时操作


def main():
say_hi()
say_yes()


if __name__ == '__main__':
main()

--------------------------------------------------

# Output:
Hi!
Hi!
Hi!
Hi!
Hi!
Yes!
Yes!
Yes!
Yes!
Yes!

多线程操作

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
import time
import threading # 导入 threading 模块


def say_hi():
for i in range(5):
print('Hi!')
time.sleep(1) # 耗时操作


def say_yes():
for i in range(5):
print('Yes!')
time.sleep(1) # 耗时操作


def main():
t1 = threading.Thread(target=say_hi) # 创建一个线程对象
t2 = threading.Thread(target=say_yes) # 创建一个线程对象
t1.start() # 启动线程
t2.start() # 启动线程


if __name__ == '__main__':
main()

--------------------------------------------------

# Output:
Hi!
Yes!
Hi!
Yes!
Hi!
Yes!
Yes!
Hi!
Hi!
Yes!

单线程和多线程的区别就像这个栗子

你要来一段热舞rap,

  • 单线程的操作是:rap完再热舞,或者热舞完再rap
  • 多线程的操作是:边热舞边rap

创建子线程并启动

创建线程操作可以分为三步:

  1. 导入 threading 模块
  2. 创建一个Thread对象
  3. 通过Thread对象启动线程

创建一个Thread对象的时候可以传入参数
threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)

  • group 应该为 None;为了日后扩展 ThreadGroup 类实现而保留。
  • target:线程中需要执行的函数。默认是 None,表示不需要调用任何方法。
  • name:线程名称。默认情况下,由 “Thread-N” 格式构成一个唯一的名称,其中 N 是小的十进制数。
  • args:用于调用目标函数的参数元组。默认是 ()。(必须是可迭代对象)
  • kwargs:用于调用目标函数的关键字参数字典。默认是 {}。(必须是可迭代对象)
  • daemon不是None,线程将被显式的设置为守护模式,不管该线程是否是守护模式。
  • daemon=None,线程将继承当前线程的守护模式属性。
  • 如果子类重载了构造函数,必须先调用父类构造器Thread.__init__(),再做其他事情。

函数方式

函数方式适合于 —— 一个线程中要做的事情比较少,一个函数就能搞定。

  1. 导入 threading 模块
  2. 创建一个Thread对象并把函数传进去
  3. 通过Thread对象启动线程
1
2
3
4
5
6
7
8
9
import threading    # 1. 导入 thrading 模块


def funcName(): # 需要子线程执行的函数
pass


t1 = threading.Thread(target=funcName) # 2. 创建一个Thread对象并把函数传进去
t1.start() # 3. 启动线程

继承方式

继承方式适合于 —— 一个线程里面做的事情比较复杂,需要分成多个函数来做。

  1. 导入 threading 模块
  2. 创建一个类继承 Thread 类
  3. 重写 run 方法
  4. 创建一个继承了 Thread 类的对象
  5. 用start方法启动线程
1
2
3
4
5
6
7
8
9
10
import threading    # 1. 导入 threading 模块


class clsName(threading.Thread): # 2. 继承Thread类
def run(self): # 3. 重写run方法
print(self.name) # self.name 保存的是当前线程的名称


t1 = clsName() # 4. 创建一个继承了 Thread 类的对象
t1.start() # 5. 启动线程

t1.start()只会去调用run方法。如果类中还有其他方法,只能在类中调用,不能用 t1去调用。
也就是说,继承了 Thread 的类的之类,除了run方法,其他方法都可以定义成私有的(公有的外面也不能调用啊)

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


class MyThread(threading.Thread):
def run(self):
print(self.name)
self.__say_hi()
self.__say_yes()

def __say_hi(self):
print('Hi!')

def __say_yes(self):
print('Yes!')


t1 = MyThread()
t1.start()

--------------------------------------------------

# Output:
Thread-1
Hi!
Yes!

插队 join()

join([timeout])
当某个线程对象成功调用了 join() 方法之后,会优先获得CPU的资源,让其他线程等待
知道这个线程对象执行结束后,才把资源让给别的线程

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
42
43
44
import threading


def f1(n):
for i in range(n):
print(threading.current_thread().getName())


def f2(n):
for i in range(n):
print(threading.current_thread().getName() + "------" + str(i))


def main():
t1 = threading.Thread(target=f1, args=(5,))
t2 = threading.Thread(target=f2, args=(5,))
t1.start()
t2.start()

for i in range(5):
print(threading.current_thread().getName())


if __name__ == '__main__':
main()

--------------------------------------------------

# Output:
Thread-1
Thread-1
MainThread
Thread-2------0
MainThread
Thread-2------1
MainThread
Thread-2------2
MainThread
MainThread
Thread-2------3
Thread-2------4
Thread-1
Thread-1
Thread-1

可以看到,没有加 join 方法,所以主线程 和 t1 和 t2 相互争抢资源。
如果现在需要 t1 这个线程执行完再给其他线程执行,也就是说要求 t1 独占一段时间
那就让 t1 调用 join() 方法

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
42
43
44
45
import threading


def f1(n):
for i in range(n):
print(threading.current_thread().getName())


def f2(n):
for i in range(n):
print(threading.current_thread().getName() + "------" + str(i))


def main():
t1 = threading.Thread(target=f1, args=(5,))
t2 = threading.Thread(target=f2, args=(5,))
t1.start()
t1.join()
t2.start()

for i in range(5):
print(threading.current_thread().getName())


if __name__ == '__main__':
main()

--------------------------------------------------

# Output:
Thread-1
Thread-1
Thread-1
Thread-1
Thread-1
Thread-2------0
Thread-2------1
MainThread
Thread-2------2
MainThread
Thread-2------3
MainThread
Thread-2------4
MainThread
MainThread

这样就实现了 t1 独占资源,完成后才给其他线程

查看所有线程

threading.enumerate()
以列表形式返回当前所有存活的 Thread 对象。
该列表包含守护线程,current_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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import time
import threading


def say_hi():
for i in range(3):
print('Hi!')
time.sleep(1)


def say_yes():
for i in range(3):
print('Yes!')
time.sleep(1)


def main():
t1 = threading.Thread(target=say_hi)
t2 = threading.Thread(target=say_yes)

t1.start()
t2.start()

while True:
print(threading.enumerate())
if len(threading.enumerate()) <= 1:
break
time.sleep(1)


if __name__ == '__main__':
main()

--------------------------------------------------

# Output:
Hi!
Yes!
[<_MainThread(MainThread, started 1292)>, <Thread(Thread-1, started 7968)>, <Thread(Thread-2, started 3796)>]
[<_MainThread(MainThread, started 1292)>, <Thread(Thread-1, started 7968)>, <Thread(Thread-2, started 3796)>]
Hi!
Yes!
Hi!
Yes!
[<_MainThread(MainThread, started 1292)>, <Thread(Thread-1, started 7968)>, <Thread(Thread-2, started 3796)>]
[<_MainThread(MainThread, started 1292)>, <Thread(Thread-1, started 7968)>]

多线程共享全局变量

假设现在有个任务,去抓取某个网站上所有的照片,然后发送到某个邮箱。
这里就可以分成两个子任务,一个抓取照片,一个发送照片
于是我们可以创建一个线程负责抓取照片,一个线程负责发送照片

那么可以发现,照片是两个线程都要用到的数据。
所以我们应该把照片设置为全局变量,这样两个线程才可以进行合作。

在多线程任务中,全局变量是可以给每一个线程共享的。

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

g_num = 10 # 全局变量


def add1():
global g_num
g_num += 5
print("add1----------- %d" % g_num)


def add2():
print("add2----------- %d" % g_num)


def main():
t1 = threading.Thread(target=add1, name='add1')
t2 = threading.Thread(target=add2, name='add2')

t1.start()
t2.start()
print("main----------- %d" % g_num)


if __name__ == "__main__":
main()

--------------------------------------------------

# Output:
add1----------- 15
add2----------- 15
main----------- 15

从栗子中可以看出,线程t1中的add1对全局变量g_num进行了修改
在线程t2add2g_num进行访问的时候访问到的是修改后的值
在主线程的main方法中访问也是修改后的值。

所以:全局变量对于任何一个线程,包括主线程,来说都是共享的。

全局变量对于任何一个线程来说都是共享的,那就一定会引发争抢资源的情况,这是由CPU的运行机制导致的。
如果对同一个全局变量,两个线程一个写一个读倒还好,但是如果两个同时写就会引发各种问题。
特别是在银行、金融领域,会引发更大的问题。比如一边转了帐还没收到突然出现问题,转账的扣钱了,收账的没收到,那就凉凉了~

所以为了解决这种缺陷,引入了锁的概念。
举个栗子,很多人上一个厕所,前面的进去了得锁门吧?办完事了开锁出来后面的才能进去,上锁,办事儿。
锁就是这样的概念。哪个线程先抢到锁哪个就先执行,执行完了释放锁,下一个线程抢到了就锁上…..
这叫做同步控制

互斥锁

创建锁:mutex = threading.Lock(),默认没有上锁的
锁定:mutex.acquire(blocking=True, timeout=1)
解锁:mutex.release()

  1. acquire(blocking=True, timeout=1) 成功获得锁返回True,否则返回Flase
  • blocking:设置为True时,会一直等到前面的锁释放然后锁定,设置为Flase则不会。
  • timeout:浮点型,单位——秒;
    • 值为正数时,等待时长为设定的秒数
    • 值为-1时,将无限等待
    • blocking=Flase时,timeout不起作用。
  1. release() 锁被锁定时,重置为未锁定。锁没被锁定时,引发RuntimeError错误。
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
42
43
44
import threading
import time

g_num = 0
mutex = threading.Lock()


def add1(num):
global g_num
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release()
print("add1----------- %d" % g_num)


def add2(num):
global g_num
mutex.acquire()
for i in range(num):
g_num += 1
mutex.release()
print("add2----------- %d" % g_num)


def main():
t1 = threading.Thread(target=add1, args=(1_000_000, ))
t2 = threading.Thread(target=add2, args=(1_000_000, ))

t1.start()
t2.start()
time.sleep(2)
print("main----------- %d" % g_num)


if __name__ == "__main__":
main()

--------------------------------------------------

# Output:
add1----------- 1000000
add2----------- 2000000
main----------- 2000000

死锁

锁并非只有一个,可以有多个。有多个锁,就可能发生死锁。

举个栗子,
电影里经常能看见的镜头——俩人打架不分上下,A锁住了B的脚踝,B锁住了A的脖子。
A:你放开我
B:你先放开我
A:你放开我我就放开你
B:你先放开我,我再放开你
A:凭什么我先放
B:那我也不放
…2000 year later…
两人死了。
The end.

这就是死锁,互不相让,互相锁了对方需要的资源,都等着对方先释放

线程A上了A锁干活,线程B上了B锁,这是两把锁,所以相安无事。
运行着运行着线程A需要一份资源,这份资源被线程B上了锁用着呢;线程B也需要一份资源,这份资源被线程A上了锁用着呢。
这时候线程A和线程B都在等对方释放锁,就这么一直等一直等

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
42
43
44
45
46
47
48
49
50
51
52
import time
import threading

class ThreadLockA(theading.Thread):
def run(self):
mutexA.acquire() # 使用A锁上锁

print(self.name + '-----------doA--up--')
time.sleep(1)

# 这里会上锁失败,因为上面 time.sleep(1)的时候B锁已经被上锁了
# 或者此线程后执行,A锁已经被上锁了,会上锁失败。
# 所以死锁会发生在这
mutexB.acquire() # 请求B锁上锁
print(self.name + '-----------doA--down--')
mutexB.release()

mutexA.release() # 对A锁进行解锁

class ThreadLockB(threading.Thread);
def run(self):
mutexB.acquire() # 使用B锁上锁

print(self.name + '-----------doB--up--')
time.sleep(1)

# 这里会上锁失败,因为上面 time.sleep(1)的时候A锁已经被上锁了
# 或者此线程后执行,A锁已经被上锁了,会上锁失败。
# 所以死锁会发生在这
mutexA.acquire()
print(self.name + '-----------doB--down--')
mutexA.release()

mutexB.release() # 对B锁进行解锁

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

if __name__ == '__main__':
t1 = ThreadLockA()
t2 = ThreadLockB()
t1.start()
t2.start()

--------------------------------------------------

# Output:
-----------doA--up--
-----------doB--up--

···
这里已经卡死了

为了避免这种情况,又两种方法

  1. 程序设计时要尽量避免(使用银行家算法)
  2. 添加超时时间等

哔哔