C#中await/async閑說

自從C#5.0增加異步編程之後,異步編程越來越簡單,async和await用的地方越來越多,越來越好用,只要用異步的地方都是一連串的異步,如果想要異步編程的時候,需要從底層開始編寫,這樣後邊使用的時候就是異步,那麼底層是如何實現??我們如何編寫高效率的異步方法??

#了解基於任務的異步模式(TAP)

基於任務的異步編程模型 (TAP) 提供了異步代碼的抽象化,你只需像往常一樣將代碼編寫為一連串語句即可,在開始調用的地方運行。例如:var task = method()①; await task②; 在①的時候開始運行可能還沒有運行完,在②程序掛起等待運行完,中間怎麼運行的你不需要知道,編譯器會做若干操作的。當開啟多個任務的時候,像要他們都執行完,在執行其他的時候,可以await Task.WhenAll(task1,task2 …..);

#了解async/await

await 運算符應用於異步方法,在方法的執行中插入掛起點,直到所等待任務完成。使用async 和await定義異步方法不一定會創建新線程,當編譯器看到await關鍵字時,線程會掛起等待運行結束。
await 僅可用於由 async 關鍵字修改的異步方法中,使用 async 修飾符定義的方法通常包含一個或多個 await 表達式,使用await運算符的任務通常是實現[基於任務的異步模式(TAP)]的方法調用返回,返回值包括 Task、Task<TResult>、ValueTask 和 ValueTask<TResult> 對象的方法。

# 調用 Task.Wait() 或者 Task.Result 立刻產生死鎖的充分條件

1. 調用 Wait() 或 Result 的代碼位於 UI 線程。
2. Task 的實際執行在其他線程,且需要返回 UI 線程。
死鎖的原因:UWP、WPF、Windows Forms 程序的 UI 線程都是單線程的。為了避免產生死鎖,你應該一條道走到黑, Async All the Way。或者.ConfigureAwait(false)

# ValueTask與Task的區別

7.0為async新增的ValueTask的作用(如果沒有在Nuget上下載System.Threading.Tasks.Extensions,ValueTask就在這個庫中),ValueTask用於值類型的異步;Task為引用類型的,每次需要分配空間。
例如:

public async Task<int> CalculateSum(int a, int b) {
    if (a == 0 && b == 0)
    {
        return 0;
    }

    return await Task.Run(() => a + b);
}

當a,b=0的時候不會運行到task里,這個時候返回task就造成了資源的浪費,修改為以下會效率更高

public async ValueTask<int> CalculateSum2(int a, int b)
{
    if (a == 0 && b == 0)
    {
        return 0;
    }

    return await Task.Run(() => a + b);
}

但是也不是說到處用ValueTask會好,當是引用類型的時候,用ValueTask,你需要關注更多的數據,這個時候用Task會更好。

# await/async原理分析

[AsyncStateMachine(typeof(Class1.<CalculateSum2>d__1))]
public ValueTask<int> CalculateSum2(int a, int b)
{
    Class1.<CalculateSum2>d__1 <CalculateSum2>d__;
    <CalculateSum2>d__.a = a;
    <CalculateSum2>d__.b = b;
    <CalculateSum2>d__.<>t__builder = AsyncValueTaskMethodBuilder<int>.Create();
    <CalculateSum2>d__.<>1__state = -1;
    AsyncValueTaskMethodBuilder<int> <>t__builder = <CalculateSum2>d__.<>t__builder;
    <>t__builder.Start<Class1.<CalculateSum2>d__1>(ref <CalculateSum2>d__);
    return <CalculateSum2>d__.<>t__builder.Task;
}

對CalculateSum2代碼解析,發現沒有await/async,原來又是編譯器提供的語法糖。

[__DynamicallyInvokable, DebuggerStepThrough, SecuritySafeCritical]
public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{
    if (stateMachine == null)
    {
        throw new ArgumentNullException("stateMachine");
    }
    ExecutionContextSwitcher executionContextSwitcher = default(ExecutionContextSwitcher);
    RuntimeHelpers.PrepareConstrainedRegions();
    try
    {
        ExecutionContext.EstablishCopyOnWriteScope(ref executionContextSwitcher);
        stateMachine.MoveNext();
    }
    finally
    {
        executionContextSwitcher.Undo();
    }
}

對Start方法進行分析,可以看出MoveNext,程序的運行其實還是一步一步進行的,那麼await/async會不會創建一個線程,這倒是不一定,這個由線程池決定,那麼異步了不創建一個線程,怎麼異步的,這裏的異步可能是運行在已經有的線程上。

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

DDD中的聚合和UML中的聚合以及組合的關係

UML:

聚合關係:成員對象是整體的一部分,但是成員對象可以脫離整體對象獨立存在。
如汽車(Car)與引擎(Engine)、輪胎(Wheel)、車燈(Light)之間的關係為聚合關係,引擎、輪胎、車燈可以脫離車而存在,比如把一個引擎換到另一個汽車上也可以。

組合關係:也表示的是一種整體和部分的關係,但是在組合關係中整體對象可以控製成員對象的生命周期,一旦整體對象不存在,成員對象也不存在。

所以,UML中聚合與組合的共同點:是兩者都是整體與部分的關係,差別點:是整體消亡后,成員對象是否可以脫離整體對象而單獨存在。

DDD聚合

也是一種整體和部分的關係,部分脫離整體會變得毫無意義,整體和部分之間強調一致的生命周期,也就是整體消亡的話部分也一起消亡,不能單獨存在。所以,從定義來看DDD中的聚合應該和UML中的組合關係是同一種關係。所以,Martin Flower也說,DDD中的聚合是限定在DDD這個方法論的上下文中,它不同於其他上下文(UML)中的聚合。

一個例子

按照上面的定義,我們再來分析一下一個典型的例子,就是公司和部門的關係。

UML的角度:
1、一個公司由多個部門組成,所以滿足整體和部分的關係;
2、一個部門不能脫離公司和加入到其他公司;

所以,我們推導出,在UML中公司和部門應該屬於組合關係。

DDD的角度:
雖然基於UML的角度,公司和部門屬於組合關係,那在DDD中是否應該把部門聚合在公司下面呢?我的看法是,雖然從生命周期上,確實部門不能脫離公司。但是DDD的聚合設計要考慮的因素會更加豐滿,比如:

  • DDD強調需求和Bounded Context,也就是會基於需求和上下文進行建模,我們建模前必須要先確定當前的需求和上下文是什麼;
  • 整體在當前上下文是否強關心部分的存在;
  • 整體和部分之間是否存在某些不變性規則;
  • 操作整體與操作部分的業務場景是否一致;
  • 性能問題,如果整體聚合的部分的數量過大,那也不會考慮聚合,即小聚合原則;
  • 一致性問題,我們在設計系統時,即便把本該是聚合在一起的對象分開設計為多個聚合,也可以從技術上去解決一致性,比如通過領域服務來完成多個聚合的協同創建、刪除、修改,並能通過數據庫事務來保證嚴格的強一致性;
  • DDD領域建模會對領域概念進行抽象,所以再領域模型中,在有些業務系統如組織架構管理系統中,也許就沒有公司了,而是只有部門,把公司也看成是一個頂層的部門就行,所以自然就不會有公司這個聚合根了;

所以,在進行DDD聚合設計時,如果僅從整體消亡後部分是否仍然存在意義這個點去推導的話,那考慮的就太單薄了,很有可能會得出不合理的聚合設計,最終很可能會導致聚合設計過大。這是沒有認真分析業務需求,沒有分析業務規則不變性,沒有對領域概念進行合理抽象,沒有進行OO軟件設計原則的應用的表現。

所以,以上案例由於需求不明,無法進行聚合設計。

題外話

我覺得DDD是對OOA/D的一個衍生,OOA/D是一種面向對象分析與設計的思想,強調通過設計對象,為對象分配職責,並讓對象之間通過協作的方式來完成軟件功能。而DDD則是對OO中的對象進行進一步的細化,比如首次提出了聚合、聚合根、實體、值對象、工廠、領域服務、領域事件,等。尤其是聚合的提出,讓OO設計更加豐富,大大減少了對象之間的關係複雜度,以及對象之間邊界的更加清晰。但是聚合的設計也很有難度,比如技術人員需要從我上面列舉的這些角度(不限於此)去進行聚合分析設計,這對開發人員的能力素質是一個很大的要求,可以說如果不會OOA/D的分析思維,就很難進行DDD領域建模。所以,我想這也是DDD很難在業務系統中落地的一個很大的原因之一吧。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

HTTP認證之摘要認證——Digest(一)

導航

  • HTTP認證之基本認證——Basic(一)
  • HTTP認證之基本認證——Basic(二)
  • HTTP認證之摘要認證——Digest(一)
  • HTTP認證之摘要認證——Digest(二)

一、概述

Digest認證是為了修復基本認證協議的嚴重缺陷而設計的,秉承“絕不通過明文在網絡發送密碼”的原則,通過“密碼摘要”進行認證,大大提高了安全性。

相對於基本認證,主要有如下改進:

  • 絕不通過明文在網絡上發送密碼
  • 可以有效防止惡意用戶進行重放攻擊
  • 可以有選擇的防止對報文內容的篡改

需要注意的是,摘要認證除了能夠保護密碼之外,並不能保護其他內容,與HTTPS配合使用仍是一個良好的選擇。以下是摘要認證的具體流程圖:

看到上面出現了那麼多之前沒見過的參數,是不是有點慌(或是興奮)?別著急,這裏先給出一個概覽:

  • WWW-Authentication:用來定義使用何種方式(Basic、Digest、Bearer等)去進行認證以獲取受保護的資源
  • realm:表示Web服務器中受保護文檔的安全域(比如公司財務信息域和公司員工信息域),用來指示需要哪個域的用戶名和密碼
  • qop:保護質量,包含auth(默認的)和auth-int(增加了報文完整性檢測)兩種策略,(可以為空,但是)不推薦為空值
  • nonce:服務端向客戶端發送質詢時附帶的一個隨機數,這個數會經常發生變化。客戶端計算密碼摘要時將其附加上去,使得多次生成同一用戶的密碼摘要各不相同,用來防止重放攻擊
  • nc:nonce計數器,是一個16進制的數值,表示同一nonce下客戶端發送出請求的數量。例如,在響應的第一個請求中,客戶端將發送“nc=00000001”。這個指示值的目的是讓服務器保持這個計數器的一個副本,以便檢測重複的請求
  • cnonce:客戶端隨機數,這是一個不透明的字符串值,由客戶端提供,並且客戶端和服務器都會使用,以避免用明文文本。這使得雙方都可以查驗對方的身份,並對消息的完整性提供一些保護
  • response:這是由用戶代理軟件計算出的一個字符串,以證明用戶知道口令
  • Authorization-Info:用於返回一些與授權會話相關的附加信息
  • nextnonce:下一個服務端隨機數,使客戶端可以預先發送正確的摘要
  • rspauth:響應摘要,用於客戶端對服務端進行認證
  • stale:當密碼摘要使用的隨機數過期時,服務器可以返回一個附帶有新隨機數的401響應,並指定stale=true,表示服務器在告知客戶端用新的隨機數來重試,而不再要求用戶重新輸入用戶名和密碼了

二、剖析

1.當打開需要認證的頁面時,會彈出一個對話框,要求用戶輸入用戶名和密碼

2.使用Fidder監聽請求,可以看到在未進行認證或認證失敗的情況下,服務端會返回401 Unauthorized給客戶端,並附帶Challenge

3.輸入正確的用戶名和密碼后,瀏覽器會生成密碼摘要以及其他信息發送給服務端,服務端認證成功后,返回一些與授權會話相關的附加信息,放在Authorization-Info中。

其中,客戶端選擇的保護質量策略為authresponse就是通過計算得到的密碼摘要,具體計算方式如下(使用默認的MD5加密算法):

MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))

算法 A1
MD5(默認) <username>:<realm>:<password>
MD5-sess MD5(<username>:<realm>:<password>):<nonce>:<cnonce>
qop A2
auth(默認) <request-method>:<uri>
auth-int <request-method>:<uri>:MD5(<request-entity-body>)

另外,rspauth使得客戶端可以對服務器進行認證,稱為響應摘要。響應摘要的計算與請求摘要類似,但由於響應中沒有方法,而且報文實體數據有所不同,所有隻有報文主題信息A2不同。具體區別如下:

qop A2
auth(默認) :<uri>
auth-int :<uri>:MD5(<response-entity-body>)

4.當服務端隨機數過期時,再次請求認證,可以看到質詢中增加了stale=true,用戶無需再次輸入用戶名和密碼,瀏覽器會自動使用新的質詢參數進行密碼摘要的計算。

三、注意事項

1.預授權:服務端預先告知客戶端下一個隨機數是多少,使得客戶端可以直接生成正確的Authorization首部,避免了多次“請求/質詢”。常用的有一下三種方式:

  • 服務器預先在Authorization-Info成功首部中發送下一個隨機數nextnonce。雖然這種機制加快了事務處理的速度,但是它也破壞了對同一台服務器的多次請求進行管道化的功能,可能會造成很大的損失。
  • 服務器允許在一小段時間內使用同一個隨機數。這也就是我們上面剖析中使用的機制,在一定時間內使用同一個隨機數或限制某個隨機數的重用次數,當過期時,聲明stale=true。雖然這確實降低了安全性,但是重用的隨機數的生存周期是可控的,應該在安全和性能之間找到平衡。
  • 客戶端和服務器使用同步的、可預測的隨機數生成算法。

2.RFC 2617建議採用這個假想的隨機數公式:

BASE64(timestamp MD5(timestamp “:” ETag “:” private-key))

其中,timestamp是服務器產生隨機數的時間或其他不重複的值,ETag是與所請求實體有關的HTTP ETag首部的值,private-key是只有服務器知道的私鑰。

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Python 裝飾器

裝飾器

什麼是裝飾器

裝飾器顧名思義就是一個有裝飾功能的工具,那麼裝飾器又是用來裝飾什麼的?為什麼要裝飾這個東西?裝飾的目的是什麼呢?本文會一一作答讓小弟一個一個說

開放封閉原則

談及裝飾器就要引申一個概念,那就是開放封閉原則,那麼問題又來了 什麼是開放封閉……好好好,直接說這個,開放封閉本來是兩個對立的概念,也就是說是一對反義詞,那麼為什麼要提出開放封閉原則呢?原因是在日常的開發工作中,一般最初上線的產品的功能是不盡完善的,就是說不夠完美但已經能夠支撐日常使用,其餘的功能可以日後擴展,舉例:N年前的QQ和現在的QQ(PS:雖然現在本人不怎麼用了)。在後期擴展功能時,因為函數已經是寫好的,而且存在大量調用,所以要直接去給函數增添新的功能顯然不現實,所以我們只能新建函數去給原來的函數擴展功能(其實這就是一個裝飾器啦)

那麼開放封閉原則到底是什麼? 答案是對源碼封閉,對新功能開放

  • 封閉原則:不要改變源代碼

  • 開放原則:能增加一些額外的功能

    如果還有客觀對開放封閉原則似知似解的話,沒關係接着往下看,不影響您食用本文,因為Python裝飾器本身就是對開放封閉原則完美的詮釋

裝飾器初識

不低調的說,裝飾器就是一個函數,名字本來很高大上,但本質就是一個函數,裝飾器函數的功能就是要裝飾一個函數,在不改變被裝飾函數的源代碼及調用方式的前提下,為其增加額外的功能。是不是有點門道了,是不是覺得這玩意也沒啥啦,真是優秀的同學。

代碼show(技術博客不寫代碼干白話說出去丟人)

def warpper(f):  #定義一個函數(裝飾器),傳入的參數是被修飾的函數的函數名

def inner(*args,**kwargs): #嵌套一個內存函數,這個函數主題才是執行被裝飾函數源碼的關鍵

    '''這塊可以加要在被裝飾函數執行之前的操作哈'''

    ret = f(*args,**kwargs) #這裏的形參我會在下面說明

    '''這裏可以寫被裝飾函數之後的,兄嘚,別客氣,想加啥方法加什麼'''

    return ret #這裏的返回值如果我一會兒不忘的話也會在下面說明

return inner

@warpper #這個叫語法糖,嗯……可以吃(可能老外命名的時候就是這麼想的),結構是@加函數名,作用下面會說明
def func():
  print('我就是那個被裝飾的函數')
簡單說明

因為本人比較懶,所以原諒我直接把代碼甩上去了,後面有註釋,看懂了的大佬可以say goodbye啦,想打我的接着聽我白話,那我就把備註再重複一遍,哈哈,你也看到了,裝飾器用到了函數的嵌套(再具體點就是閉包,要問什麼是閉包,百度吧,哈哈),首先外層函數接收到一個函數名,然後返回值是內層函數的函數名;再來內層函數可以接收參數,其中ret = f(*args,**kwargs)有兩個作用,一是執行了傳入的函數,也就是執行了被修飾的函數,二是將返回值賦給了一個變量,此處要說明一下,對被修飾的函數功能的擴展要寫在這裏哦,最後 ret作為內層函數的返回值返回給函數執行者。

語法糖

@warpper這東西和 func = wrapper(func)是一樣的,也就是說最後三行代碼可以這樣寫

#@warpper 
def func():
  print('我就是那個被裝飾的函數')
func = wrapper(func)   #注意奧,這玩意要寫在被裝飾函數的下邊,語法糖才寫上邊

至於為什麼要寫着東西,或者為什麼要用語法糖?請聽下回分解~~~,收起你滴拳頭,是這樣,我剛開始的時候談到了,裝飾器是要在不改變源碼和其調用方式的前提下給其增加新的功能,注意到了么 調用方式 嗯……沒錯就是調用方式,如果我不這樣寫那我是不是要wrapper(func)()這樣去調用啊,是不是有點繞了,但是我把wrapper(func)賦值給了一個和被裝飾函數同名的變量,那我此時要怎麼調用,是不是就是func(),這樣就滿足了開放封閉原則,完美!其實本質就是要把裝飾器“偽裝”成原函數,包括調用方式、參數、返回值,裝就要裝的像一點,對吧。

裝飾帶參數的函數
def wrapper(f):
def inner(*args,**kwargs):
f(*args,**kwargs)
return inner
@wrapper
def func(a,b):
print(a,b)

函數func中有兩個形參a和b,說一下這兩個參數在裝飾器中的旅程,函數inner的萬能參數接收到a和b打包,然後inner函數充當中間商將打包后的元組給了f,在f中打散又成了變量a和b,在裝飾器時就不會影響傳參了,這就是裝飾帶參數的函數

裝飾有返回值的函數

def wrapper(f):
def inner():
ret = f()
return ret
return inner
@wrapper
def func():
return('我不管,我最帥')

哈哈,這段代碼中有我的心聲,你們都懂的,很簡單,裝飾有返回值的參數,在inner函數中將f()賦值給ret,這樣ret就接收到了返回值,再將ret返回給inner(),以此來達到“模擬”被裝飾函數的返回值,也可以說是通過這種方法來拿到被裝飾函數的返回值。

裝飾器帶參數

先舉個例子,當我們需要寫一個簡單的登陸認證功能的時候,我們的目的是要用裝飾器給調用的函數增加一個認證是否登陸過此網站的功能,也就是要驗證用戶名和密碼,但比如不同公司的網站數據庫肯定並非同一個,這個時候就需要帶參數的裝飾器啦,它可以實現我們要用一個裝飾器裝飾多個類似函數的目的

def wrapper_out(n):
   def wrapper(f):
       def inner(*args,**kwargs):
           with open(n,encoding='utf-8') as f1:
           '''此部分省略認證的詳細功能'''
           f(*args,**kwargs)
       return inner
   return wrapper

@wrapper_out('webpage1')
def wangzhi1():
   print('歡迎訪問網址1')
   
@wrapper_out('webpage2')
def wangzhi1():
   print('歡迎訪問網址2')

如果使用標準的裝飾器函數的話只能裝飾其中的一個函數,當裝飾另一個函數時會因為訪問不到正確的數據庫而報錯。

@wrapper_out('webpage1')這段代碼先執行wrapper_out('webpage1')這個函數先把參數webpage傳給n,並且返回一個wrapper,此時@和wrapper結合在一塊有沒有很眼熟,沒錯,這就是我們所熟悉的標準的裝飾器了,餘下的流程就和標準的裝飾器完全相同了。

多個裝飾器裝飾一個函數

這是一種特殊的情況,下面重點分析這種情況的結果是如何產生的,會有點繞。

def wrapper1(func1):
    def inner1():
        print('wrapper1 ,before func')
        func1()
        print('wrapper1 ,after func')
    return inner1

def wrapper2(func2): # func2 == inner1
    def inner2():
        print('wrapper2 ,before func')
        func2() # inner1
        print('wrapper2 ,after func')
    return inner2

@wrapper2  
@wrapper1  
def f():
print('in f') # 3

f()  

結果:

wrapper2,before func
wrapper1,before func
in f
wrapper1,after func
wrapper2,after func

怎麼說?是不是和你預期的結果有所不同,下面來按步驟說一下為什麼會產生這樣的結果(我盡量表達清楚哈)

1. 函數定義不調用不執行直接pass
2.   @wrapper2
@wrapper1  
def f():
單獨看下面這兩行,標準裝飾器哈 @wrapper1 等價於 f = wrapper1(f) = inner1(返回值哈)
3. @wrapper2
  @wrapper1   #注意哈 看步驟2 這裏現在是inner1咯、
  @wrapper2 等價於 inner1 = wrapper2(inner1) = inner2(返回值)
4. 此時的 f = inner2 執行f() 就從inner2()開始執行
5. 執行inner2 首先打印'wrapper2 ,before func'
6. 然後執行func2,由步驟3可知當前wrapper2中的參數是inner1 也就是執行inner1 所以打印wrapper1 ,before func
7. 然後執行func1 參考步驟2 可知這裏的func1()執行的是f()也就是真正的原函數,打印in f
8. 順序執行,打印wrapper1 ,after func
9. inner1執行完(其實就是func2執行完)還是順序執行 打印wrapper2 ,after func
10. 結果出來了、哈哈

這部分本人能力也只能寫成這樣了,各位看官看不懂那一定是小弟沒表述清楚,不過沒關係,在下再支一招,看圖

 

結合結果分析,相信各位老闆也能推導出正確的結果了,嗯、真帥!

好,到此對Python裝飾器應該有那麼一丟丟的認識了哈,我不管,就得有認識。下面的內容和文章關係不大哈

第一篇正經寫的文章,可能文章內容表達不盡如人意的正經哈,但初心是好的,就是分享知識,分享心得嘛,嗯,不管怎麼說,我還是很欣慰的對自己,哈哈,能有人從中有收穫就更perfect嘍

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

每日一問:不一樣的角度吐槽下 DataBinding

我們項目採用的是 kotlin && DataBinding 處理的,可能你會疑問,既然用的是 kotlin,為啥沒有用 kotlinx?新的頁面當然是用的 kotlinx 啦,但我們有相當龐大的歷史代碼,並且我們的通用 adapter 其實也是基於 DataBinding 來封裝的。所以,我們還是不得不來討(吐)論(槽)一下這個 DataBinding 的坑。事實上,這個問題在我當年面試字節跳動的時候就被問及過。

這是一個非常開放性的問題,所以看到這篇文章的小夥伴一定得帶一個有色眼鏡進行審視,下面會盡可能地列舉中筆者遇到過的坑,當然能力有限,有可能不少東西並不是 DataBinding 的問題,需要大家一起來甄別並進行補充。

DataBinding 是早些時候 Google 推出來的一個支持庫,只要勇於解決我們代碼中頻繁出現的 findViewById() 問題,在此之前,我相信大部分人都聽過或者使用過 Butterknife,截止目前,該庫都已經更新到 10.1.0 了,而且作者是 JakeWharton 大神,實為相當好用。

但我們今天不對 Butterknife 進行過多的評論,而轉移到我們的主角 DataBinding。

對於 DataBinding 的使用和介紹這裏就不做講述了,建議大家還是直接到 官方文檔
進行查閱學習,今天在這兒,主要和大家分享討論一下在筆者 1 年多的 DataBinding 中踩過的一些坑。我想這些 tips 肯定不是全部,並且有些其實是個人誤操所致,不管怎麼說,我們今天就捨棄對它的讚美,吐槽吐槽這個 DataBinding。

極難進行錯誤定位

oh,這應該是使用 DataBinding 的小夥伴們最最最大的坑點了,你甚至一定在各種技術輪胎和開發交流群中見過一些小夥伴對它的深惡痛絕。不管你代碼中有了什麼錯誤,你一定收到的錯誤日誌是一大堆 DataBinding 生成的類找不到。

可能非常多的小夥伴會對此保持疑問,這不,真正的錯誤都在 build 日誌的最後能看到么?實在沒有看到,你也可以在控制台輸入 ./gradlew build --stacktrace 查看到詳細日誌。

但事實真的如此么?有沒有小夥伴經歷過不管你怎麼查看日誌都找不到真正的錯誤代碼的。

我想一定有!雖然我們一般都要求對 commit 保持「少量多次」的原則,但這並不能保證我們時刻都得嚴格遵守,在部分業務非常紊亂的時候,我們並不希望經常做 commit,因為每一次 commit,我們也得做一次編譯確認當前的代碼沒有任何語法問題。而對於代碼量龐大、尤其是還受各種歷史原因組件化不夠徹底的項目,一次編譯基本就可以讓你去吃一個飯了。我司的項目就是如此,受限於歷史原因,組件化做的並不徹底,導致本地編譯在沒有命中緩存的情況下至少得 10 分鐘,這還是在 16G 的 mac pro 上。而且大多數人的電腦可能配置還更低,在編譯的時候很難做其他和工作相關的事情。

為了處理這個編譯問題,我司不得不編寫一個編譯腳本,把編譯操作都在服務器上進行處理,在做了一系列優化操作后,在沒命中緩存的情況下編譯 debug 包還是需要 4.5 分鐘,當然現在在編譯中我們可以做其他有意思的事情了。

好像前面這兩段說了很多無關緊要的東西,但是!我真正想要表達的是,這時候出現了錯誤,並且日誌無法對錯誤進行定位,你會發現非常痛苦,你可能已經改動了數十個文件,新建了不少 XML,因為無法定位到日誌,你不得不一行一行的進行語法檢查。

我在這個問題上體驗過編碼兩小時,查編譯失敗問題花費時間更多的尷尬情況,通常來說,這樣的錯誤都不容易發現,我深深的記住我有一次因為組件化歷史問題,不得不把一個組件代碼 move 到另外一個組件,然後就發生了不可預料的錯誤的悲痛場景。當你 stash 改動后就可以編譯,但 pop 出來就錯誤的時候,你才會知道什麼是手段極其殘忍。

OK,目前對於這樣的情況,還是有所總結,這種情況 95% 是 XML 代碼的問題,你可以直接檢查 XML 了。

代碼極難維護

我們使用 DataBinding 的時候,必然會喜歡它的雙向綁定操作。在 XML 裏面直接做一些簡單的邏輯處理,這樣的操作讓我們的代碼變得非常簡潔,並且可以省去 findViewById() 帶來的不必要的性能損耗。但這樣的操作讓後續維護功能代碼的人非常痛苦。

我們的代碼裏面就有相當部分的這樣的代碼,部分頁面邏輯非常複雜,比如一個商品詳情頁,會牽扯到非常大量的信息展示和各種促銷秒殺狀態,還有不少的動效,這時候不少邏輯放在 XML 裏面后,後續維護的同事在處理產品改動的時候,一定在心中暗自謾罵。

通常來說,我習慣於把這些複雜的邏輯放置在我們的 Java 代碼中。就目前來看,在代碼中查看這些複雜的交互邏輯得心應手不少。

@{} 不會去做檢查

早些時候,我連續犯過兩次低級錯誤,所以對這個問題記憶深刻。我們可以發現,XML 裏面支持我們用類似 @{} 這樣的方式去為 TextView 設置显示內容,但在 XML 裏面我們並沒有檢測機制,所以極易出現原本你這個是一個 Number 類型的值,編譯器卻當做一個 resourceId 進行處理而報錯。實際上,在代碼裏面設置編譯器是會直接報錯的。

根據控件 ID 在代碼裏面找一個東西很難

在我們項目的早期代碼中,XML 裏面的命名規範基本是 xxx_xxx_xxx 這樣的格式,但在 DataBinding 裏面為我們生成的變量卻採用的是駝峰命名法,這導致我們根據一個控件 id 去對應 class 裏面尋找的時候,還得自己更改為駝峰命名法命名的名字,這一度讓我們感到非常不適,所以我們後面的代碼 XML 命名規範就跟着變成駝峰命名法了。這可能和命名規範有些許出入,不過我們堅信適合自己的,才是最好的理念。

部分 XML 中的表達式在不同 gradle 版本上表現有所不同

前面說到,我們平時會採用服務器編譯,所以此前有出現過 XML 文件裏面的某個屬性設置在本地編譯不過,但在服務器上甚至其他同事的電腦上可以編譯的問題。老實說,目前我並沒有找到真正的原因,我姑且把這個問題甩鍋給了此前做這個的同事。我後續更改了他的實現方式才讓這個問題得到妥善處理,但至今沒有明白問題出在哪裡,因為那樣的方式我認為本身代碼是沒有什麼問題的。可惜我現在沒有時間去尋找這個代碼,大概是設置一個公用的 onClickListener 的問題。

BindingAdapter 不好維護

我們通常會用到 @BindingAdapter 方式來做一些公用邏輯,而不是直接去把邏輯放在頁面通過設置屬性來使用它,這樣就會出現這些公用邏輯比較難維護,當然,這極有可能是我們項目的歷史問題,但我覺得這算是一個坑點了。不知道有沒有人出現這樣的屬性在 XML 裏面沒有提示的情況。就像你自定義 View Styleable 名字不唯一一樣。

多模塊依賴問題

這個問題我之前還沒有發現,因為我們每個模塊都用到了 DataBinding,所以認為每個模塊的 gradle 都設置上 DataBinding 的配置,並不算什麼令人可以吐槽的事,但看起來這個問題挺嚴重的,所以也在這分享給大家。轉自 wanAndroid 上面 xujiafeng 的回答}

DataBinding在多模塊開發的時候,有這樣一個機制:
如果子模塊使用了 DataBinding,那麼主模塊也必須在 gradle 加上配置,不然就會報錯;
如果主模塊和子模塊都添加上了 DataBinding 的配置,那麼在編譯時,子模塊的 XML 文件產生的 Binding 類除了在自己的 build 里會有一份外,在主模塊下也會有一份。
那麼,如果主模塊與子模塊都有一個 layout 根目錄的 activity_main.xml,主模塊生成的 ActivityMainBinding 會是根據子模塊的文件生成的!這種情況我們還可以通過讓主模塊和子模塊使用不同的命名,那麼下面這個問題就更要命了:

如果子模塊的某個 XML 文件使用了一些第三方的控件,那麼主模塊由於也會生成這個文件的 Binding 類,並且其會有第三方控件的引用,這時候由於主模塊沒有引入這些控件,就會報錯,解決辦法是在子模塊應用第三方控件的時候,使用 API 的方式應用,這樣主模塊就堅決引用到了這些第三方控件,這是這樣違背了解耦的原則。

哎,個人能力有限,就想到哪兒說到哪兒了。可能有不少其實並不是 databinding 的坑,是個人使用問題,還往明白的人能直接指出。PS:再給我一個機會,我不想在用 Databinding。

希望有其他槽點或者認為上面的東西是有更好的處理方式的小夥伴一定要在下面留言,盼復盼復~

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

ElasticSearch簡介

目錄

  • 1. 定義
  • 2. 與 Lucene 的關係
  • 3. 優點
  • 4. 缺點
  • 5. 解決的問題
  • 6. 應用場景
  • 7. 倒排索引(摘自Elasticsearch權威指南)

1. 定義

Elasticsearch 是一個高度可擴展的開源全文搜索和分析引擎。它允許您快速,近實時地存儲,搜索和分析大量數據。它通常用作底層引擎、技術,為具有複雜搜索功能和要求的應用程序提供支持。

Elasticsearch 也使用 Java 開發並使用 Lucene 作為其核心來實現所有索引和搜索的功能,但是它的目的是通過簡單的 RESTful API 來隱藏 Lucene 的複雜性,從而讓全文搜索變得簡單。

ES 是基於Lucene這個非常成熟的索引方案,另加上一些分佈式的實現:集群,分片,複製等。

2. 與 Lucene 的關係

Lucene 是一套用於全文檢索和搜尋的開源程式庫,由 Apache 軟件基金會支持和提供。Lucene 提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。但是 Lucene 操作複雜,一般不直接利用 Lucene 作為搜索引擎,ElasticSearch 就是利用 Java 簡化了 Lucene 的使用。

3. 優點

  1. 具備橫向可擴展性:只需要增加一台服務器,做些配置,啟動 ES 進程就可以快速併入集群。’
  2. 分片機制:同一個索引分成多個分片(sharding),類似於 redis 中的分片,採取分而治之的思想來更好地解決問題。
  3. 高可用:提供複製機制,一個分片可以設置多個複製,使得某台服務器宕機的話,集群依舊可以正常運行,並會把丟失的複製恢復到其它可用節點上’

4. 缺點

  1. 節點數據的一致性問題:其默認的機制是通過多播機制,同步元數據信息,但是在比較繁忙的集群中,可能會由於網絡的阻塞,或者節點處理能力達到飽和導致各節點元數據不一致——也就是所謂的腦裂問題,這樣會使集群處於不一致狀態。目前並沒有一個徹底的解決方案來解決這個問題,但是可以通過將工作節點與元數據節點分開的部署方案來緩解這種情況。
  2. 沒有細粒度的權限管理,沒有像MySQL那樣的分各種用戶,每個用戶又有不同的權限。

5. 解決的問題

  • 更快的在大量數據中檢索相關數據,性能遠優於傳統數據庫
  • 結合分詞器,根據關鍵詞返回統計結果

6. 應用場景

  • 全文檢索:例如淘寶 app 搜索 17寸電腦關鍵詞,搜索系統將依據關鍵詞分詞查詢,按照指定的匹配度返回對應的商品。這是 ES 最核心也是最常用的功能。
  • 記錄和日誌分析:圍繞Elasticsearch構建的生態系統使其成為最容易實施和擴展日誌記錄解決方案之一。結合Logstash,ElasticSearch 和Kibana 三個組件,可以搭建一套高效的日誌收集和分析系統,也就是我們常見的ELK系統。
  • 數據可視化:Kibana 是一款功能強大且易於使用的可視化工具,可以結合 ES 對大量數據提供圖表選項、地理數據等可視化組件。

7. 倒排索引(摘自Elasticsearch權威指南)

Elasticsearch 是通過 Lucene 的倒排索引技術實現比關係型數據庫更快的過濾。特別是它對多條件的過濾支持非常好。

Elasticsearch 使用一種稱為 倒排索引 的結構,它適用於快速的全文搜索。一個倒排索引由文檔中所有不重複詞的列表構成,對於其中每個詞,有一個包含它的文檔列表。

例如,假設我們有兩個文檔,每個文檔的 content 域包含如下內容:

  1. The quick brown fox jumped over the lazy dog
  2. Quick brown foxes leap over lazy dogs in summer

為了創建倒排索引,我們首先將每個文檔的 content 域拆分成單獨的 詞(我們稱它為 詞條tokens),創建一個包含所有不重複詞條的排序列表,然後列出每個詞條出現在哪個文檔。結果如下所示:

Term      Doc_1  Doc_2
-------------------------
Quick   |       |  X
The     |   X   |
brown   |   X   |  X
dog     |   X   |
dogs    |       |  X
fox     |   X   |
foxes   |       |  X
in      |       |  X
jumped  |   X   |
lazy    |   X   |  X
leap    |       |  X
over    |   X   |  X
quick   |   X   |
summer  |       |  X
the     |   X   |
------------------------

現在,如果我們想搜索 quick brown ,我們只需要查找包含每個詞條的文檔:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
quick   |   X   |
------------------------
Total   |   2   |  1

兩個文檔都匹配,但是第一個文檔比第二個匹配度更高。如果我們使用僅計算匹配詞條數量的簡單 相似性算法 ,那麼,我們可以說,對於我們查詢的相關性來講,第一個文檔比第二個文檔更佳。

但是,我們目前的倒排索引有一些問題:

  • Quickquick 以獨立的詞條出現,然而用戶可能認為它們是相同的詞。
  • foxfoxes 非常相似, 就像 dogdogs ;他們有相同的詞根。
  • jumpedleap, 儘管沒有相同的詞根,但他們的意思很相近。他們是同義詞。

使用前面的索引搜索 +Quick +fox 不會得到任何匹配文檔。(記住,+ 前綴表明這個詞必須存在。)只有同時出現 Quickfox 的文檔才滿足這個查詢條件,但是第一個文檔包含 quick fox ,第二個文檔包含 Quick foxes

我們的用戶可以合理的期望兩個文檔與查詢匹配。我們可以做的更好。

如果我們將詞條規範為標準模式,那麼我們可以找到與用戶搜索的詞條不完全一致,但具有足夠相關性的文檔。例如:

  • Quick 可以小寫化為 quick
  • foxes 可以 詞幹提取 –變為詞根的格式– 為 fox 。類似的, dogs 可以為提取為 dog
  • jumpedleap 是同義詞,可以索引為相同的單詞 jump

現在索引看上去像這樣:

Term      Doc_1  Doc_2
-------------------------
brown   |   X   |  X
dog     |   X   |  X
fox     |   X   |  X
in      |       |  X
jump    |   X   |  X
lazy    |   X   |  X
over    |   X   |  X
quick   |   X   |  X
summer  |       |  X
the     |   X   |  X
------------------------

這還遠遠不夠。我們搜索 +Quick +fox 仍然 會失敗,因為在我們的索引中,已經沒有 Quick 了。但是,如果我們對搜索的字符串使用與 content 域相同的標準化規則,會變成查詢 +quick +fox ,這樣兩個文檔都會匹配!

github地址

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

Java NIO學習系列一:Buffer

  前面三篇文章中分別總結了標準Java IO系統中的File、RandomAccessFile、I/O流系統,對於I/O系統從其繼承體系入手,力求對類數量繁多的的I/O系統有一個清晰的認識,然後結合一些I/O的常規用法來加深對標準I/O系統的掌握,感興趣的同學可以看一下:

  <<Java I/O系統學習系列一:File和RandomAccessFile>>

  <<Java I/O系統學習系列二:輸入和輸出>>

  <<Java I/O系統學習系列三:I/O流的典型使用方式>>

  從本文開始我會開始總結NIO部分,Java NIO(注意,這裏的NIO其實叫New IO)是用來替換標準Java IO以及Java 網絡API的,其提供了一系列不同與標準IO API的方式來處理IO,從JDK1.4開始引入,其目的在於提高速度。

  之所以能夠提高速度是因為其所使用的結構更接近於操作系統執行I/O的方式:通道和緩衝器。我們可以把它想象成一個煤礦,通道是一個包含煤層(數據)的礦藏,而緩衝器則是派送到礦藏的卡車。卡車滿載煤炭而歸,我們再從卡車上獲得煤炭。也就是說,我們並沒有直接和通道交互,而是和緩衝器交互,並把緩衝器派送到通道。通道要麼從緩衝器獲得數據,要麼向緩衝器發送數據。

   在標準IO的API中,使用字節流和字符流。而在Java NIO中是使用Channel(通道)和Buffer(緩衝區),數據從channel中讀取到buffer中,或從buffer寫入到channel中。Java NIO類庫中的核心組件為:

  • Buffer
  • Channel
  • Selector

  本文中我們會着重總結Buffer相關的知識點(後面的文章中會繼續介紹Channel即Selector),本文主要會圍繞如下幾個方面展開:

  Buffer簡介

  Buffer的內部結構  

  Buffer的主要API

  ByteBuffer

  Buffer類型

  總結

 

1. Buffer簡介

  Java NIO中的Buffer一般和Channel配對使用。可以從Channel中讀取數據到Buffer,或者寫數據到Channel中。一個Buffer其實就是代表一個內存塊,你可以往裡面寫數據或者從中讀取數據。這個內存塊被包裝成一個Buffer對象,並且提供了一系列方法使得操作內存塊更便捷。

  通過Buffer來讀寫數據通常包括如下4步:

  1. 寫數據到Buffer中;
  2. 調用buffer.flip();
  3. 從Buffer讀取數據;
  4. 調用buffer.clear()或buffer.compact();

  當往Buffer中寫數據時,Buffer能夠記錄寫了多少數據。當要從Buffer中讀取數據時,就需要通過調用flip()方法將Buffer從寫模式切換到讀模式。一旦讀完所有數據,需要清空Buffer,讓它再次處於寫狀態。可以通過調用clear()或compact()方法來完成這一步:

  • clear()方法會清空整個Buffer;
  • compact()方法僅僅清空你已經從Buffer中讀取的數據,未讀數據會被移動到Buffer起始位置,可以緊接着未讀的數據寫入新的數據;

  如下是一個簡單的使用例子,通過FileChannel和ByteBuffer讀取pom.xml文件,並逐字節輸出:

public class BufferDemo {

    public static void main(String[] args) {
        try {
            RandomAccessFile raf = new RandomAccessFile("pom.xml","r");
            FileChannel channel = raf.getChannel();
            ByteBuffer buffer = ByteBuffer.allocate(48);
            int byteReaded = channel.read(buffer);
            while(byteReaded != -1) {
                buffer.flip();
                while(buffer.hasRemaining()) {
                    System.out.print((char)buffer.get());
                }
                buffer.clear();
                byteReaded = channel.read(buffer);
            }
            raf.close();
        }catch (Exception e) {
            e.printStackTrace();
        }
    }    
}

 

2. Buffer的內部結構

  上面說到Buffer封裝了一塊內存塊,並提供了一系列的方法使得可以方便地操縱內存中的數據。至於如何操縱?Buffer提供了4個索引。要理解Buffer的工作原理,就需要從這些索引說起:

  • capacity(容量);
  • position(位置);
  • limit(界限);
  • mark(標記);

   其中position和limit的含義取決於Buffer是處於什麼模式(讀或者寫模式),capacity的含義則和模式無關,而mark則只是一個標記,可以通過mark()方法進行設置。下圖描述了讀寫模式下三種屬性分別代表的含義,詳細解釋見下文:

2.1 Capacity

  Buffer代表一個內存塊,所以其是有確定大小的,也叫“容量”。可以往buffer中寫入各種數據如byte、long、chars等,當Buffer被寫滿了則需要將其清空(可以通過讀取數據或者清空數據)之後才能繼續寫入數據。

2.2 Position

  當往Buffer中寫數據時,寫入的地方就是所謂的position,其初始值為0,最大值為capacity-1。當往Buffer中寫入一個byte或者long的數據時,position會前移以指向下一個即將被插入的位置。

  當從Buffer中讀取數據時,讀取數據的地方就是所謂的position。當執行flip將Buffer從寫模式切換到讀模式時,position會被重置為0。隨着不斷從Buffer讀取數據,position也會不斷後移指向下一個將被讀取的數據。

2.3 Limit

  在寫模式下,Buffer的limit是指能夠往Buffer中寫入多少數據,其值等於Buffer的capacity。

  在讀模式下,Buffer的limit是指能夠從Buffer讀取多少數據出來。因此當從寫模式切換到讀模式下時,limit就被設置為寫模式下的position的值(這很好理解,寫了多少才能讀到多少)。

 2.4 Mark

  mark其實就是一個標記,可以通過mark()方法設置,設置值為當前的position。

 

  下面是用於設置和複位索引以及查詢它們值的方法:

 

  capacity()      返回緩衝區容量
  clear()      清空緩衝區,將position設置為0,limit設置為容量。我們可以調用此方法覆寫緩衝區
  flip()       將limit設置為position,position設置為0。此方法用於準備從緩衝區讀取已經寫入的數據
  limit()        返回limit值
  limit(int lim)    設置limit值
  mark()       將mark設置為position
  position()     返回position值
  position(int pos)  設置position值
  remaining()    返回(limit – position)
  hasRemaining()  若有介於position和limit之間的元素,則返回true

 

3. Buffer的主要API

  除了如上和索引相關的方法之外,Buffer還提供了一些其他的方法用於寫入、讀取等操作。

3.1 給Buffer分配空間

  要獲得一個Buffer對象就可以通過Buffer類的allocate()方法來實現,如下分別是分配一個48字節的ByteBuffer和1024字符的CharBuffer:

ByteBuffer buf = ByteBuffer.allocate(48);
CharBuffer buf = CharBuffer.allocate(1024);

3.2 往Buffer中寫數據

  有兩種方式往Buffer中寫入數據:

  • 從Channel中往Buffer寫數據;
  • 通過Buffer的put()方法寫入數據;
int bytesRead = inChannel.read(buf); // read into buffer
buf.put(127);

  put()方法有多個重載版本,比如從指定位置寫入數據,或寫入字節數組等。

3.3 flip()

  flip()方法將Buffer從寫模式切換到讀模式。調用flip()方法會將position設為0,limit設為position之前的值。

3.4 從Buffer讀數據

  也有兩種方法從Buffer讀取數據:

  • 從Buffer中讀數據到Channel中;
  • 調用Buffer的get()方法讀取數據;
int bytesWritten = inChannel.write(buf); // read from buffer into channel
byte aByte = buf.get();

3.5 rewind()

  rewind()方法將position設置為0,可以從頭開始讀數據。

3.6 clear()和compact()

  當從Buffer讀取數據結束之後要將其切換回寫模式,可以調用clear()、compact()這兩個方法,兩者之間的區別如下:

  調用clear(),會將position設為0,limit設為capacity,也就是說Buffer被清空了,但是裏面的數據仍然存在,只是這時沒有標記可以告訴你哪些數據是已讀,哪些是未讀。

  如果讀取到一半需要寫入數據,但是未讀的數據稍後還需要讀取,這時可以使用compact(),其會將所有未讀取的數據複製到Buffer的前面,將position設置到這些數據後面,limit設置為capacity,所以此時是從未讀的數據後面開始寫入新的數據。

3.7 mark()和reset()

  調用mark()方法可以標誌一個指定的位置(即設置mark值),之後調用reset()方法時position又會回到之前標記的位置。

 

4. ByteBuffer

   ByteBuffer是一個比較基礎的緩衝器,繼承自Buffer,是可以存儲未加工字節的緩衝器,並且也是唯一直接與通道交互的緩衝器。可以通過ByteBuffer的allocate()方法來分配一個固定大小的ByteBuffer,並且其還有一個方法選擇集,用於以原始的字節形式或基本類型輸出和讀取數據。但是,沒辦法輸出或讀取對象,即使是字符串對象也不行。這種處理雖然很低級,但卻正好,因為這是大多數操作系統中更有效的映射方式。

  ByteBuffer也分為直接和非直接緩衝器,通過allocate()創建的就是非直接緩衝器,而通過allocateDirect()方法就可以創建出一個緩衝器直接緩衝器,這是一個與操作系統有更高耦合性的緩衝器,也就意味着它能夠帶來更高的速度,但是分配的開支也會更大。

  儘管ByteBuffer只能保存字節類型的數據,但是它具有可以從其所容納的字節中產生出各種不同基本類型值的方法。下面的例子展示怎樣使用這些方法來插入和抽取各種數值:

public class GetData {    
    private static final int BSIZE = 1024;
    public static void main(String[] args){
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        int i = 0;
        while(i++ < bb.limit())
            if(bb.get() != 0)
                System.out.println("nonzero");
        System.out.println("i = " + i);
        bb.rewind();
        // store and read a char array:
        bb.asCharBuffer().put("Howdy!");
        char c;
        while((c = bb.getChar()) != 0)
            System.out.print(c + " ");
        System.out.println();
        bb.rewind();
        // store and read a short:
        bb.asShortBuffer().put((short)471142);
        System.out.println(bb.getShort());
        bb.rewind();
        // sotre and read an int:
        bb.asIntBuffer().put(99471142);
        System.out.println(bb.getInt());
        bb.rewind();
        // store and read a long:
        bb.asLongBuffer().put(99471142);
        System.out.println(bb.getLong());
        bb.rewind();
        // store and read a float:
        bb.asFloatBuffer().put(99471142);
        System.out.println(bb.getFloat());
        bb.rewind();
        // store and read a double:
        bb.asDoubleBuffer().put(99471142);
        System.out.println(bb.getDouble());
        bb.rewind();
    }
}

 

5. Buffer類型

  Java NIO中包含了如下幾種Buffer:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

  這些Buffer類型代表着不同的數據類型,使得可以通過Buffer直接操作如char、short等類型的數據而不是字節數據。其中MappedByteBuffer略有不同,後面會專門總結。

  通過ByteBuffer我們只能往Buffer直接寫入或者讀取字節數組,但是通過對應類型的Buffer比如CharBuffer、DoubleBuffer等我們可以直接往Buffer寫入char、double等類型的數據。或者利用ByteBuffer的asCharBuffer()、asShorBuffer()等方法獲取其視圖,然後再使用其put()方法即可直接寫入基本數據類型,就像上面的例子。

  這就是視圖緩衝器(view buffer)可以讓我們通過某個特定的基本數據類型的視窗查看其底層的ByteBuffer。ByteBuffer依然是實際存儲數據的地方,“支持”着前面的視圖,因此對視圖的任何修改都會映射成為對ByteBuffer中數據的修改。這使得我們可以很方便地向ByteBuffer插入數據。視圖還允許我們從ByteBuffer一次一個地(與ByteBuffer所支持的方式相同)或者成批地(通過放入數組中)讀取基本類型值。在下面的例子中,通過IntBuffer操縱ByteBuffer中的int型數據:

public class IntBufferDemo {    
    private static final int BSIZE = 1024;
    public static void main(String[] args){
        ByteBuffer bb = ByteBuffer.allocate(BSIZE);
        IntBuffer ib = bb.asIntBuffer();
        // store an array of int:
        ib.put(new int[]{11,42,47,99,143,811,1016});
        // absolute location read and write:
        System.out.println(ib.get(3));
        ib.put(3,1811);
        // setting a new limit before rewinding the buffer.
        ib.flip();
        while(ib.hasRemaining()){
            int i = ib.get();
            System.out.println(i);
        }
    }
}

  上例中先用重載后的put()方法存儲一個整數數組。接着get()和put()方法調用直接訪問底層ByteBuffer中的某個整數位置。這些通過直接與ByteBuffer對話訪問絕對位置的方式也同樣適用於基本類型。

 

6. 總結

  本文簡單總結了Java NIO(Java New IO),其目的在於提高速度。Java NIO類庫中主要包括Buffer、Channel、Selector,本文主要總結了Buffer相關的知識點:

  • Buffer叫緩衝器,她是和Channel(通道)交互的,可以從channel中讀數據到buffer中,或者從buffer往channel中寫數據;
  • Buffer內部封裝了一塊內存,提供了一系列API使得可以方便地操作內存中的數據。其內部是通過capacity、position、limit、mark等變量來跟蹤標記封裝的數據的;
  • ByteBuffer是最基本的Buffer,是唯一可以直接與通道交互的緩衝器,其可以直接操縱字節數據或字節數組;
  • 除了ByteBuffer之外,Buffer還有許多別的類型如:MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer;
  • 雖然只有ByteBuffer能夠直接和通道交互,但是可以從ByteBuffer獲取多種不同的視圖緩衝器,進而同時具備了直接操作基本數據類型和與通道交互的能力;

  基礎知識的總結也許是比較枯燥的,但是如果你已經看到這裏說明你很有耐心,如果覺得對你有幫助的話,不妨點個贊關注一下吧^_^

 

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益

【nodejs原理&源碼雜記(8)】Timer模塊與基於二叉堆的定時器

目錄

  • 一.概述
  • 二. 數據結構
    • 2.1 鏈表
    • 2.2 二叉堆
  • 三. 從setTimeout理解Timer模塊源碼
    • 3.1 timers.js中的定義
    • 3.2 Timeout類定義
    • 3.3 active(timeout)
    • 3.4 定時器的處理執行邏輯
    • 3.5 實例分析
  • 四. 小結

示例代碼託管在:http://www.github.com/dashnowords/blogs

博客園地址:《大史住在大前端》原創博文目錄

華為雲社區地址:【你要的前端打怪升級指南】

一.概述

Timer模塊相關的邏輯較為複雜,不僅包含JavaScript層的實現,也包括C++編寫的與底層libuv協作的代碼,想要完整地看明白是比較困難的,本章僅以setTimeout這個API的實現機製為主線,講述源碼中的JavaScript相關的實現部分,這部分只需要一些數據結構的基本知識就可以理解。

二. 數據結構

setTimeout這個API的實現基於兩類基本數據結構,我們先來複習一下相關的特點。對數據結構知識比較陌生的小夥伴可以參考【野生前端的數據結構基礎練習】系列博文自行學習,所有的章節都有示例代碼。

2.1 鏈表

鏈表是一種物理存儲單元上非連續的存儲結構,存儲元素的邏輯順序是由鏈表中的指針鏈接次序來決定的。每一個節點包含一個存放數據的數據域和存放下一個節點的指針域(雙向鏈表中指針數量為2)。鏈表在插入元素時的時間複雜度為O(1)(因為隻影響插入點前後的節點,無論鏈表有多大),但是由於空間不連續的特點,訪問一個未排序鏈表的指定節點時就需要逐個對比,時間複雜度為O(n),比數組結構就要慢一些。鏈表結構也可以根據指針特點分為單向鏈表,雙向鏈表循環鏈表Timer模塊中使用的鏈表結構就是雙向循環鏈表,Node.js中源碼的底層數據結構實現都是獨立的,鏈表的源碼放在lib/internal/linkedlist.js:

'use strict';

function init(list) {
  list._idleNext = list;
  list._idlePrev = list;
}

// Show the most idle item.
function peek(list) {
  if (list._idlePrev === list) return null;
  return list._idlePrev;
}

// Remove an item from its list.
function remove(item) {
  if (item._idleNext) {
    item._idleNext._idlePrev = item._idlePrev;
  }

  if (item._idlePrev) {
    item._idlePrev._idleNext = item._idleNext;
  }

  item._idleNext = null;
  item._idlePrev = null;
}

// Remove an item from its list and place at the end.
function append(list, item) {
  if (item._idleNext || item._idlePrev) {
    remove(item);
  }

  // Items are linked  with _idleNext -> (older) and _idlePrev -> (newer).
  // Note: This linkage (next being older) may seem counter-intuitive at first.
  item._idleNext = list._idleNext; //1
  item._idlePrev = list;//2

  // The list _idleNext points to tail (newest) and _idlePrev to head (oldest).
  list._idleNext._idlePrev = item;//3
  list._idleNext = item;//4
}

function isEmpty(list) {
  return list._idleNext === list;
}

鏈表實例初始化了兩個指針,初始時均指向自己,_idlePrev指針將指向鏈表中最新添加進來的元素,_idleNext指向最新添加進來的元素,實現的兩個主要操作為removeappend。鏈表的remove操作非常簡單,只需要將刪除項前後的元素指針加以調整,然後將被刪除項的指針置空即可,就像從一串鎖鏈中拿掉一節,很形象。

源碼中的idlePrevidleNext很容易混淆,建議不用強行翻譯為“前後”或者“新舊”,(反覆記憶N次都記不住我也很無奈),直接按對應位置來記憶就可以了,愛翻譯成什麼就翻譯成什麼。

源碼中的鏈表實現並沒有提供指定位置插入的方法,append( )方法默認只接收listitem兩個參數,新元素會被默認插入在鏈表的固定位置,這與它的使用方式有關,所以沒必要實現完整的鏈表數據結構。append稍微複雜一些,但是源碼中也做了非常詳細的註釋。首先需要確保插入的元素是獨立的(也就是prevnext指針都為null),然後再開始調整,源碼中的鏈表是一個雙向循環鏈表,我們調整一下源碼的順序會更容易理解,其實插入一個元素就是要將各個元素的prevnext兩個指針調整到位就可以了。先來看_idlePrev指針鏈的調整, 也就是指針調整代碼中標記為2和3的語句:

item._idlePrev = list;//2
list._idleNext._idlePrev = item;//3

這裏可以把list看作是一個prev指針連接起來的單向鏈表,相當於將新元素item按照prev指針的指向添加到list和原本的list._idleNext指向的元素中間,而1和4語句是調整了反方向的next指針鏈:

item._idleNext = list._idleNext; //1
list._idleNext = item;//4

調整后的鏈表以next指針為依據就可以形成反方向的循環鏈表,然後只需要記住list._idleNext指針指向的是最新添加的項就可以了。

如上圖所示,nextprev分別可以作為鏈表的邏輯順序形成循環鏈。

2.2 二叉堆

源碼放在lib/internal/priority_queue.js中,一些博文也直接翻譯為優先隊列,它們是抽象結構和具體實現之間的關係,特性是一致的。二叉堆是一棵有序的完全二叉樹,又以節點與其後裔節點的關係分為最大堆最小堆完全二叉樹的特點使其可以很容易地轉化為一維數組來存儲,且不需要二外記錄其父子關係,索引為i的節點的左右子節點對應的索引為2i+12i+2(當然左右子節點也可能只有一個或都不存在)。Node.js就使用一維數組來模擬最小堆。源碼基本上就是這一數據結構和“插入”,“刪除”這些基本操作的實現。

結構的使用最主要的是為了獲得堆頂的元素,因為它總是所有數據里最大或最小的,同時結構是一個動態調整的數據結構,插入操作時會將新節點插入到堆底,然後逐層檢測和父節點值的相對大小而“上浮”直到整個結構重新變為;進行移除操作(移除堆頂元素也是移除操作的一種)時,需要將堆尾元素置換到移除的位置,以維持整個數據結構依然是一棵完全二叉樹,然後通過與父節點和子節點進行比較來決定該位置的元素應該“上浮”或“下沉”,並遞歸這個過程直到整個數據結構被重建為。相關的文章非常,本文不再贅述(可以參考這篇博文【二叉堆的添加和刪除元素方法】,有動畫好理解)。

三. 從setTimeout理解Timer模塊源碼

timer模塊並不需要手動引入,它的源碼在/lib/timers.js目錄中,我們以這樣一段代碼來看看setTimeout方法的執行機制:

setTimeout(()=>{console.log(1)},1000);
setTimeout(()=>{console.log(2)},500);
setTimeout(()=>{console.log(3)},1000);

3.1 timers.js中的定義

最上層方法的定義進行了一些參數格式化,將除了回調函數和延遲時間以外的其他參數組成數組(應該是用apply來執行callback方法時把這些參數傳進去),接着做了三件事,生成timeout實例,激活實例,返回實例。

3.2 Timeout類定義

Timeout類定義在【lib/internal/timers.js】中:

初始化了一些屬性,可以看到傳入構造函數的callback,after,args都被記錄下來,可以看到after的最小值為1msTimeout還定義了一些原型方法可以先不用管,然後調用了initAsyncResource( )這個方法,它在實例上添加了[async_id_symbol][trigger_async_id_symbol]兩個標記后,又調用了emitInit( )方法將這些參數均傳了進去,這個emitInit( )方法來自於/lib/internal/async_hooks.js,官方文檔對async_hook模塊的解釋是:

The async_hooks module provides an API to register callbacks tracking the lifetime of asynchronous resources created inside a Node.js application.

它是一個實驗性質的API,是為了Node.js內部創建的用於追蹤異步資源生命周期的模塊,所以推測這部分邏輯和執行機制關係不大,可以先擱在一邊。

3.3 active(timeout)

獲得了timeout實例后再回到上層函數來,接下來執行的是active(timeout)這個方法,它調用的是insert( item, true, getLibuvNow()),不難猜測最後這個方法就是從底層libuv中獲取一個準確的當前時間,insert方法的源碼如下:

首先為timeout實例添加了開始執行時間idleStart屬性,接下來的邏輯涉及到兩個對象,這裏提前說明一下:timerListMap是一個哈希表,延時的毫秒數為key,其value是一個雙向鏈表,鏈表中存放着timeout實例,所以timerListMap就相當於一個按延時時間來分組存放定時器實例的Hash+linkedList結構,另一個重要對象timerListQueue就是上面講過的優先隊列(後文使用“二叉堆”這一概念)。

這裡有一個小細節,就是將新的定時器鏈表加入二叉堆時,比較函數是自定義傳入的,在源碼中很容易看到compareTimersLists ( )這個方法使用鏈表的expiry屬性的值進行比較來得到最小堆,由此可以知道,堆頂的鏈表總是expiry最小的,也就是說堆頂鏈表的__idlePrev指向的定時器,就是所有定時器里下一個需要觸發回調的。

接下來再來看看active( )函數體的具體邏輯,如果有對應鍵的鏈表則獲取到它(list變量),如果沒有則生成一個新的空鏈表,然後將這個鏈表添加進二叉堆,跳過中間的步驟,在最後可以看到執行了:

L.append(list, item);

這個L實際上是來自於前文提過的linkedList.js中的方法,就是將timeout實例添加到list鏈表中,來個圖就很容易理解了:

中間我們跳過了一點邏輯,就是在新鏈表生成時執行的:

if(nextExpiry > expiry){
    scheduleTimer(msecs);
    nextExpiry = expiry;
}

nextExpirytimer模塊中維護的一個模塊內的相對全局變量,這裏的expiry是新鏈表的下一個定時器的過期時間(也就是新鏈表中唯一一個timeout實例的過期時間),這裏針對的情況就是新生成的定時器比已存在的所有定時器都要更早觸發,這時就需要重新調度一下,並把當前這個定時器的過期時間點設置為nextExpiry時間。

這個scheduleTimer( )使用internalBinding('timers')引入的,在lib/timer.cc中找到這個方法:

void ScheduleTimer(const FunctionCallbackInfo<Value>& args) {
  auto env = Environment::GetCurrent(args);
  env->ScheduleTimer(args[0]->IntegerValue(env->context()).FromJust());
}

再跳到env.cc:

void Environment::ScheduleTimer(int64_t duration_ms) {
  if (started_cleanup_) return;
  uv_timer_start(timer_handle(), RunTimers, duration_ms, 0);
}

可以看到這裏就將定時器的信息和libuv的事件循環聯繫在一起了,libuv還沒有研究,所以這條邏輯線暫時到此為止。再回到之前的示例,當三個定時器都添加完成后,內存中的對象關係基本是下面的樣子:

3.4 定時器的處理執行邏輯

至此我們已經將定時器的信息都存放好了,那麼它是如何被觸發的呢?我們找到node.js的啟動文件lib/internal/bootstrap/node.js284-290行,可以看到,在啟動函數中,Node.js通過調用setTimers( )方法將定時器處理函數processTimers傳遞給了底層,它最終會被用來調度執行定時器,processTimers方法由lib/internal/timers.js中提供的getTimerCallbacks(runNextTicks) 方法運行得到,所以聚焦到/lib/internal/timers.js中:

推測libuv每次需要檢查是否有定時器到期時都會運行processTimers( )方法,來看一下對應的邏輯,一個無限循環的while語句,直到二叉堆的堆頂沒有任何定時器時跳出循環並返回0。在循環體內部,會用堆頂元素的過期時間和當前時間相比,如果list.expiry更大,說明時機未到還不需要執行,把它的過期時間賦值給nextExpiry然後返回(返回邏輯先不細究)。如果邏輯執行到471行,說明堆頂元素的過期時間已經過了,ranAtLeastOneList這個標記位使得這段邏輯按照如下方式運行:

1.獲取到一個expiry已經過期的鏈表,首次向下執行時`ranAtLeastOneList`為false,則將其置為true,然後執行`listOnTimeout()`這個方法;
2.然後繼續取堆頂的鏈表,如果也過期了,再次執行時,會先執行`runNextTicks()`,再執行`listOnTimeout()`。

我們按照邏輯順序,先來看看listOnTimeout( )這個方法,它有近100行(我們以上面3個定時器的實例來看看它的執行邏輯):

  function listOnTimeout(list, now) {
    const msecs = list.msecs; //500 , 500ms的鏈表在堆頂

    debug('timeout callback %d', msecs);

    var diff, timer;
    let ranAtLeastOneTimer = false;
    while (timer = L.peek(list)) {  //取鏈表_idlePrev指向的定時器,也就是鏈表中最先到期的
      diff = now - timer._idleStart;  //計算當前時間和它開始計時那個時間點的時間差,

      // Check if this loop iteration is too early for the next timer.
      // This happens if there are more timers scheduled for later in the list.
      // 原文翻譯:檢測當前事件循環對於下一個定時器是否過早,這種情況會在鏈表中還有其他定時器時發生。
      // 人話翻譯:就是當前的時間點只需要觸發鏈表中第一個500ms定時器,下一個500ms定時器還沒到觸發時間。
      //         極端的相反情況就是由於阻塞時間已經過去很久了,鏈表裡的N個定時器全都過期了,都得執行。
      if (diff < msecs) {
        //更新鏈表中下一個到期定時器的時間記錄,計算邏輯稍微有點繞
        list.expiry = Math.max(timer._idleStart + msecs, now + 1);
        list.id = timerListId++;
        timerListQueue.percolateDown(1);//堆頂元素值發生更新,需要通過“下沉”來重構“堆”
        debug('%d list wait because diff is %d', msecs, diff);
        return; //直接結束了
      }

      //是不是貌似見過這段,先放着等會一塊說
      if (ranAtLeastOneTimer)
        runNextTicks();
      else
        ranAtLeastOneTimer = true;

      // The actual logic for when a timeout happens.
      L.remove(timer);

      const asyncId = timer[async_id_symbol];

      if (!timer._onTimeout) {
        if (timer[kRefed])
          refCount--;
        timer[kRefed] = null;

        if (destroyHooksExist() && !timer._destroyed) {
          emitDestroy(asyncId);
          timer._destroyed = true;
        }
        continue;
      }

      emitBefore(asyncId, timer[trigger_async_id_symbol]);

      let start;
      if (timer._repeat) //這部分看起來應該是interval的邏輯,interval底層實際上就是一個重複的timeout
        start = getLibuvNow();

      try {
        const args = timer._timerArgs;
        if (args === undefined)
          timer._onTimeout();  //設置定時器時傳入的回調函數被執行了
        else
          timer._onTimeout(...args);
      } finally {
        if (timer._repeat && timer._idleTimeout !== -1) {
          timer._idleTimeout = timer._repeat;
          if (start === undefined)
            start = getLibuvNow();
          insert(timer, timer[kRefed], start);//interval的真實執行邏輯,重新獲取時間然後插入到鏈表中
        } else if (!timer._idleNext && !timer._idlePrev) {
          if (timer[kRefed])
            refCount--;
          timer[kRefed] = null;

          if (destroyHooksExist() && !timer._destroyed) {
            emitDestroy(timer[async_id_symbol]);
            timer._destroyed = true;
          }
        }
      }

      emitAfter(asyncId);
    }
      
    //這塊需要注意的是,上面整個邏輯都包在while(timer = L.peek(list)){...}裏面

    // If `L.peek(list)` returned nothing, the list was either empty or we have
    // called all of the timer timeouts.
    // As such, we can remove the list from the object map and
    // the PriorityQueue.
    debug('%d list empty', msecs);

    // The current list may have been removed and recreated since the reference
    // to `list` was created. Make sure they're the same instance of the list
    // before destroying.
    // 原文翻譯:當前的list標識符所引用的list有可能已經經過了重建,刪除前需要確保它指向哈希表中的同一個實例。
    if (list === timerListMap[msecs]) {
      delete timerListMap[msecs];
      timerListQueue.shift();
    }
  }

3.5 實例分析

代碼邏輯因為包含了很多條件分支,所以不容易理解,我們以前文的實例作為線索來看看定時器觸發時的執行邏輯:

程序啟動后,processTimer( )方法不斷執行,第一個過期的定時器會在堆頂的500ms定時器鏈表(下面稱為500鏈表)中產生,過期時間戳為511。

假設時間戳到達600時程序再次執行processTimer( ),此時發現當前時間已經超過nextExpiry記錄的時間戳511,於是繼續向下執行進入listOnTimeout(list, now),這裏的list就是哈希表中鍵為500的鏈表,now就是當前時間600,進入listOnTimeout方法后,獲取到鏈表中最早的一個定時器timer,然後計算diff參數為600-11=589, 589 > 500, 於是繞過條件分支語句,ranAtLeastOneTimer為false也跳過(跳過後其值為true),接下來的邏輯從鏈表中刪除了這個timer,然後執行timer._onTimeout指向的回調函數,500鏈表只有一個定時器,所以下一循環時L.peek(list)返回null,循環語句跳出,繼續向後執行。此時list依然指向500鏈表,於是執行刪除邏輯,從哈希表和二叉堆中均移除500鏈表,兩個數據結構在底層會進行自調整。

processTimer( )再次執行時,堆頂的鏈表變成了1000ms定時器鏈表(下面稱為1000鏈表),nextExpiry賦值為list.expiry,也就是1001,表示1000ms定時器鏈表中下一個到期的定時器會在時間戳超過1001時過期,但時間還沒有到。下面分兩種情況來分析:

1.時間戳為1010時執行processTimer( )

時間來到1010點,processTimer( )被執行,當前時間1010大於nextExpiry=1001,於是從堆頂獲取到1000鏈表,進入listOnTimeout( ),第一輪while循環執行時的情形和500鏈表執行時是一致的,在第二輪循環中,timer指向1000鏈表中后添加的那個定時器,diff的值為 1010 – 21 = 989,989 < 1000 ,所以進入if(diff < msecs)的條件分支,list.expiry調整為下一個timer的過期時間1021,然後通過下沉來重建二叉堆(堆頂元素的expiry發生了變化),上面的實例中只剩了唯一一個鏈表,所以下沉操作沒有引發什麼實質影響,接着退出當前函數回到processTimer的循環體中,接着processTimer里的while循環繼續執行,再次檢查棧頂元素,時間還沒到,然後退出,等時間超過下一個過期時間1021后,最後一個定時器被觸發,過程基本一致,只是鏈表耗盡後會觸發listOnTimeout後面的清除哈希表和二叉堆中相關記錄的邏輯。

總結一下,鏈表的消耗邏輯是,從鏈表中不斷取出peek位置的定時器,如果過期了就執行,如果取到一個沒過期的,說明鏈表裡後續的都沒過期,就修改鏈表上的list.expiry屬性然後退回到processTimer的循環體里,如果鏈表耗盡了,就將它從哈希表和二叉堆中把這個鏈表刪掉。

2.時間戳為1050時執行processTimer( )

假如程序因為其他原因直到時間為1050時才開始檢查1000鏈表,此時它的兩個定時器都過期了需要被觸發,listOnTimeout( )中的循環語句執行第一輪時是一樣的,第二次循環執行時,跳過if(diff < msecs)的分支,然後由於ranAtLeastOneTimer標記位的變化,除了第一個定時器的回調外,其他都會先執行runNextTicks( )然後再執行定時器上綁的回調,等到鏈表耗盡后,進入後續的清除邏輯。

我們再來看一種更極端的情況,假如程序一直阻塞到時間戳為2000時才執行到processTimer(此時3個定時器都過期了,2個延遲1000ms,1個延遲500ms,堆頂為500ms鏈表),此時processTimer( )先進入第一次循環,處理500鏈表,然後500鏈表中唯一的定時器處理完后,邏輯回到processTimer的循環體,再進行第二輪循環,此時獲取到堆頂的1000鏈表,發現仍然需要執行,那麼就會先執行runNextTicks( ),然後在處理1000鏈表,後面的邏輯就和上面時間戳為1050時執行processTimer基本一致了。

至此定時器的常規邏輯已經解析完了,還有兩個細節需要提一下,首先是runNextTicks( ),從名字可以推測它應該是執行通過process.nextTick( )添加的函數,從這裏的實現邏輯來看,當有多個定時器需要觸發時,每個間隙都會去消耗nextTicks隊列中的待執行函數,以保證它可以起到“盡可能早地執行”的職責,對此不了解的讀者可以參考上一篇博文【譯】Node.js中的事件循環,定時器和process.nextTick。

四. 小結

timer模塊比較大,在了解基本數據結構的前提下不算特別難理解,setImmediate( )process.nextTick( )的實現感興趣的讀者可以自行學習,想要對事件循環機制有更深入的理解,需要學習C++和libuv的相關原理,筆者尚未深入涉獵,以後有機會再寫。

【精選推薦文章】

如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!!

想要讓你的商品在網路上成為最夯、最多人討論的話題?

網頁設計公司推薦更多不同的設計風格,搶佔消費者視覺第一線

不管是台北網頁設計公司台中網頁設計公司,全省皆有專員為您服務

想知道最厲害的台北網頁設計公司推薦台中網頁設計公司推薦專業設計師"嚨底家"!!

SAP中的數據庫表索引

數據庫表中的索引可以加快查詢的速度。索引是數據庫表字段的有序副本。附加的字段包含指向真實數據庫錶行的指針。排序可以使訪問錶行的速度變快,例如,可以使用二分搜索。數據庫表至少有一個主索引,由它的key字段定義。它也可以有一到多個二級索引。

本文鏈接:https://www.cnblogs.com/hhelibeb/p/11061879.html 

英文原文:https://help.sap.com/doc/abapdocu_753_index_htm/7.53/en-US/abenddic_database_tables_index.htm

主索引

主索引是由主鍵的key字段構造的唯一索引,AS ABAP總會自動創建它。對於每個索引字段的組合,表中最多只能有一條記錄。 如果無法使用主索引識別記錄集,比如說,沒有使用主索引查詢字段,就會發生全表掃描,或者數據庫系統會嘗試使用合適的二級索引(如果有的話)。

二級索引

除了由主鍵定義的主索引,也可以為數據庫表定義唯一或不唯一的二級索引。創建二級索引通常會提高數據庫的讀性能,前提是讀取的時候使用到了二級索引。

二級索引包含一系列數據庫表字段,有一個最大3位長度的文本数字組成的ID。0是一個保留ID,用來表示主索引。string和rawstring類型的字段無法成為索引字段(全文索引除外)。也不建議使用數據類型FLTP的字段作為索引字段。

數據庫表在數據庫中被創建的時候,二級索引也會被定義。此外,可以晚些在相同的系統中創建新的二級索引。如果如果在其他系統增加新的二級索引而不作修改的話,它們會被創建為擴展索引。以下是建議的索引的命名空間:

  • 客戶為標準表添加的索引ID前綴為’Y’或者’Z’。
  • 合作夥伴為標準表添加的索引ID前綴為’J’,不同合作夥伴創建的索引的名稱可能衝突。
  • 其他表可以有任意名字的索引,不過不應以’Y’,’Z’或’J’開頭。

數據庫中的索引名字通常是DBTAB~ID,DBTAB是數據庫表的名字,ID是3位字符的ID。也可能有其它名字,比如空格或下劃線。

二級索引可以是唯一的,但是(不像主索引)沒必要。對唯一索引而言,數據庫表不能含有同樣索引值的多行數據。試圖插入重複的行,會取消數據庫操作,並在ABAP中觸發相應的異常。在指定了client的表中,唯一索引必須包含client字段。

訪問數據庫時,數據庫系統的優化器會檢查是否有合適的索引,並使用它。索引的選擇取決於平台,意味着可以在ABAP字典中定義非唯一索引在不同的數據庫系統中是否可用。有幾種選項,

  • Index in all database systems:這個索引會在每個數據庫中創建。
  • In selected database systems:可以使用選擇列表或排除列表來定義數據庫系統,每個列表最多有4個條目。
  • No database index:不在任何數據庫中創建索引,這個選項可以用於刪除二級索引。

這些選項對錶緩存的二級索引無效。如果表緩存有相關設置,那麼系統就會根據表緩存的設置決定是否使用二級索引。

唯一二級索引總是會被創建,而且無法從數據庫刪除。可以使用事務代碼ST05中的SQL跟蹤功能來判斷訪問數據時系統使用的索引。

索引對於查詢數據的提升效果取決於索引代表結果數據集的能力。只有索引中可以對結果集進行有效約束的字段才是有用的。這種情況下,索引中的字段順序是一個對於數據的訪問速度十分重要的因素。第一個字段必須是那些有着大量不同可選值的字段。在查詢中,要在查詢條件中指定索引的第一個字段,這樣索引才有用。另外,只有一個索引字段前面的全部索引字段都在查詢條件內時,這個索引字段才生效。字段的訪問速度和索引是否為唯一索引無關。

對於以下情況,創建二級索引可以帶來好處:

  • 如果需要查詢的表記錄不包含在現有索引內,響應時間很久,應該創建二級索引。
  • 這個字段的選擇性很強,每個值可以用於區分少於5%的表記錄。
  • 數據庫主要用於讀取。因為更改表時也需要更新索引,會降低寫入性能。
  • 如果讀取的字段也在索引里,那麼在訪問索引后不需要再次從索引之外讀取它們。如果只有少量字段經常被選擇,把它們全部包含在索引里的做法可以大大提高性能。

注:選擇性(Selectivity),是指不重複的索引值(也叫基數,Cardinality)與表記錄數(#T)的比值, Index Selectivity = Cardinality / #T

二級索引也會增加系統負載,因為每次表內容被修改時,二級索引都要做相應調整。表的每個額外的索引都會降低插入行的性能。如果需要頻繁在表中插入數據,那麼應該只建立很少的索引。太多索引也會導致數據庫的優化器找不到正確的索引。為了避免這點,表中的索引最好不相交(沒有相同的字段)。

索引應該只包含幾個字段,比如,原則上不超過4個。這是因為索引字段在被更新的時候,索引也要被更新。適合作為索引的字段是:

  • 經常被查詢,並且選擇性高。需要把選擇性最高的字段放在索引的開始位置。
  • 如果一個字段在大部分表記錄中的值都是初始值,那麼它不應成為索引字段。
  • 如果一個數據庫表有不止一個索引,那麼索引間不應該重疊。

不應該為一個表創建超過5個索引,因為,

  • 每個索引都會增加更新開銷。
  • 數據量會增加。
  • 數據庫優化器會因為可選擇的索引過多變得更加容易出錯。

索引只支持明確的條件值,比如=或者LIKE。如果條件中包含某些不確定因素,比如<>,那麼索引將無法改善性能。條件中包含OR時,優化器通常停止工作。換句話說,使用索引時,OR條件的字段是不生效的。一個例外是OR關係互相獨立。因此,對於包含OR和索引字段結合的條件,有時需要修改條件的形式。(可以看下面的例子)

注意

  • 某些數據庫的索引會忽略0,意味着查詢0值時,沒有索引可用。
  • 如有必要,可以在ABAP SQL(Open SQL)中使用附加項%_HINTS為database hints來調整系統優化器,以決定使用哪個二級索引。

例子

下面這個句子會導致優化器無法使用索引,因為遇到了OR:

SELECT * FROM spfli 
         WHERE carrid = 'LH' AND 
              ( CITYFROM = 'FRANKFURT' OR  cityfrom = 'NEW YORK' ).

替換成下面這樣的一個相等的句子,可以根據現有索引對整個條件進行優化(原因見前文):

SELECT * 
       FROM spfli 
       WHERE ( carrid = 'LH' AND cityfrom = 'FRANKFURT' ) OR 
             ( carrid = 'LH' AND cityfrom = 'NEW YORK' ).

全文索引

SAP HANA數據庫支持全文索引,全文索引可以作為二級索引。全文索引會在數據庫中被創建為一個額外的可見的列。全文索引的列的內容會被保存在這個額外的列中,以某種格式存儲,在相關數據被訪問的時候會發揮作用。

以下是全文索引的使用條件:

  • 只有對SAP HANA數據庫中的列存儲類型的表,才可以創建全文索引。
  • 只能為數據類型為指定的幾種內建數據類型的列(CHAR, SHORTSTRING, STRING, or RAWSTRING)創建全文索引,一個全文索引只能對應一個列。
  • 數據庫表必須包含一個文本語言列。

全文索引總是非唯一索引。使用全文索引的訪問基於數據庫中的WHERE CONTAINS元素。目前這個元素在ABAP SQL中還不可用,需要使用Native SQL或者AMDP。

注意

更多有關全文索引的信息,參看:SAP HANA Developer Guide.

 

參考閱讀:MySQL索引入門簡述

 

【精選推薦文章】

自行創業 缺乏曝光? 下一步"網站設計"幫您第一時間規劃公司的門面形象

網頁設計一頭霧水??該從何著手呢? 找到專業技術的網頁設計公司,幫您輕鬆架站!

評比前十大台北網頁設計台北網站設計公司知名案例作品心得分享

台北網頁設計公司這麼多,該如何挑選?? 網頁設計報價省錢懶人包"嚨底家"

【機器學習之數學】03 有約束的非線性優化問題——拉格朗日乘子法、KKT條件、投影法

目錄

  • 1 將有約束問題轉化為無約束問題
    • 1.1 拉格朗日法
      • 1.1.1 KKT條件
      • 1.1.2 拉格朗日法更新方程
      • 1.1.3 凸優化問題下的拉格朗日法
    • 1.2 罰函數法
  • 2 對梯度算法進行修改,使其運用在有約束條件下
    • 2.1 投影法
      • 2.1.1 梯度下降法 to 投影梯度法
      • 2.1.2 正交投影算子
  • References
  • 相關博客

梯度下降法、最速下降法、牛頓法等迭代求解方法,都是在無約束的條件下使用的,而在有約束的問題中,直接使用這些梯度方法會有問題,如更新后的值不滿足約束條件。

那麼問題來了,如何處理有約束的優化問題?大致可以分為以下兩種方式:

  1. 將有約束的問題轉化為無約束的問題,如拉格朗日乘子法和KKT條件;
  2. 對無約束問題下的求解算法進行修改,使其能夠運用在有約束的問題中,如對梯度下降法進行投影,使得更新后的值都滿足約束條件。

1 將有約束問題轉化為無約束問題

1.1 拉格朗日法

僅含等式約束的優化問題
\[ \begin{array}{cl}{\text { minimize }} & {f(\boldsymbol{x})} \\ {\text { subject to }} & {\boldsymbol{h}(\boldsymbol{x})=\mathbf{0}}\end{array} \]

其中,\(x \in \mathbb{R}^n\)\(f : \mathbb{R}^{n} \rightarrow \mathbb{R}\)\(\boldsymbol{h} : \mathbb{R}^{n} \rightarrow \mathbb{R}^{m}, \boldsymbol{h}=\left[h_{1}, \ldots, h_{m}\right]^{\top}, \text { and } m \leq n\)

該問題的拉格朗日函數為:
\[ l(\boldsymbol{x}, \boldsymbol{\lambda})=f(\boldsymbol{x})+\boldsymbol{\lambda}^{\top} \boldsymbol{h}(\boldsymbol{x}) \]

FONC:對拉格朗日函數 \(l(\boldsymbol{x}, \boldsymbol{\lambda})\) 求偏導數,令偏導數都等於 0,求得的解必然滿足原問題的等式約束,可以從這些解裏面尋找是否有局部最優解。這是求得局部最優解的一階必要條件。

拉格朗日條件:(分別對 \(\bm x\)\(\bm \lambda\) 求偏導)
\[ \begin{array}{l}{D_{x} l\left(\boldsymbol{x}^{*}, \boldsymbol{\lambda}^{*}\right)=\mathbf{0}^{\top}} \\ {D_{\lambda} l\left(\boldsymbol{x}^{*}, \boldsymbol{\lambda}^{*}\right)=\mathbf{0}^{\top}}\end{array} \]

上式中,對 \(\lambda\) 求偏導數得到的就是等式約束。

拉格朗日條件是必要而非充分條件,即滿足上述方程的點 \(\boldsymbol x^{*}\) 不一定是極值點。

1.1.1 KKT條件

既含等式約束又含不等式約束的優化問題:
\[ \begin{array}{rl}{\operatorname{minimize}} & {f(\boldsymbol{x})} \\ {\text { subject to }} & {\boldsymbol{h}(\boldsymbol{x})=\mathbf{0}} \\ {} & {\boldsymbol{g}(\boldsymbol{x}) \leq \mathbf{0}}\end{array} \]

其中,\(f : \mathbb{R}^{n} \rightarrow \mathbb{R}\)\(\boldsymbol{h} : \mathbb{R}^{n} \rightarrow \mathbb{R}^{m}, m \leq n\),並且 \(\boldsymbol{g} : \mathbb{R}^{n} \rightarrow \mathbb{R}^{p}\)

將該問題轉化為拉格朗日形式:
\[ l(\boldsymbol{x}, \boldsymbol{\lambda})=f(\boldsymbol{x})+\boldsymbol{\lambda}^{\top} \boldsymbol{h}(\boldsymbol{x}) +\boldsymbol{\mu}^{\top} \boldsymbol{g}(\boldsymbol{x}) \]

\(\bm x^{*}\) 是原問題的一個局部極小點,則必然存在 \(\bm{\lambda}^{* \top} \in \mathbb{R}^m\)\(\bm{\mu}^{* \top} \in \mathbb{R}^p\),使得下列KKT條件成立:

  1. \(\bm {\mu}^{*} \geq 0\)
  2. \(D f\left(\boldsymbol{x}^{*}\right)+\boldsymbol{\lambda}^{* \top} D \boldsymbol{h}\left(\boldsymbol{x}^{*}\right)+\boldsymbol{\mu}^{* \top} D \boldsymbol{g}\left(\boldsymbol{x}^{*}\right)=\mathbf{0}^{\top}\)
  3. \(\boldsymbol{\mu}^{* \top} \boldsymbol{g}\left(\boldsymbol{x}^{*}\right)=0\)
  4. \({\boldsymbol{h}(\boldsymbol{x}^*)=\mathbf{0}}\)
  5. \({\boldsymbol{g}(\boldsymbol{x}^*) \leq \mathbf{0}}\)

KKT條件中,\(\bm{\lambda}^{*}\) 是拉格朗日乘子向量,\(\bm{\mu}^{*}\) 是KKT乘子向量,\(\bm{\lambda}^{*}\)\(\bm{\mu}^{*}\) 的元素分別稱為拉格朗日乘子和KKT乘子。

1.1.2 拉格朗日法更新方程

將含約束的優化問題轉化為拉格朗日形式后,我們可以用更新方程對該問題進行迭代求解。

這也是一種梯度算法,但拉格朗日乘子、KKT 乘子的更新和自變量 \(\bm x\) 的更新不同,自變量 \(\bm x\) 繼續採用梯度下降法更新,而拉格朗日乘子、KKT 乘子的更新方程如下:
\[ \boldsymbol{\lambda}^{(k+1)}=\boldsymbol{\lambda}^{(k)}+\beta_{k} \boldsymbol{h}\left(\boldsymbol{x}^{(k)}\right), \\ \boldsymbol{\mu}^{(k+1)}=\left[\boldsymbol{\mu}^{(k)}+\beta_{k} \boldsymbol{g}\left(\boldsymbol{x}^{(k)}\right)\right]_{+} \]

其中,\([\cdot]_{+}=\max \{\cdot, 0\}\)

1.1.3 凸優化問題下的拉格朗日法

拉格朗日乘子法和KKT條件在一般的含約束條件的優化問題中,都只是一階必要條件,而在凸優化問題中,則變成了充分條件。

凸優化問題指的是目標函數是凸函數,約束集是凸集的優化問題。線性規劃、二次規劃(目標函數為二次型函數、約束方程為線性方程)都可以歸為凸優化問題。

凸優化問題中,局部極小點就是全局極小點。極小點的一階必要條件就是凸優化問題的充分條件。

1.2 罰函數法

考慮一般形式的有約束優化問題:
\[ \begin{array}{cl}{\operatorname{minimize}} & {f(\boldsymbol{x})} \\ {\text { subject to }} & {\boldsymbol{x} \in \Omega}\end{array} \]

將問題變為如下無約束的形式:
\[ \operatorname{minimize} f(\boldsymbol{x})+\gamma P(\boldsymbol{x}) \]

其中,\(\gamma\) 是懲罰因子,\(P : \mathbb{R}^{n} \rightarrow \mathbb{R}\) 是罰函數。求解該無約束優化問題,把得到的解近似作為原問題的極小點。

罰函數需要滿足以下 3 個條件:

  1. \(\bm P\) 是連續的;
  2. 對所有 \(\bm x \in \mathbb{R}^n\)\(P(\boldsymbol{x}) \ge 0\) 成立;
  3. \(P(\boldsymbol{x})=0\),當且僅當 \(\bm x\) 是可行點(即 \({\bm{x} \in \Omega}\))。

2 對梯度算法進行修改,使其運用在有約束條件下

2.1 投影法

梯度下降法、最速下降法、牛頓法等優化算法都有通用的迭代公式:
\[ \boldsymbol{x}^{(k+1)}=\boldsymbol{x}^{(k)}+\alpha_{k} \boldsymbol{d}^{(k)} \]

其中,\(\boldsymbol{d}^{(k)}\) 是關於梯度 \(\nabla f(\bm x^{(k)})\) 的函數,如在梯度下降法中,\(\boldsymbol{d}^{(k)} = -\nabla f(\bm x^{(k)})\)

考慮優化問題:
\[ \begin{array}{cl}{\operatorname{minimize}} & {f(\boldsymbol{x})} \\ {\text { subject to }} & {\boldsymbol{x} \in \Omega}\end{array} \]

在上述有約束的優化問題中,\(\boldsymbol{x}^{(k)}+\alpha_{k} \boldsymbol{d}^{(k)}\) 可能不在約束集 \(\Omega\) 內,這是梯度下降等方法無法使用的原因。

而投影法做的是,如果 \(\boldsymbol{x}^{(k)}+\alpha_{k} \boldsymbol{d}^{(k)}\) 跑到約束集 \(\Omega\) 外面去了,那麼將它投影到約束集內“最接近”的點;如果 \(\boldsymbol{x}^{(k)}+\alpha_{k} \boldsymbol{d}^{(k)} \in \Omega\),那麼正常更新即可。

投影法的更新公式為:
\[ \boldsymbol{x}^{(k+1)}=\boldsymbol{\Pi}\left[\boldsymbol{x}^{(k)}+\alpha_{k} \boldsymbol{d}^{(k)}\right] \]

其中 \(\bm \Pi\) 為投影算子,\(\bm \Pi[\bm x]\) 稱為 \(\bm x\)\(\Omega\) 上的投影。

2.1.1 梯度下降法 to 投影梯度法

梯度下降法的迭代公式為:
\[ \boldsymbol{x}^{(k+1)}=\boldsymbol{x}^{(k)}-\alpha_{k} \nabla f\left(\boldsymbol{x}^{(k)}\right) \]

將投影算法引入梯度下降法,可得投影梯度法,迭代公式如下:
\[ \boldsymbol{x}^{(k+1)}=\boldsymbol{\Pi}\left[\boldsymbol{x}^{(k)}-\alpha_{k} \nabla f\left(\boldsymbol{x}^{(k)}\right)\right] \]

2.1.2 正交投影算子

含線性約束優化問題的投影梯度法可以利用正交投影算子來更新 \(\bm x^{(k)}\)

含線性約束的優化問題如下所示:
\[ \begin{array}{cl}{\operatorname{minimize}} & {f(\boldsymbol{x})} \\ {\text { subject to }} & {\boldsymbol{A x}=\boldsymbol{b}}\end{array} \]

其中,\(f : \mathbb{R}^{n} \rightarrow \mathbb{R}\)\(\boldsymbol{A} \in \mathbb{R}^{m \times n}, m<n\)\(\operatorname{rank} \boldsymbol{A}=m, \boldsymbol{b} \in \mathbb{R}^{m}\),約束集 \(\Omega=\{\boldsymbol{x} :\boldsymbol{A} \boldsymbol{x}=\boldsymbol{b} \}\)

這種情況下,正交投影算子矩陣 \(\bm P\) 為:
\[ \boldsymbol{P}=\boldsymbol{I}_{n}-\boldsymbol{A}^{\top}\left(\boldsymbol{A} \boldsymbol{A}^{\top}\right)^{-1} \boldsymbol{A} \]

正交投影算子 \(\bm P\) 有兩個重要性質:

  1. \(P=P^{\top}\).
  2. \(P^{2}=P\).

在投影梯度算法中,可以按照如下公式更新 \(\bm x^{(k)}\)
\[ \boldsymbol{x}^{(k+1)}=\boldsymbol{x}^{(k)}-\alpha_{k} \boldsymbol{P} \nabla \boldsymbol{f}(\boldsymbol{x}^{(k)}) \]

References

Edwin K. P. Chong, Stanislaw H. Zak-An Introduction to Optimization, 4th Edition

相關博客

【機器學習之數學】01 導數、偏導數、方嚮導數、梯度
【機器學習之數學】02 梯度下降法、最速下降法、牛頓法、共軛方向法、擬牛頓法
【機器學習之數學】03 有約束的非線性優化問題——拉格朗日乘子法、KKT條件、投影法

【精選推薦文章】

智慧手機時代的來臨,RWD網頁設計已成為網頁設計推薦首選

想知道網站建置、網站改版該如何進行嗎?將由專業工程師為您規劃客製化網頁設計及後台網頁設計

帶您來看台北網站建置台北網頁設計,各種案例分享

廣告預算用在刀口上,網站設計公司幫您達到更多曝光效益