并发:指的是任务数多余cpu核数,通过操作系统的各种任务调度算法,实现用多个任务“一起”执行(实际上总有一些任务不在执行,因为切换任务的速度相当快,看上去一起执行而已)

并行:指的是任务数小于等于cpu核数,即任务真的是一起执行的

一、线程的概念

线程是一个进程内部的一条执行路径,是程序执行的最小单位

线程有自己的栈和局部变量,多个线程共享同一进程的地址空间

多线程就是在一个进程中创建多个线程,每个线程完成一个任务

二、线程的实现

1. 使用threading模块

(1)单线程执行

import time

def saySorry():
    print('亲爱的,我错了,我能吃饭了吗')
    time.sleep(1)


if __name__ == '__main__':
    for i in range(5):
        saySorry()

运行会发现,每隔一秒打印一句“亲爱的,我错了,我能吃饭了吗”

(2)多线程执行

实例化Thread对象,通过target参数指定线程将要执行的函数名,args参数通过元组来指定函数的位置实参,kwargs参数通过字典来指定函数的关键字实参。

import time
import threading

def saySorry():
    print('亲爱的,我错了,我能吃饭了吗')
    time.sleep(1)

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

可以明显看出来,使用了多线程并发的操作,花费的时间要短很多

当调用start()时,才会真正创建线程,并且开始执行

2、主线程会等待所有子线程结束后才结束

import threading
from time import ctime, sleep

def sing():
    for i in range(5):
        print('正在唱歌。。。%d' %i)
        sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞。。。%d' %i)
        sleep(1)



if __name__ == '__main__':
    print('---开始---:%s' %ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    sleep(5)  
    print('---结束---%s' %ctime())

运行结果:

---开始---:Mon Sep 16 20:02:46 2019
正在唱歌。。。0
正在跳舞。。。0
正在跳舞。。。1
正在唱歌。。。1
正在跳舞。。。2
正在唱歌。。。2
正在跳舞。。。3
正在唱歌。。。3
正在跳舞。。。4
正在唱歌。。。4
---结束---Mon Sep 16 20:02:51 2019
import threading
from time import ctime, sleep

def sing():
    for i in range(5):
        print('正在唱歌。。。%d' %i)
        sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞。。。%d' %i)
        sleep(1)



if __name__ == '__main__':
    print('---开始---:%s' %ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    # sleep(5)  # 通过屏蔽延时,程序是否会马上停止
    print('---结束---%s' %ctime())

输出结果:

---开始---:Mon Sep 16 20:04:45 2019
正在唱歌。。。0
正在跳舞。。。0
---结束---Mon Sep 16 20:04:45 2019
正在唱歌。。。1
正在跳舞。。。1
正在跳舞。。。2
正在唱歌。。。2
正在跳舞。。。3
正在唱歌。。。3
正在唱歌。。。4
正在跳舞。。。4

3、查看线程数量

import threading
from time import sleep, ctime


def sing():
    for i in range(5):
        print('正在唱歌。。。%d' %i)
        sleep(1)


def dance():
    for i in range(5):
        print('正在跳舞。。。%d' %i)
        sleep(1)


if __name__ == '__main__':

    print('---开始---:%s' %ctime())

    t1 = threading.Thread(target=sing)
    t2 = threading.Thread(target=dance)

    t1.start()
    t2.start()

    while 1:
        length = len(threading.enumerate())
        print('当前运行的线程数为:%d' %length)
        if length<=1:
            break
        sleep(0.5)

运行结果:

---开始---:Mon Sep 16 20:12:37 2019
正在唱歌。。。0
正在跳舞。。。0
当前运行的线程数为:3
当前运行的线程数为:3
正在跳舞。。。1
正在唱歌。。。1
当前运行的线程数为:3
当前运行的线程数为:3
正在唱歌。。。2正在跳舞。。。2

当前运行的线程数为:3
当前运行的线程数为:3
正在跳舞。。。3正在唱歌。。。3

当前运行的线程数为:3
当前运行的线程数为:3
正在跳舞。。。4正在唱歌。。。4

当前运行的线程数为:3
当前运行的线程数为:3
当前运行的线程数为:1

4、线程执行代码的封装

通过使用threading模块能完成多任务的程序开发,为了让每个线程的封装性更完美,所以使用threading模块时,往往会定义一个新的子类class,只要继承threading.Thread就可以了,然后重写run方法。

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()

运行结果:

I'mThread-1@0
I'mThread-1@1
I'mThread-1@2

说明:

python的threading.Thread类有一个run方法,用于定义线程的功能函数,可以在自己的线程类中覆盖该方法。而创建自己的线程实例后,通过Thread类的start方法,可以启动该线程,交给python虚拟机进行调度,当该线程获得执行的机会时,就会调用run方法执行线程。

每个线程默认有一个名字,尽管上面的例子中没有指定线程对象的name,但是python会自动为线程指定一个名字。

5、线程的执行顺序

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)
            print(msg)

def test():
    for i in range(5):
        t = MyThread()
        t.start()


if __name__ == '__main__':
    test()

执行结果:(运行的结果可能不一样,但是大体是一致的)

I'mThread-3@0
I'mThread-1@0
I'mThread-2@0
I'mThread-4@0
I'mThread-5@0
I'mThread-1@1
I'mThread-3@1
I'mThread-2@1
I'mThread-5@1I'mThread-4@1

I'mThread-1@2
I'mThread-2@2I'mThread-3@2

I'mThread-5@2I'mThread-4@2

从代码和执行结果来看,多线程程序的执行顺序是不确定的,当执行到sleep语句时候,线程将被阻塞,到sleep结束,线程进入就绪状态,等待调度。而线程调度将自动选择一个线程执行。上面代码只能保证每个线程都运行完整的run函数,但是线程的启动顺序,run函数中每次循环的执行顺序都不能确定。

当线程的run()方法结束时该线程完成。

无法控制线程调度程序,但可以通过别的方式来影响线程调度的方式。

三、多线程共享全局变量

1、共享全局变量

import time
import threading


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 = threading.Thread(target=work1)
t1.start()

time.sleep(3)  # 延时,保证t1线程中的事情做完

t2 = threading.Thread(target=work2)
t2.start()

运行结果:

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

2、列表当作实参传递到线程中

import time
import threading


def work1(nums):
    nums.append(44)
    print('---in work1---', nums)


def work2(nums):
    time.sleep(1)  # 延时保证t1线程中的事情做完
    print('---in work2---',nums)


g_nums = [11,22,33]

t1 = threading.Thread(target=work1, args=(g_nums,))
t1.start()

t2 = threading.Thread(target=work2,args=(g_nums,))
t2.start()

运行结果

---in work1--- [11, 22, 33, 44]
---in work2--- [11, 22, 33, 44]

在一个进程内,所有线程共享全局变量,很方便多个线程间共享数据

缺点就是,线程对全局变量随意更改,可能造成多线程之间对全局变量的混乱(即线程非安全)

3、多线程全局共享变量开发可能会遇到的问题

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

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

a、在g_num=0时,t1取得g_num=0。此时系统把t1调度为”sleeping”状态,把t2转换为”running”状态,t2也获得g_num=0

b、然后t2对得到的值进行加1并赋给g_num,使得g_num=1

c、然后系统又把t2调度为”sleeping”,把t1转为”running”。线程t1又把它之前得到的0加1后赋值给g_num。

d、这样导致虽然t1和t2都对g_num加1,但结果仍然是g_num=1

测试1:

import time
import threading


g_num = 0

def word1(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=word1, args=(100,))
t1.start()


t2 = threading.Thread(target=work2, args=(100,))
t2.start()


while len(threading.enumerate()) != 1:
    time.sleep(1)

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

运行结果:

---线程创建之前g_num is 0---
---in work1,g_num is 100---
---in work2, g_num is 200---
2个线程对同一个全局变量操作之后的最终结果是:200

测试2:

import time
import threading


g_num = 0

def word1(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=word1, args=(1000000,))
t1.start()


t2 = threading.Thread(target=work2, args=(1000000,))
t2.start()


while len(threading.enumerate()) != 1:
    time.sleep(1)

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

运行结果:

---线程创建之前g_num is 0---
---in work2, g_num is 1331011---
---in work1,g_num is 1382473---
2个线程对同一个全局变量操作之后的最终结果是:1382473

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

四、同步

1、同步的概念

同步就是协同步调,按预定的先后次序进行运行。如:你说完,我再说。

“同”字从字面上容易理解为一起动作。

其实不是,"同"字应是指协同、协助、互相配合。

如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B执行,再将结果给A;A再继续操作。

02-13 01:36
查看更多