生产者-消费者

原文地址

前言

在计算机世界当中,数据在不断产生的同时,也在不停地被处理着。产生数据的一方被我们称作生产者,而另一方则被称为消费者。在网络请求中,服务器是提供数据的生产者,而用户是数据的消费者。伴随着互联网的高速发展,即便是百万乃至千万用户级的访问量也并非罕见,而服务器在面临如此高的并发请求时仍能做到游刃有余。这背后的技术并不是三言两语能够说清的,但其中关系着一个重要的模式——生产者-消费者模式

生产者-消费者模式是通过增加缓冲区来平衡生产者消费者的速率差问题。生产者消费者彼此不直接通信,通过缓冲区来完成任务

打个比方,以前肯德基的点餐员在点餐之后会顾客配好餐,然后再结账。这种时候,后排的人总是要等到前排完成所有的环节之后才能开始点餐,而现在用户点完餐只需要等待叫号取餐。取餐区就是肯德基在就餐环节中加入的缓冲区

好的模式总是源于生活

生产者-消费者模式似乎更多的被应用在后端的并发处理中,前端开发也很少去提及这一模式。其实在开发的各个环节中,系统早就做了很多生产者-消费者的平衡工作,我们在享受着它带来的便利的同时,却不自知。本文盘点在开发一些常见的生产者-消费者

线程&队列

由于生产者-消费者模式是为了平衡二者因处理能力的不同而产生的速率差问题,所以速率差具体表现存在两种情况:生产>消费生产<消费

  • 生产>消费

    比较常见的一种情况是存在多个并发的网络请求,当请求任务执行完毕时,需要将数据持久化到数据库中。为了避免数据被破坏,在网络层和数据层中间一般会引入一个同步处理机制,作为平衡数据生产和数据处理的缓冲区。在应用中,同步处理机制可能会在网络层中完成,也可能放到数据层当中

    前者在网络请求层创建了一个串行队列,请求结果dispatch到这个队列中进行回调,甚至直接dispatch到主线程中回调即可。第三方网络请求库如AFNetworking就在在回调前将请求结果处理放在同一个队列中执行

      [session dataTaskWithRequest: request 
                  completionHandler: ^(NSData * _Nullable data, 
                                      NSURLResponse * _Nullable response, 
                                      NSError * _Nullable error) {
          if (error != nil) {
              dispatch_async(dispatch_get_main_queue(), ^ {
                  complete(data, error);
              });
          }
      }
    

    后者则是在数据层内部创建了一个串行队列,将所有IO操作都派发到这个队列中执行

      - (void)insertData: (NSData *)data {
          dispatch_async(_dbQueue, ^{
              [_db executeUpdate: [self _insertSqlWithData: data];
          });
      }
    
  • 消费>生产

    在派发任务少于可执行线程数量时,数据消费的能力要高于数据产出。在平时开发中通过async将任务派发到并发队列之后,GCD会从缓存的线程池查找可执行线程,然后执行任务。在libdispatch中可以看到GCD线程池最多缓存64个线程

    考虑到并发队列的可控性太弱,如果对任务执行顺序有需求,又想享受GCD动态控制线程带来的好处,那么维护一套自己的serialQueue或者使用NSOperationQueue也是可行的,下面代码截取自GCD封装

      LXD_INLINE DispatchContext __LXDDispatchContextGetForQos(LXDQualityOfService qos) {
          static DispatchContext contexts[5];
          int count = (int)[NSProcessInfo processInfo].activeProcessorCount;
      count = MIN(1, MAX(count, LXD_QUEUE_MAX_COUNT));
          switch (qos) {
              case LXDQualityOfServiceUserInteractive: {
                  static dispatch_once_t once;
                  dispatch_once(&once, ^{
                      contexts[0] = __LXDDispatchContextCreate("com.sindrilin.user_interactive", count, qos);
                  });
                  return contexts[0];
              }
          
              case LXDQualityOfServiceUserInitiated: {
                  static dispatch_once_t once;
                  dispatch_once(&once, ^{
                      contexts[1] = __LXDDispatchContextCreate("com.sindrilin.user_initated", count, qos);
                  });
                  return contexts[1];
              }
          
              case LXDQualityOfServiceUtility: {
                  static dispatch_once_t once;
                  dispatch_once(&once, ^{
                      contexts[2] = __LXDDispatchContextCreate("com.sindrilin.utility", count, qos);
                  });
                  return contexts[2];
              }
          
              case LXDQualityOfServiceBackground: {
                  static dispatch_once_t once;
                  dispatch_once(&once, ^{
                      contexts[3] = __LXDDispatchContextCreate("com.sindrilin.background", count, qos);
                  });
                  return contexts[3];
              }
          
              case LXDQualityOfServiceDefault:
              default: {
                  static dispatch_once_t once;
                  dispatch_once(&once, ^{
                      contexts[4] = __LXDDispatchContextCreate("com.sindrilin.default", count, qos);
              });
                  return contexts[4];
              }
          }
      }
    

    当然,除了任务队列方案以外,pthread_t提供了创建、维护线程的能力,如果有需要,我们也能维护自己的线程池来获得更好的控制能力,可以参见EvansMusic的回答的回答,下面是使用线程池的伪代码

      if (_threadsPool.count > 0) {
          Thread *thread = _threadsPool.pop();
          thread.wakeup();
          thread.execute(task);
      } else {
          Thread *thread = Thread();
          thread.execute(task);
          thread_sleep(thread);
          _threadPool.append(thread);
      }
    

锁&信号量

当锁作为缓冲机制生效时,基本是消费>生产的情况。通过线程锁让消费者线程陷入休眠中避免造成不必要的开销,等待唤醒消费资源。

if (_lock.try()) {
    thread.consume(resource);
} else {
    thread.sleep();
    wait_threads.append(thread);
}

信号量是多线程中功能更强大的缓冲机制,它允许大于1个的消费者进入临界区:

GCD的信号量实现可以参见Semaphore原理与实现

双缓冲机制

双缓冲机制在笔者的印象中在硬件上的实现居多,软件上见到的相对比较少,比如GCD就采用了双缓存区的方式。虽然iOS允许我们创建多个任务队列、并设置不同的优先级,但是任务最终会根据优先级和串行是否并行重新加入到系统的八个全局队列中。overcommit属性的队列表示的是并行执行的任务队列,在有新任务加入进来之后如果没有可执行的线程,那么就会新建线程去执行这个任务

除了GCD的任务队列设计,iOS的渲染机制也是采用双缓冲区的方式实现的。双缓冲区相较单缓冲区更好的处理了帧数据的读取和刷新的效率平衡,但是也可能导致页面撕裂的情况。为了解决这个问题,iOS始终使用垂直同步,详细可以参见iOS保持界面流畅的技巧

其他

消费者-生产者是一种经过漫长历史考验的代码设计之一,缓冲区的加入很好的避免了两者需要等待对方执行完成,很好的平衡了二者速率不一导致的资源浪费。也顺带也解除了两者的依赖,实现了解耦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容