《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

失足程式設計師 發表於 2021-04-06
CPU

序言

什麼高TPS?QPS,其實很多人都知道,還有人說大資料,大流量這些關鍵詞夜以繼日的出現在我們眼前;

針對高TPS,QPS這些詞彙還有一個次可能比較陌生那就是CCU,tps,qps接受滿天飛,CCU在遊戲服務端出現比較多,

一個運營(SP)如果問研發(CP)你們遊戲承載是多少?通常他們想知道,你們能承載多少玩家線上,並且能承載每個玩家在一秒內有多少個操作;

通常,MMO的RPG類遊戲,FPS類遊戲,對玩家同時操作要求都相對較高,比如團戰,這時候玩家的操作是極具頻繁的;

在遊戲界很多人都知道傳統的頁遊,或者備份手遊,在伺服器端,設計是就是以不同的地圖切換來做整個世界場景,

每一個場景通過傳送門切換到下一章地圖;在傳統遊戲做法就是每一張地圖就是一個執行緒,這個在遊戲界廣為流傳的程式碼《秦美人》模式;

這樣做的好處就是每一個地圖都是單獨的執行緒,自己管理自己範圍的事情,不會衝突,但是理論終究是理論,

實際線上運營情況就是,某些地圖比如《主城》《副本入口》《活動入口》這些地方聚集了大量的玩家,在某些低等級地圖或者沒什麼任務和裝逼產出的地圖玩家少的可憐甚至沒有;

在傳統遊戲,比如最早的盛大代理的《冒險島》業內家喻戶曉的《秦美人》程式碼都有分線這麼一說就是為了解決在一張地圖人太多,一個執行緒處理不了的問題;

這種傳統模式就是一句話,忙得忙死,閒的閒死,一句套用現在皮友的一句話,澇的澇死,旱的旱死;

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

 那麼應運而生的是什麼

沒錯就是我們今天要講的環形排隊佇列;可能聽起來有點繞口,

其目的是什麼呢?通過佇列模型,達到執行緒恭喜,不再有專有執行緒,減少執行緒數量,執行緒做什麼事情由佇列說了算;

 

我們通常佇列是這樣的,先進先出,

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

排隊佇列是什麼情況呢?

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

就是佇列裡面的某一項,當從佇列裡面獲取到這一項的時候,發現這一項本身不是一個任務而是一個佇列;

然後取出這一項佇列裡面的第一項來執行,

一般來講我們需要的佇列基本也就是兩層就夠了,

當然你們如果需要三層,或者更多,你們可以稍加改動我們後面的程式碼;

翠花上程式碼

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換 

佇列列舉

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換
 1 package com.ty.test.queue;
 2 
 3 /**
 4  * 佇列key值
 5  *
 6  * @author: Troy.Chen(失足程式設計師, 15388152619)
 7  * @create: 2021-04-06 11:19
 8  **/
 9 public enum QueueKey {
10     /**
11      * 預設佇列是不區分,順序執行
12      */
13     Default,
14     /**
15      * 登入任務處理
16      */
17     Login,
18     /**
19      * 聯盟,工會處理
20      */
21     Union,
22     /**
23      * 商店處理
24      */
25     Shop,
26     ;
27 
28 }
View Code

 

 佇列任務

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換
 1 package com.ty.test.queue;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6  * @author: Troy.Chen(失足程式設計師, 15388152619)
 7  * @create: 2021-04-06 11:35
 8  **/
 9 public abstract class TyEvent implements Serializable {
10 
11     private static final long serialVersionUID = 1L;
12 
13     private QueueKey queueKey;
14 
15     public abstract void run();
16 
17     public TyEvent(QueueKey queueKey) {
18         this.queueKey = queueKey;
19     }
20 
21     public QueueKey getQueueKey() {
22         return queueKey;
23     }
24 }
View Code

最關鍵的程式碼來了,這個是主佇列,

  1 package com.ty.test.queue;
  2 
  3 import java.io.Serializable;
  4 import java.util.HashMap;
  5 import java.util.concurrent.LinkedBlockingQueue;
  6 import java.util.concurrent.TimeUnit;
  7 import java.util.concurrent.atomic.AtomicInteger;
  8 import java.util.concurrent.locks.ReentrantLock;
  9 
 10 /**
 11  * 佇列
 12  *
 13  * @author: Troy.Chen(失足程式設計師, 15388152619)
 14  * @create: 2021-04-06 11:14
 15  **/
 16 public class TyQueue implements Serializable {
 17 
 18     private static final long serialVersionUID = 1L;
 19     /*同步鎖保證佇列資料的準確性*/
 20     protected ReentrantLock reentrantLock = new ReentrantLock();
 21     /*子佇列容器*/
 22     protected HashMap<QueueKey, TySubQueue> subQueueMap = new HashMap<>();
 23 
 24     /*佇列容器*/
 25     protected LinkedBlockingQueue<Object> queue = new LinkedBlockingQueue<>();
 26     /*佇列裡面包括子佇列任務項*/
 27     protected final AtomicInteger queueSize = new AtomicInteger();
 28 
 29     /**
 30      * 新增任務
 31      *
 32      * @param obj
 33      */
 34     public void add(TyEvent obj) {
 35         reentrantLock.lock();
 36         try {
 37             if (obj.getQueueKey() == QueueKey.Default) {
 38                 /*預設模式直接加入佇列*/
 39                 queue.add(obj);
 40             } else {
 41                 TySubQueue subQueue = subQueueMap.get(obj.getQueueKey());
 42                 if (subQueue == null) {
 43                     subQueue = new TySubQueue(obj.getQueueKey());
 44                     subQueueMap.put(obj.getQueueKey(), subQueue);
 45                 }
 46                 /*有排隊情況的,需要加入到排隊子佇列*/
 47                 subQueue.add(obj);
 48                 /*這裡是關鍵,*/
 49                 if (!subQueue.isAddQueue()) {
 50                     subQueue.setAddQueue(true);
 51                     /*如果當前子佇列不在佇列項裡面,需要加入到佇列項裡面去*/
 52                     queue.add(subQueue);
 53                 }
 54             }
 55             /*佇列的資料加一*/
 56             queueSize.incrementAndGet();
 57         } finally {
 58             reentrantLock.unlock();
 59         }
 60     }
 61 
 62 
 63     /**
 64      * 獲取任務
 65      *
 66      * @return
 67      * @throws InterruptedException
 68      */
 69     public TyEvent poll() throws InterruptedException {
 70         Object poll = this.queue.poll(500, TimeUnit.MILLISECONDS);
 71         if (poll instanceof TySubQueue) {
 72             try {
 73                 reentrantLock.lock();
 74                 TySubQueue subQueue = (TySubQueue) poll;
 75                 poll = subQueue.poll();
 76             } finally {
 77                 reentrantLock.unlock();
 78             }
 79         }
 80         if (poll != null) {
 81             /*執行減一操作*/
 82             this.queueSize.decrementAndGet();
 83             return (TyEvent) poll;
 84         }
 85         return null;
 86     }
 87 
 88     /**
 89      * 當任務執行完成後操作
 90      *
 91      * @param obj
 92      */
 93     public void runEnd(TyEvent obj) {
 94         reentrantLock.lock();
 95         try {
 96             if (obj.getQueueKey() != QueueKey.Default) {
 97                 TySubQueue subQueue = subQueueMap.get(obj.getQueueKey());
 98                 if (subQueue != null) {
 99                     if (subQueue.size() > 0) {
100                         /*這個時候需要把佇列重新新增到主佇列*/
101                         queue.add(subQueue);
102                     } else {
103                         /*當子佇列空的時候,標識佇列已經不在主佇列裡面,等待下次加入新任務*/
104                         subQueue.setAddQueue(false);
105                     }
106                 }
107             }
108         } finally {
109             reentrantLock.unlock();
110         }
111     }
112 }

 

子佇列,也就是排隊佇列的關鍵所在

 1 package com.ty.test.queue;
 2 
 3 import java.io.Serializable;
 4 import java.util.LinkedList;
 5 
 6 /**
 7  * 下級佇列
 8  *
 9  * @author: Troy.Chen(失足程式設計師, 15388152619)
10  * @create: 2021-04-06 11:14
11  **/
12 public class TySubQueue extends LinkedList<TyEvent> implements Serializable {
13 
14     private static final long serialVersionUID = 1L;
15 
16     private final QueueKey queueKey;
17     private boolean addQueue = false;
18 
19     public TySubQueue(QueueKey queueKey) {
20         this.queueKey = queueKey;
21     }
22 
23     public QueueKey getQueueKey() {
24         return queueKey;
25     }
26 
27     public boolean isAddQueue() {
28         return addQueue;
29     }
30 
31     public TySubQueue setAddQueue(boolean addQueue) {
32         this.addQueue = addQueue;
33         return this;
34     }
35 
36     @Override
37     public String toString() {
38         return "{" + "queueKey=" + queueKey + ", size=" + size() + '}';
39     }
40 }

 

 測試一下結果

 1 package com.ty.test.queue;
 2 
 3 /**
 4  * @author: Troy.Chen(失足程式設計師, 15388152619)
 5  * @create: 2021-04-06 11:46
 6  **/
 7 public class Test {
 8 
 9     public static final TyQueue queue = new TyQueue();
10 
11     public static void main(String[] args) {
12         queue.add(new TyEvent(QueueKey.Default) {
13             @Override
14             public void run() {
15                 System.out.println(Thread.currentThread().getId() + ", 1");
16             }
17         });
18         queue.add(new TyEvent(QueueKey.Default) {
19             @Override
20             public void run() {
21                 System.out.println(Thread.currentThread().getId() + ", 2");
22             }
23         });
24         queue.add(new TyEvent(QueueKey.Default) {
25             @Override
26             public void run() {
27                 System.out.println(Thread.currentThread().getId() + ", 3");
28             }
29         });
30 
31         T t1 = new T();
32         T t2 = new T();
33         T t3 = new T();
34         t1.start();
35         t2.start();
36         t3.start();
37     }
38 
39     public static class T extends Thread {
40 
41         @Override
42         public void run() {
43             while (!Thread.currentThread().isInterrupted()) {
44                 TyEvent poll = null;
45                 try {
46                     poll = queue.poll();
47                     if (poll != null) {
48                         /*執行任務*/
49                         poll.run();
50                     }
51                 } catch (InterruptedException interruptedException) {
52                     Thread.currentThread().interrupt();
53                 } catch (Throwable throwable) {
54                     throwable.printStackTrace(System.out);
55                 } finally {
56                     if (poll != null) {
57                         /*當然任務執行完成後*/
58                         queue.runEnd(poll);
59                     }
60                 }
61             }
62         }
63 
64     }
65 
66 }

 

我們用三個執行緒測試一下,在沒有排隊情況下執行輸出

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

 我們可以看到三個任務分別有三個執行緒執行了;

接下來我們在佇列裡面再額外加入三個登入排隊佇列

 1         queue.add(new TyEvent(QueueKey.Login) {
 2             @Override
 3             public void run() {
 4                 System.out.println(Thread.currentThread().getId() + ", Login 1");
 5             }
 6         });
 7         queue.add(new TyEvent(QueueKey.Login) {
 8             @Override
 9             public void run() {
10                 System.out.println(Thread.currentThread().getId() + ", Login 2");
11             }
12         });
13         queue.add(new TyEvent(QueueKey.Login) {
14             @Override
15             public void run() {
16                 System.out.println(Thread.currentThread().getId() + ", Login 3");
17             }
18         });

 

再看看輸出情況,

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換

很明顯的可以看到我們加入到登入佇列的任務,又同一個執行緒順序執行的;

總結

排隊佇列就是為了讓同一型別任務順序執行或者叫多工操作同一個物件的時候減少加鎖 帶來的額外開銷,減少執行緒等待的時間;

更合理的利用的執行緒。避免澇的澇死,旱的旱死;

我要訂一個小目標

《環形佇列》遊戲高《TPS》模式下減少cpu執行緒切換