python进阶6——线程和同步

阻塞与同步异步

同步

同步就是在发出一个调用后,如果没有得到结果,调用就不返回。调用返回,就一定得到返回值。 换句话说,就是由调用者主动等待这个调用的结果。

异步

而异步相反,调用在发出后就直接返回了,但是没有顺带返回结果。所以一个异步过程调用发出并返回后,调用者不会立刻得到结果。而是在调用发出后,被调用者主动通过状态、通知来通知调用者,或通过回调函数继续处理。

阻塞和非阻塞

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)期间的状态. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。

非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

还是上面的例子,你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。如果是关心阻塞 IO/ 异步 IO, 参考 Unix Network Programming View Book–还是2014年写的以解释概念为主,主要是同步异步 阻塞和非阻塞会被用在不同层面上,可能会有不准确的地方,并没有针对 阻塞 IO/ 异步 IO 等进行讨论,大家可以后续看看这两个回答:

作者:Yi Lu

链接:https://www.zhihu.com/question/19732473/answer/20851256

经典例子

老张爱喝茶,废话不说,煮开水。

出场人物:老张,水壶两把(普通水壶,简称水壶;会响的水壶,简称响水壶)。

1 老张把水壶放到火上,立等水开。(同步阻塞)

2 老张把水壶放到火上,去客厅看电视,时不时去厨房看看水开没有。(同步非阻塞)

老张还是觉得自己有点傻,于是变高端了,买了把会响笛的那种水壶。水开之后,能大声发出嘀~~~~的噪音。

3 老张把响水壶放到火上,立等水开。(异步阻塞)

4 老张把响水壶放到火上,去客厅看电视,水壶响之前不再去看它了,响了再去拿壶。(异步非阻塞)

所谓同步异步,只是对于水壶而言。

普通水壶,同步;响水壶,异步。虽然都能干活,但响水壶可以在自己完工之后,提示老张水开了。这是普通水壶所不能及的。同步只能让调用者去轮询自己(情况2中),造成老张效率的低下。

所谓阻塞非阻塞,仅仅对于老张而言。

立等的老张,阻塞;看电视的老张,非阻塞。情况1和情况3中老张就是阻塞的,媳妇喊他都不知道。虽然3中响水壶是异步的,可对于立等的老张没有太大的意义。所以一般异步是配合非阻塞使用的,这样才能发挥异步的效用。

作者:愚抄

链接:https://www.zhihu.com/question/19732473/answer/23434554

python并发

GIL锁

GIL指全局解释器锁(Global Interpreter Lock),在多核多线程处理中,会有多个线程同时操作一个变量,但同时Python使用引用计数进行内存管理,多个不同的变量名可能指向同一片内存,如果不加锁访问,显然可能会导致内存泄漏的问题。同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行,就像C++的代码,有GCC,VC,等不同的编译环境,但是并不是所有的python环境都有GIL锁。在有GIL的环境中,线程必须先拿到GIL,才能执行,但GIL只有一个,拿不到通行证的线程,就不允许进入CPU执行。这就使得python代码在这种情况下无法使用多线程加速。

import time
from threading import Thread

COUNT = 50000000


def countdown(n):
    while n > 0:
        n -= 1


t1 = Thread(target=countdown, args=(COUNT // 2,))
t2 = Thread(target=countdown, args=(COUNT // 2,))
start = time.time()
t1.start()
t2.start()#t2执行时t1也在执行
t1.join()
t2.join()
end = time.time()
print('Mul Time taken in seconds -', end - start)
countdown(COUNT)
endd = time.time()
print('Time taken in seconds -', endd - end)

在这里插入图片描述

在GIL的影响下,我们希望并发执行的两段线程和执行一段线程的时间并没有明显的差别。但换到支持多线程加速的C++下形式明显不一样:

#include<iostream>
#include <thread>
#include<chrono>
using namespace std;
using namespace std::chrono;
void NumberAdd(long start, long end, long& ans) {
    int sum = 0;
    for (long i = start; i < end; i++)
        sum = sum + 1;
    ans = sum;
}
template<class T>//测量时间函数
void measure(T&& func) {
    auto beg_t = system_clock::now();       //开始时间
    func();								    //执行函数
    auto end_t = system_clock::now();       //结束时间
    duration<double> diff = end_t - beg_t;
    printf("performTest total time: ");
    cout << diff.count() << endl;
}
int main() {
    long times = 500000000;
    measure([times]() {           //双线程
        long ans1 = 0, ans2 = 0;
        thread t1 = thread(NumberAdd, 0, times / 2, ref(ans1));
        thread t2 = thread(NumberAdd, times / 2, times, ref(ans2));
        t1.join();
        t2.join();
        cout << "result of two treads: " << ans1 + ans2 << endl;
        }
    );

    measure([times]() {           //单线程
        long ans = 0;
        NumberAdd(0, times, ans);
        cout << "result of single treads: " << ans << endl;
        }
    );
}

在这里插入图片描述
C++的双线程仅用了一半左右的时间。
在我们迫切希望使用多线程加速时,python应该选用不带GIL的编译环境,或者自己执行C extension,虽然python对C的扩展支持不错,但这依然是一个麻烦的问题。

python进程结构

守护进程

创建主进程会创建守护进程
1)守护进程会在主进程代码执行结束后就终止,其子进程也会被迫终止。
2)守护进程内无法再开启子进程,否则抛出异常:AssertionError

from multiprocessing import Process

import time
def foo():
    print(123)
    time.sleep(1)
    print("end123")

def bar():
    print(456)
    time.sleep(3)
    print("end456")


p1=Process(target=foo)
p2=Process(target=bar)

p1.daemon=True
p1.start()
p2.start()
print("main-------")

在这里插入图片描述
print main后主进程就结束了,p1,p2也会因此立刻结束。(但会报错)

互斥锁

为了避免共享资源并发访问导致的不一致问题,引入了互斥锁。在 Python 中,可以使用 threading.Lock 类来创建一个互斥锁对象。当一个线程需要访问共享资源时,它需要先获取互斥锁对象,然后执行相应的操作,最后释放互斥锁。这样可以确保每个线程都能正确地对共享资源进行操作,避免了数据不一致的问题。

import threading
import time


def run():
    lock.acquire()  # 获取锁
    global num
    num += 1
    lock.release()  # 释放锁


lock = threading.Lock()  # 实例化一个锁对象

num = 0
t_obj = []

for i in range(20000):
    t = threading.Thread(target=run)
    t.start()
    t_obj.append(t)

for t in t_obj:
    t.join()

print("num:", num)

这里使用了 Python 的 threading 模块来创建多个线程,每个线程都会调用 run 函数。在 run 函数中,对全局变量 num 进行加 1 操作,由于多个线程可能同时访问 num 变量,因此需要使用锁来保证线程安全。在线程开始时就获得一个锁,再到结束时才释放,保证同时只有一个线程访问全局变量。

这段代码的作用是测试多线程环境下对共享变量的访问是否安全。由于多个线程可能同时访问同一个变量,因此需要使用锁来保证线程安全。在这个例子中,我们使用了一个互斥锁来保护全局变量 num,以确保每个线程都能正确地对其进行操作。
在这里插入图片描述
这里给出线程不安全的例子,

import threading

# 共享数据
shared_data = 0

# 执行增加共享数据的操作
def increment():
    global shared_data
    for _ in range(1000000):
        shared_data += 1

# 创建两个线程来执行增加操作
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)

# 启动线程
thread1.start()
thread2.start()

# 等待线程结束
thread1.join()
thread2.join()

# 打印最终的共享数据
print("Shared data:", shared_data)

上述代码是个明显线程不安全的例子,两个线程会并发地执行increment函数,该函数将共享数据shared_data增加1000000次。在每次增加操作中,线程会先读取共享数据的值,然后将其加1,最后再将结果写回共享数据,直觉告诉我们答案应该是2000000。
在这里插入图片描述
在这里插入图片描述
几乎每次的答案都不尽相同,这是因为这两个线程是并发执行的,可能会出现以下情况:

1,线程1读取shared_data的值为0。
2,然后线程2也读取shared_data的值为0。
3,线程1将0加1得到1,并将结果写回shared_data。
4,线程2也将0加1得到1,并将结果写回shared_data。
在这个例子中,两个线程的操作交叉进行,导致最终的shared_data的值可能不是预期的2000000,而是少于该值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值