同步和异步的一些事

最近在编程总是会遇到async, Mutex等字眼,今天来盘点以下同步和异步,顺便复习以下操作系统

进程(Process)

正在运行的程序。人们写的程序只定义了入口函数(比如__main_, main(), __main__)。如果执行这个程序,操作系统会为这个程序生成进程。比如在linux中,内核会从0号进程fork()出一个新的进程,接着把我们的代码装载进这个进程,最后这个进程从入口函数开始一步步运行。进程具有一定的资源,比如虚拟内存空间,正在执行程序文件。进程具有一定的标志,比如权限,调度优先级,进程运行状态。

线程(Thread)

考虑到进程的创建和销毁需要维护大量的数据结构(进程表),再加上随着科技的发展,多核cpu逐渐称为主流,为了充分利用多核,人们创造了更小的单位:线程。多个线程能够共享同一个进程的资源。貌似linux在某个版本之后的调度算法将cpu的最小调度单元改成了线程,从而增加了对多核的支持。

协程(Coroutine)

比线程更小的单位。协程的调度并不需要系统调用,而是在一个线程内部实现。相当于一个用户态的的操作系统,但是基本的io操作依赖于线程的io系统调用。主要用来解决io密集线程过多的问题。比如1亿个线程的io读写,那么有很多的时间耗费在了线程切换之上(线程也有数据结构,只是比进程简单, 而且线程的切换是系统调用)。因此人们干脆把调度算法写在线程中,在线程调度协程,那么就只有一个线程,1亿个协程。时间虽然都花在了协程的切换之上,但由于协程的数据结构更简单,且不用频繁系统调用,耗时也减少了。

互斥

资源有限而需求无限。A和B都想买车票,一个售票口只能排队。假如A在B前面,B只能进入等待。

同步(sync)

B在A之后才能执行。

  1. 上面互斥的例子,B只能等待A完成也是一种同步。
  2. 生产者和消费者和商店。消费者到商店购买产品,没有产品只能等生产者造出产品送到商店才行。同步能让消费者进入等待。

异步(async)

B在A之后执行,不过B可以忙别的事。

  1. 上面互斥的例子,B可以刷刷抖音,或者视线排队在边上坐着。等待A的通知,或者售票员喇叭的通知。
  2. 上面同步的第二个例子i,消费者可以刷刷抖音或者在家里躺着。等待生产者的通知,或者商店的短信。

异步的实现

  1. 创建(spawn)一个线程等待资源,主线程干别的事。
  2. 线程获取资源后通过回调函数执行下一步,或者通过消息队列通知主线程。

简化异步

这个困惑了我很久,如今网上的热词如异步线程, 阻塞队列天生并发等等等其实都是说的一件事: 简化异步。众所周知,操作系统已经帮我们实现了线程间异步。以linux为例, 线程天生就有就绪, 等待, 运行三个状态,并且内核也维护了一个阻塞队列。如果从头实现异步,我们需要

  • 创建自己的消息队列
  • 时不时自己管理线程
  • 甚至维护自己的线程池

而这些所谓的高并发语言, 都维护了一个自己的阻塞队列,维护了自己的消息队列,维护了自己的线程池,维护了自己的并发io库,并且拥有自己的异步语法糖。我们只需要创建一个异步函数即可,线程的分配,分离底层库都帮我们实现了。

因此异步线程可以看作特殊的线程: 操作系统的调度器作普通的线程,语言本身的运行时调度器则区别对待。因此用来写服务器非常方便,既可以减少普通线程的开销,又不用自己写搞协程,收到一个请求就async出一个线程。

简化异步的编程范式

以nodejs为例分析简化异步写法的历史与发展,其他的大同小异。

  • 回调函数时期。为了支持异步,于是将IO进行封装,尾巴带了一个回调函数

      // 同步IO函数
      function trueIO(): ResultType {
          ...
          return res;
      }
    
      // 将IO操作和trueIO装入一个线程,并加入阻塞队列
      // 从而不会阻止主线程
      function trueIOAsync(callback: (result: ResultType) => void) {
          setTimeout(function () {
              const res = trueIO();
              callback(trueIO);
          }, 0)
      }
    
      // 没有实现阻塞队列的语言可能会用spawn
      function trueIOAsync(callback: (result: ResultType) => void) {
          spawn(function () {
              const res = trueIO();
              callback(trueIO);
          })
      }
    
      // 调用异步IO函数
      trueIOAsync(function (res) {
          console.log(res);
      })
    

    注:这个trueIOspawn是我假设的函数

  • Promise时期。后来有人觉得回调函数不太优雅,加上动态语言,函数式思想,builder模式,链式调用的流行,就改变了封装方法,Promise应运而生

    class Promise {
        callback: ((value: any) => void)[];
    
        new(fn: ((result: ResultType) => void, r(err: ErrType) => void) => void) {
           fn(this.innerResolve);
        }
    
        then<T>(fn: (value: T) => void) {
            this.callback.push(fn);
            return this;
        }
    
        innerResolve(result) {
            this.callback.forEach(fn => fn(result));
        }
    }
    

    这样就能够统一管理回调函数,于是可以把回调函数封装成链式调用

     // 封装异步IO函数,加入阻塞队列
     function trueIOPromise() {
         return new Promise(function (res) {
         setTimeout(function () {
             const res = trueIO();
             res(res);
         }, 0);
     })
    
     // 调用异步IO函数
     trueIOPromise().then((res) {console.log(res) });
     }
    

    上面是比较底层的Promise,我们也可以画蛇添足通过异步回调函数构造Promise

    function trueIOPromise() {
        return new Promise(function (res) {
            trueIOAsync(res(res));
        });
    }
    
  • async, await。有人觉得new Promise(...)依旧不够优雅,因此出现了async和await。

    // 借用上面的trueIOPromise()
    async function asyncFn() {
        return await trueIOPromise();
    }
    
    // 经过转译后
    function asyncFn() {
        return trueIOPromise();
    }
    
    
    // 第二个例子
    async function asyncFn() {
        res = await trueIOPromise();
        res++;
        return res;
    }
    
    // 经过转译后
    function asyncFn() {
        return new Promise(function (resolve) {
            let res = undefined;
            trueIOPromise().then(function (result) {
               res = result;
               res++;
               resolve(res);
            })
        })
    }
    

以上就是三个阶段。至于基于强大过程宏的rustfuture, invoke也是大同小异。

异步封装

其实创建一个Promise并不会加入阻塞队列,只有调用栈最底层Promise使用了诸如setTimeout的异步调用才会。那么如果使用async/await封装同步函数将毫无作用。

async function truIOAsync() {
    return trueIO();
}


// 转译
function trueIOAsync() {
    return new Promise(function (resolve) {
        let result = trueIO();
        resolve(result);
    })
}
// 使用异步调用才有用
async function trueIOAsync() {
    return await new Pormise(function (resolve) {
        setTimeout(function () {
            let result = trueIO();
            resolve(result);
        }, 0)
    })
}

封装同步函数就是脱下裤子放屁,毫无作用。大可不必追求新的写法而闹笑话。

同步锁

为了实现资源访问的互斥,并且为了减少开关中断系统调用的开销,在代码层面维护一个锁。一次只能一个线程访问。

异步锁

考虑了异步线程和线程的区别。因为异步本质上是交给另一个线程去工作,那么就会有多线程互斥问题。同步锁要求所有线程互斥,包括异步线程。而异步锁则不要求主线程和异步线程两者互斥。

死锁

多个线程需要两个以上的资源,每个资源互斥,则可能死锁。

热门相关:仙城纪   大神你人设崩了   巡狩万界   时间都知道   天启预报