惋惜Java中沒有yield return

項目中一個消息推送需求,推送的用戶數幾百萬,用戶清單很簡單就是一個txt文件,是由hadoop計算出來的。格式大概以下:javascript

uid  caller
123456  12345678901
789101  12345678901
……

如今要作的就是讀取文件中的每個用戶而後給他推消息,具體的邏輯可能要複雜點,但今天關心的是如何遍歷文件返回用戶信息的問題。html

以前用C#已經寫過相似的代碼,大體以下:java

     /// <summary>
        /// 讀取用戶清單列表,返回用戶信息。
        /// </summary>
        /// <param name="parameter">用戶清單文件路徑</param>
        /// <param name="position">推送斷點位置,用戶斷點推送</param>
        /// <returns></returns>
        public IEnumerable<UserInfo> Provide(string parameter, int position)
        {
            FileStream fs = new FileStream(parameter, FileMode.Open);
            StreamReader reader = null;
            try
            {
               reader = new StreamReader(fs);
                //獲取文件結構信息
                string[] schema = reader.ReadLine().Trim().Split(' ');
                for (int i = 0; i < position; i++)
                {
                    //先空讀到斷點位置
                    reader.ReadLine();  
                }
                while (!reader.EndOfStream)
                {
                    UserInfo userInfo = new UserInfo();
                    userInfo.Fields = new Dictionary<string, string>();
                    string[] field = reader.ReadLine().Trim().Split(' ');
                    for (int i = 0; i < schema.Length; i++)
                    {
                        userInfo.Fields.Add(schema[i].ToLower(), field[i]);
                    }

                    yield return userInfo;
                }
            }
            finally
            {
               reader.Close(); fs.Close();
            }
        }

代碼很簡單,就是讀取清單文件返回用戶信息,須要注意的就是標紅的地方,那麼yield return的做用具體是什麼呢。對比下面這個版本的代碼:程序員

     public IEnumerable<UserInfo> Provide2(string parameter, int position)
        {
            List<UserInfo> users = new List<UserInfo>();
            FileStream fs = new FileStream(parameter, FileMode.Open);
            StreamReader reader = null;
            try
            {
                reader = new StreamReader(fs);
                //獲取文件結構信息
                string[] schema = reader.ReadLine().Trim().Split(' ');
                for (int i = 0; i < position; i++)
                {
                    //先空讀到斷點位置
                    reader.ReadLine();
                }
                while (!reader.EndOfStream)
                {
                    UserInfo userInfo = new UserInfo();
                    userInfo.Fields = new Dictionary<string, string>();
                    string[] field = reader.ReadLine().Trim().Split(' ');
                    for (int i = 0; i < schema.Length; i++)
                    {
                        userInfo.Fields.Add(schema[i].ToLower(), field[i]);
                    }

                    users.Add(userInfo);
                }

                return users;
            }
            finally
            {
                reader.Close();
                fs.Close();
            }
        }

本質區別是第二個版本一次性返回全部用戶的信息,而第一個版本實現了惰性求值(Lazy Evaluation),針對上面的代碼簡單調試下,你會發現一樣是經過foreach進行迭代,第一個版本每次代碼運行到yield return userInfo的時候會將控制權交給「迭代」它的地方,然後面的代碼會在下次迭代的時候繼續運行。數據庫

     static void Main(string[] args)
        {
            string filePath = @"D:\users.txt";

            foreach (var user in new FileProvider().Provide(filePath,0))
            {
                Console.WriteLine(user);
            }
        }    

而第二個版本則須要等全部用戶信息所有獲取到才能返回。相比之下好處是顯而易見的,好比前者佔用更小的內存,cpu的使用更穩定:編程

固然作到這點是須要付出代價(維護狀態),真正的好處也並不在此。以前我在博客中對C#的yield retrun有一個簡單的總結,可是並無深刻的研究:c#

  1. IEnumerable是對IEnumerator的封裝,以支持foreach語法糖。
  2. IEnumerable<T>和IEnumerator<T>分別繼承自IEnumerable和IEnumerator以提供強類型支持(即狀態機中的「現態」是強類型)。
  3. yield return是編譯器對IEnumerator和IEnumerable實現的語法糖。
  4. yield return 表現是實現IEnumerator的MoveNext方法,本質是實現一個狀態機。
  5. yield return能暫時將控制權交給外部,我比做「程序上奇點」,讓你實現穿越。能達到目的:延遲計算、異步流。

先看第1條,迭代器模式是大部分語言都支持一個設計模式,它是一種行爲模式:設計模式

行爲模式是一種簡化對象之間通訊的設計模式。api

在C#中是IEnumerator接口,Java是Iterator,且目前都有泛型版本提供,語法層面上兩個接口基本是一致的。安全

    //C#   
    public interface IEnumerator
    {
        object Current { get; }
        bool MoveNext();
        void Reset();
    }

    //Java
    public interface Iterator<E> {
         boolean hasNext();
         E next();
         void remove();    
    }    

在C#2(注意須要區別.net,c#,CLR之間的區別)以前建立一個迭代器,C#和Java的代碼量是差很少的。兩個語言中除了迭代器接口以外還分別提供了一個IEnumerable和Iterable接口:

    //C#
    public interface IEnumerable<out T> : IEnumerable
    {
        IEnumerator<T> GetEnumerator();
    }

    //Java
    public interface Iterable<T> {
        Iterator<T> iterator();
    }

那麼它們的關係是什麼呢?語法上看IEnumerable是對IEnumerator的封裝,以支持foreach語法糖,但這麼封裝的目的是什麼呢,咱們的類直接實現IEnumerator接口不就好了。回答這個問題咱們須要理解迭代的本質是什麼。咱們使用的迭代器的目的是在不知道集合內部狀態的狀況下去遍歷它,調用者每次只想獲取一個元素,因此在返回上一個值的時候須要跟蹤當前的工做狀態:

  • 必須具備某個初始狀態。
  • 每次調用MoveNext的時候,須要維護當前的狀態。
  • 使用Current屬性的時候,返回生成的上一個值。
  • 迭代器須要知道什麼時候完成生成值的操做

因此實現迭代器的本質是本身維護一個狀態機,咱們須要本身維護全部的內部狀態,看下面一個簡單的實現IEnumerator接口的例子:

   public class MyEnumerator : IEnumerator
    {
        private object[] values;
        int position;

        public MyEnumerator(object[] values)
        {
            this.values = values;
            position = -1;
        }

        public object Current
        {
            get
            {
                if (position == -1 || position == values.Length)
                {
                    throw new InvalidOperationException();
                }

                return values[position];
            }
        }

        public bool MoveNext()
        {
            if (position != values.Length)
            {
                position++;
            }

            return position < values.Length;
        }

        public void Reset()
        {
            position = -1;
        }
    }
      static void Main(string[] args)
        {
            object[] values = new object[] { 1, 2, 3 };
            MyEnumerator it = new MyEnumerator(values);

            while (it.MoveNext())
            {
                Console.WriteLine(it.Current);
            }
        }

這個例子很簡單,也很容易實現,如今假設同時有兩個線程去迭代這個集合,那麼使用一個MyEnumerator對象明顯是不安全的,這正是IEnumerable存在的緣由,它容許多個調用者並行的迭代集合,而各自的狀態獨立互不影響,同時實現了foreach語法糖。這也是爲何C#中在迭代Dictionary的時候只是只讀緣由:

增長一個IEnumerable並無使問題變的很複雜:

  public class MyEnumerable : IEnumerable
    {
        private object[] values;

        public MyEnumerable(object[] values)
        {
            this.values = values;
        }

        public IEnumerator GetEnumerator()
        {
            return new MyEnumerator(values);
        }
    }

      static void Main(string[] args)
        {
            object[] values = new object[] { 1, 2, 3 };
            MyEnumerable ir = new MyEnumerable(values);

            foreach (var item in ir)
            {
                Console.WriteLine(item);
            }
        }   

言歸正傳,回到以前的yield return之上,看着很像咱們一般寫的return,可是yield return後面跟的是一個UserInfo對象,而方法的返回對象實際上是一個IEnumerable<UserInfo>對象,其實這裏也能夠返回一個IEnumerator<UserInfo>,這又是爲何呢?正如我前面說的「yield return是編譯器對IEnumerator和IEnumerable實現的語法糖」,其實全部這些又是編譯器在幕後作了不少鮮爲人知的「勾當」,不過這一次它作的更多。

看下用yield return實現上面的例子須要幾行代碼:

      static IEnumerable<int> GetInts()
        {
            yield return 1;
            yield return 2;
            yield return 3;
        }

多麼簡潔優雅!!!yield return大大簡化了咱們建立迭代器的難度。經過IL DASM反彙編看下編譯器都幹了些什麼:

能夠看到編譯器本質上仍是生成一個類的,主要看下MoveNext方法:

說實話,IL我基本一無所知,但大體能夠看出來使用了switch(實現跳轉表)和相似C語言中的goto語句(br.s),還好還有強大的reflect,逆向工程一下即可以還原「真相」:

private bool MoveNext()
{
    switch (this.<>1__state)
    {
        case 0:
            this.<>1__state = -1;
            this.<>2__current = 1;
            this.<>1__state = 1;
            return true;

        case 1:
            this.<>1__state = -1;
            this.<>2__current = 2;
            this.<>1__state = 2;
            return true;

        case 2:
            this.<>1__state = -1;
            this.<>2__current = 3;
            this.<>1__state = 3;
            return true;

        case 3:
            this.<>1__state = -1;
            break;
    }
    return false;
}

這即是編譯器所作的操做,其實就是實現了一個狀態機,要看完整的代碼朋友們能夠本身試下。這是一個再簡單不過的例子貌似編譯器作的並很少,那麼看下文章一開始我寫的那個讀取文件的例子:

[CompilerGenerated]
private sealed class <Provide>d__0 : IEnumerable<UserInfo>, IEnumerable, IEnumerator<UserInfo>, IEnumerator, IDisposable
{
    // Fields
    private int <>1__state;
    private UserInfo <>2__current;
    public string <>3__parameter;
    public int <>3__position;
    public FileProvider <>4__this;
    private int <>l__initialThreadId;
    public string[] <field>5__5;
    public FileStream <fs>5__1;
    public StreamReader <reader>5__2;
    public string[] <schema>5__3;
    public UserInfo <userInfo>5__4;
    public string parameter;
    public int position;

    // Methods
    [DebuggerHidden]
    public <Provide>d__0(int <>1__state);
    private void <>m__Finally6();
    private bool MoveNext();
    [DebuggerHidden]
    IEnumerator<UserInfo> IEnumerable<UserInfo>.GetEnumerator();
    [DebuggerHidden]
    IEnumerator IEnumerable.GetEnumerator();
    [DebuggerHidden]
    void IEnumerator.Reset();
    void IDisposable.Dispose();

    // Properties
    UserInfo IEnumerator<UserInfo>.Current { [DebuggerHidden] get; }
    object IEnumerator.Current { [DebuggerHidden] get; }
}

 
Expand Methods
private bool MoveNext()
{
    bool CS$1$0000;
    try
    {
        int i;
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                this.<fs>5__1 = new FileStream(this.parameter, FileMode.Open);
                this.<reader>5__2 = null;
                this.<>1__state = 1;
                this.<reader>5__2 = new StreamReader(this.<fs>5__1);
                this.<schema>5__3 = this.<reader>5__2.ReadLine().Trim().Split(new char[] { ' ' });
                i = 0;
                goto Label_0095;

            case 2:
                goto Label_0138;

            default:
                goto Label_0155;
        }
    Label_0085:
        this.<reader>5__2.ReadLine();
        i++;
    Label_0095:
        if (i < this.position)
        {
            goto Label_0085;
        }
        while (!this.<reader>5__2.EndOfStream)
        {
            this.<userInfo>5__4 = new UserInfo();
            this.<userInfo>5__4.Fields = new Dictionary<string, string>();
            this.<field>5__5 = this.<reader>5__2.ReadLine().Trim().Split(new char[] { ' ' });
            for (int i = 0; i < this.<schema>5__3.Length; i++)
            {
                this.<userInfo>5__4.Fields.Add(this.<schema>5__3[i].ToLower(), this.<field>5__5[i]);
            }
            this.<>2__current = this.<userInfo>5__4;
            this.<>1__state = 2;
            return true;
        Label_0138:
            this.<>1__state = 1;
        }
        this.<>m__Finally6();
    Label_0155:
        CS$1$0000 = false;
    }
    fault
    {
        this.System.IDisposable.Dispose();
    }
    return CS$1$0000;
} 

編譯器以嵌套類型的形式建立了一個狀態機,用來正確的記錄咱們在代碼塊中所處的位置和局部變量(包括參數)在該處的值,從上面的代碼中咱們看到了goto這樣的語句,確實這在C#中是合法的可是平時咱們基本不會用到,並且這也是不被推薦的。

一句yield return看似簡單,但其實編譯作了不少,並且盡善盡美,第一個例子中標紅的還有一處:

……
            finally
            {
                reader.Close();
                fs.Close();
            }
……    

使用yield return這點倒和return相似,就是finally塊在迭代結束後必定會執行,即便迭代是中途退出或發生異常,或者使用了yield break(這跟咱們日常使用的return很像):

       static IEnumerable<int> GetInts()
        {
            try
            {
                yield return 1;
                yield return 2;
                yield return 3;
            }
            finally
            {
                Console.WriteLine("do something in finally!");
            }
        }
       //main
          foreach (var item in GetInts())
            {
                Console.WriteLine(item);
                if (item==2)
                {
                    return;
                }
            }

這是由於foreach保證在它包含的代碼塊運行結束後會執行裏面全部finally塊的代碼。但若是咱們像下面這樣迭代集合話就不能保證finally塊必定能執行了:

 IEnumerator it = GetInts().GetEnumerator();
 it.MoveNext();
 Console.WriteLine(it.Current);
 it.MoveNext();
 Console.WriteLine(it.Current);
 it.MoveNext();
 Console.WriteLine(it.Current);    

這也提醒咱們在迭代集合的時候必定要用foreach,確保資源可以獲得釋放。 還有一點須要注意的就是yield return不能用於try...catch...塊中。緣由多是編譯在處理catch和yield return之間存在「衝突」,有知道的朋友能夠告訴我一下,感激涕零。

 

好了,到目前爲止yield return貌似只是一個普通的語法糖而已,但其實簡化迭代器只是它一個表面的做用而已,它真正的妙處正如我前面總結的:

  • yield return能暫時將控制權交給外部,我比做「程序上奇點」,讓你實現穿越。能達到目的:延遲計算、異步流。

其實本文第一個代碼片斷已經能說明這點了,可是體現的做用只是延遲計算,yield return還有一個做用就是它能讓咱們寫出很是優雅的異步編程代碼,之前在C#中寫異步流,全篇充斥這各類BeginXXX,EndXXX,雖然能達到目的可是整個流程支離破碎。這裏舉《C# In Depth》中的一個例子,講的是微軟併發和協調運行庫CCR。

假如咱們正在編寫一個須要處理不少請求的服務器。做爲處理這些請求的一部分,咱們首先調用一個Web服務來獲取身份驗證令牌,接着使用這個令牌從兩個獨立的數據源獲取數據(能夠認爲一個是數據庫,另一個是Web服務)。而後咱們要處理這些數據,並返回結果。每個提取數據的階段要話費一點時間,好比1秒左右。咱們可選擇兩種常見選項僅有同步處理和異步處理。

 這個例子很具表明性,不由讓我想到了以前工做中的一個場景。咱們先來看下同步版本的僞代碼:

HoldingsValue ComputeTotalStockValue(string user, string password)
{
   Token token = AuthService.Check(user, password);
   Holdings stocks = DbService.GetStockHoldings(token);
   StockRates rates = StockService.GetRates(token);
   return ProcessStocks(stocks, rates);
}

同步很容易理解,但問題也很明確,若是每一個請求要話費1秒鐘,整個操做將話費3秒鐘,並在運行時佔用整個線程。在來看異步版本的:

void StartComputingTotalStockValue(string user, string password)
{
   AuthService.BeginCheck(user, password, AfterAuthCheck, null);
}
void AfterAuthCheck(IAsyncResult result)
{
   Token token = AuthService.EndCheck(result);
   IAsyncResult holdingsAsync = DbService.BeginGetStockHoldings
   (token, null, null); 
   StockService.BeginGetRates
   (token, AfterGetRates, holdingsAsync);
}
void AfterGetRates(IAsyncResult result)
{
   IAsyncResult holdingsAsync = (IAsyncResult)result.AsyncState;
   StockRates rates = StockService.EndGetRates(result);
   Holdings holdings = DbService.EndGetStockHoldings
   (holdingsAsync);
   OnRequestComplete(ProcessStocks(stocks, rates));
}

我想不須要作太多解釋,可是它確實比較難理解,最起碼對於初學者來講是這樣的,並且更重要的一點是它是基於多線程的。最後來看下使用CCR的yield return版本:

IEnumerator<ITask> ComputeTotalStockValue(string user, string pass)
{
   Token token = null;
   yield return Ccr.ReceiveTask(
   AuthService.CcrCheck(user, pass) 
   delegate(Token t){ token = t; }
   );
   Holdings stocks = null;
   StockRates rates = null;
   yield return Ccr.MultipleReceiveTask(
   DbService.CcrGetStockHoldings(token),
   StockService.CcrGetRates(token),
   delegate(Stocks s, StockRates sr) 
   { stocks = s; rates = sr; }
   );
   OnRequestComplete(ProcessStocks(stocks, rates));
}

在瞭解CCR之間,這個版本可能更難理解,可是這是僞代碼咱們只須要理解它的思路就能夠了。這個版本能夠說是綜合了同步和異步版本的優勢,讓程序員能夠按照以往的思惟順序的寫代碼同時實現了異步流。

這裏還有一個重點是,CCR在等待的時候並無使用專用線程。其實一開始瞭解yield return的時候,我覺得它是經過多線程實現,其實它使用的是一種更小粒度的調度單位:協程

協程和線程的區別是:協程避免了無心義的調度,由此能夠提升性能,但也所以,程序員必須本身承擔調度的責任,同時,協程也失去了標準線程使用多CPU的能力。

關於協程網上有不少資料,各類語言也有支持,能夠說C#對協程的支持是經過yield return來體現的,並且很是優雅。其餘語言中貌似也有相似的關鍵字,好像javascript中也有。

 

好吧說了這麼多回到文章的標題上來,由於某些緣由,如今要用Java實現文章一開頭那個需求。剛剛接觸Java,菜鳥一枚,本想應該也會有相似C#中yield return的語法,可是卻碰壁了。找來找去就發現有一個Thread.yield(),看了api貌似並非我想要的。好不容易在stackoverflow上找到一篇帖子:Yield Return In Java。裏面提到了一個第三方庫實現了相似C#中的yield return語法,下載地址

激動萬分啊,惋惜使用後發現貌似有點問題。這個jar包裏提供一個Yielder類,按照它Wiki中的示例,只要繼承這個類實現一個yieldNextCore方法就能夠了,Yielder類內部有一個yieldReturn()和yieldBreak()方法,貌似就是對應C#中的yield return和yield break,因而我寫下了下面的代碼:

    public static void main(String[] args) {
        Iterable<Integer> itr = new Yielder<Integer>() {
            @Override
            protected void yieldNextCore() {
                yieldReturn(1);
                yieldReturn(2);
                yieldReturn(3);
            }
        };

        Iterator<Integer> it = itr.iterator();
    
        while (it.hasNext()) {
            Object rdsEntity = it.next();
            System.out.println(rdsEntity);
        }
    }

可是運行結果確是一直不停的輸出3,不知道我實現的有問題(Wiki裏說明太少)仍是由於這個庫以來具體的操做系統平臺,我在CentOS和Ubuntu上都試了,結果同樣。不知道有沒有哪位朋友知道這個jar包,很想知道緣由啊。

 

在網上搜了不少,如我所料Java對協程確定是支持的,只不過沒有C#中yield return這樣簡潔的語法糖而已,我一開始的那個問題必然不是問題,只不過可能我是被C#寵壞了,纔會發出「惋惜Java中沒有yield return」這樣的感慨,仍是好好學習,每天向上吧。

相關文章
相關標籤/搜索