高效併發模式 -- Half-Sync/Half-Async vs. Leader-Follower

併發是提高系統吞吐量的關鍵手段,但是構建高性能的併發系統並非易事。通過利用經典的模式可以使我們站在巨人的肩上,Half-Sync/Half-Async和Leader-Follower正是兩種最經典的併發模式。

在實際中模式學習的難點往往在於好像看懂了,但是卻總不能在正確的場景中使用。爲了使大家能更好的理解和在實際中正確的使用這兩種模式,下面將通過構建高性能網絡通信的場景(Java NIO)來對模式進行一一的解析。

 

Java NIO簡介

在NIO出現前,Java IO的各種流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據被讀取,或數據完全寫入。該線程在此期間不能再幹任何事情了。Java NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什麼都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其他的事情。線程通常將非阻塞IO的空閒時間用於在其它通道上執行IO操作,所以一個單獨的線程現在可以管理多個輸入和輸出通道(channel),NIO可以減少線程資源消耗支持更多的併發連接。

NIO中的Seletor就是利用單一線程管理多個併發連接/通道(channel)的關鍵,如果你的應用打開了多個通道,但每個連接的流量都很低,使用Selector就會很高效。

使用Selector的典型代碼通常如下: 

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(8000));
Selector selector = Selector.open();
serverSocketChannel.configureBlocking(false);
//將Selector註冊到ServerSocket
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//通過selector來輪詢channel,接收事件
while (true) {
    int readyChannels = selector.select();
    if (readyChannels == 0) continue;
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            SocketChannel client = serverSocketChannel.accept();
      client.write(ByteBuffer.wrap("Hello\n".getBytes()));
      client.close();
        } else if (key.isConnectable()) {
            // a connection was established
        } else if (key.isReadable()) {
            // a channel is ready for reading
        } else if (key.isWritable()) {
            // a channel is ready for writing
        }
        keyIterator.remove();
    }
}

爲了簡單以上只處理了Accept事件。

可以看到以上通過selector來檢查事件的輪訓過程必須在線程安全的狀態下進行,試想如果是在多線程的情況下,可能會發生多個線程同時處理一個channel的同一事件的情況。

然而,完全的單線程環境會大大降低服務程序的吞吐量,所以我們可以採用以下模式來提高吞吐量。

 

Half-Sync/Half-Async

爲了提高系統的吞吐量,請求處理分爲兩層同步層和異步層。同步層通過Selector輪詢channel獲取的事件,而事件的處理則是通過其它工作線程來異步完成。兩層之間通過一個隊列來協調,這樣事件的處理過程就不會阻塞輪詢線程,可以讓不同channel的事件獲得更快的響應,不同事件的處理可以同步進行,大大提高了系統的吞吐量。

代碼修改的如下。

ExecutorService threadPool = Executors.newFixedThreadPool(NUM_OF_THREADS);
while (true) {
    int readyChannels = selector.select();
    if (readyChannels == 0) continue;
    Set<SelectionKey> selectedKeys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            SocketChannel client = serverSocketChannel.accept();
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                      client.write(ByteBuffer.wrap("Hello\n".getBytes()));
                      client.close();
                }
            });
    ...

爲了提高性能程序進一步使用了線程池來做異步事件處理。

你可能會在想對比原理圖協調同步層和異步層之間的隊列在哪呢?其實這個隊列已經包含在ExecutorService的線程池官方實現中了,以下是newFixedThreadPool的官方實現代碼:

public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

通過分析你會發現Half-Sync/Half-Async模式中,需要在同步和異步模式中切換,需要將數據通過隊列在從網絡I/O線程傳遞到工作線程,這裏通常會涉及動態的內存分配,以及需要進行鎖同步的寫入及讀取刪除等隊列操作。這些都會對性能有一定的影響。同時,數據在不同線程中傳遞或被訪問也破壞了CPU的緩存機制來從而也影響了性能。

 

Leader-Follower

Half-Sync/Half-Async,由於事件的接收和處理過程中存在線程的切換,從而導致了上面提及的一些影響性能的問題。下面要介紹的Leader-Follower就是針對如何避免以上線程切換問題而設計的一種併發模式。

Leader-Follower同樣是基於一組預啓動的線程來實現,這組線程會選出一個線程作爲Leader。Leader線程將準備接收I/O事件(如:上面提及的通過selector輪詢的方式),當Leader線程接收到事件後,就會:

  • 讓出Leader的位置,從而選取另一個線程來作爲leader準備接收I/O事件,
  • 同時該線程將繼續處理接收到的event,從而避免了事件接收與處理過程的線程切換。

以上方式就實現了不同事件的並行處理。

代碼如下:

class WorkerThread implements Runnable {
  private int workID;
  private Lock leaderToken;
  private Selector selector;
  private ServerSocketChannel serverSocketChannel;

  public WorkerThread(int workID, Lock leaderToken, Selector selector, ServerSocketChannel serverSocketChannel) {
    this.leaderToken = leaderToken;
    this.serverSocketChannel = serverSocketChannel;
    this.workID = workID;
    this.selector = selector;
  }

  @Override
  public void run() {
    while (true) {
      leaderToken.lock(); // 等待獲取Leader token,獲取後成爲leader線程
      System.out.println("work " + this.workID + " got leader token.");
      try {
        out: while (true) { // check the ready channel
          int readyChannels;
          readyChannels = selector.select();
          if (readyChannels == 0)
            continue;
          Set<SelectionKey> selectedKeys = selector.selectedKeys();
          Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
          while (keyIterator.hasNext()) {// handle the event
            SelectionKey key = keyIterator.next();
            if (key.isAcceptable()) {
              keyIterator.remove();
              SocketChannel client = serverSocketChannel.accept();
              leaderToken.unlock(); // 接收完事件後,釋放Leader Token,讓其它線程成爲Leader
              // 繼續處理事件
              System.out.println("work " + this.workID + " released leader token.");
              client.write(ByteBuffer.wrap("Hello\n".getBytes()));
              client.close();
              break out;
            }

          }

        }
      } catch (Exception e) {
        throw new RuntimeException(e);
      } finally {
        if (leaderToken.tryLock()) {
          leaderToken.unlock();
        }
      }
    }

  }
}

public class LeaderFollowerServer {
  final static int NUM_OF_THREAD = 6;
  public void start() throws Exception {
    Lock leaderToken = new ReentrantLock();
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().bind(new InetSocketAddress(8000));
    Selector selector = Selector.open();
    serverSocketChannel.configureBlocking(false);
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    for (int i = 0; i < NUM_OF_THREAD; i++) {
      new Thread(new WorkerThread(i, leaderToken, selector, serverSocketChannel)).start();
    }
  }

}

代碼中通過一個同步量(lock)的獲取與釋放來實現Leader的切換。

與Half-Sync/Half-Async相比Leader-Follower模式具有以下優點:

  1. 提高了CPU緩存親和性,避免了不同線程間數據共享。

  2. 避免了通過在不同線程間交換數據,減少了鎖同步操作。

  3. 由於接收事件到處理事件間沒有切換過程,便於提高事件處理的實時性。

 

完整代碼參見:

https://github.com/chaocai2001/pattern_oriented_software_architecture/tree/master/concurrency_patterns

 

歡迎關注我的Go語言教程(http://gk.link/a/102s8),教程中在構建大規模高可用性系統一節也向大家介紹了兩種經典的架構模式(pipe-filter 和 micro-kernel)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章