一嗓化、goroutine簡(jiǎn)介
goroutine是go語(yǔ)言中最為NB的設(shè)計(jì)棠涮,也是其魅力所在,goroutine的本質(zhì)是協(xié)程刺覆,是實(shí)現(xiàn)并行計(jì)算的核心严肪。goroutine使用方式非常的簡(jiǎn)單,只需使用go關(guān)鍵字即可啟動(dòng)一個(gè)協(xié)程谦屑,并且它是處于異步方式運(yùn)行驳糯,你不需要等它運(yùn)行完成以后在執(zhí)行以后的代碼。
go?func()//通過(guò)go關(guān)鍵字啟動(dòng)一個(gè)協(xié)程來(lái)運(yùn)行函數(shù)
二氢橙、goroutine內(nèi)部原理
概念介紹
在進(jìn)行實(shí)現(xiàn)原理之前酝枢,了解下一些關(guān)鍵性術(shù)語(yǔ)的概念。
并發(fā)
一個(gè)cpu上能同時(shí)執(zhí)行多項(xiàng)任務(wù)悍手,在很短時(shí)間內(nèi)隧枫,cpu來(lái)回切換任務(wù)執(zhí)行(在某段很短時(shí)間內(nèi)執(zhí)行程序a,然后又迅速得切換到程序b去執(zhí)行)谓苟,有時(shí)間上的重疊(宏觀上是同時(shí)的官脓,微觀仍是順序執(zhí)行),這樣看起來(lái)多個(gè)任務(wù)像是同時(shí)執(zhí)行,這就是并發(fā)涝焙。
并行
當(dāng)系統(tǒng)有多個(gè)CPU時(shí),每個(gè)CPU同一時(shí)刻都運(yùn)行任務(wù)卑笨,互不搶占自己所在的CPU資源,同時(shí)進(jìn)行仑撞,稱(chēng)為并行赤兴。
進(jìn)程
cpu在切換程序的時(shí)候妖滔,如果不保存上一個(gè)程序的狀態(tài)(也就是我們常說(shuō)的context--上下文),直接切換下一個(gè)程序桶良,就會(huì)丟失上一個(gè)程序的一系列狀態(tài)座舍,于是引入了進(jìn)程這個(gè)概念,用以劃分好程序運(yùn)行時(shí)所需要的資源陨帆。因此進(jìn)程就是一個(gè)程序運(yùn)行時(shí)候的所需要的基本資源單位(也可以說(shuō)是程序運(yùn)行的一個(gè)實(shí)體)曲秉。
線(xiàn)程
cpu切換多個(gè)進(jìn)程的時(shí)候,會(huì)花費(fèi)不少的時(shí)間疲牵,因?yàn)榍袚Q進(jìn)程需要切換到內(nèi)核態(tài)承二,而每次調(diào)度需要內(nèi)核態(tài)都需要讀取用戶(hù)態(tài)的數(shù)據(jù),進(jìn)程一旦多起來(lái)纲爸,cpu調(diào)度會(huì)消耗一大堆資源亥鸠,因此引入了線(xiàn)程的概念,線(xiàn)程本身幾乎不占有資源识啦,他們共享進(jìn)程里的資源负蚊,內(nèi)核調(diào)度起來(lái)不會(huì)那么像進(jìn)程切換那么耗費(fèi)資源。
協(xié)程
協(xié)程擁有自己的寄存器上下文和棧颓哮。協(xié)程調(diào)度切換時(shí)家妆,將寄存器上下文和棧保存到其他地方,在切回來(lái)的時(shí)候题翻,恢復(fù)先前保存的寄存器上下文和棧揩徊。因此腰鬼,協(xié)程能保留上一次調(diào)用時(shí)的狀態(tài)(即所有局部狀態(tài)的一個(gè)特定組合)嵌赠,每次過(guò)程重入時(shí),就相當(dāng)于進(jìn)入上一次調(diào)用的狀態(tài)熄赡,換種說(shuō)法:進(jìn)入上一次離開(kāi)時(shí)所處邏輯流的位置姜挺。線(xiàn)程和進(jìn)程的操作是由程序觸發(fā)系統(tǒng)接口,最后的執(zhí)行者是系統(tǒng)彼硫;協(xié)程的操作執(zhí)行者則是用戶(hù)自身程序炊豪,goroutine也是協(xié)程。
調(diào)度模型簡(jiǎn)介
groutine能擁有強(qiáng)大的并發(fā)實(shí)現(xiàn)是通過(guò)GPM調(diào)度模型實(shí)現(xiàn)拧篮,下面就來(lái)解釋下goroutine的調(diào)度模型词渤。
Go的調(diào)度器內(nèi)部有四個(gè)重要的結(jié)構(gòu):M,P串绩,S缺虐,Sched,如上圖所示(Sched未給出)
M:M代表內(nèi)核級(jí)線(xiàn)程礁凡,一個(gè)M就是一個(gè)線(xiàn)程高氮,goroutine就是跑在M之上的慧妄;M是一個(gè)很大的結(jié)構(gòu),里面維護(hù)小對(duì)象內(nèi)存cache(mcache)剪芍、當(dāng)前執(zhí)行的goroutine塞淹、隨機(jī)數(shù)發(fā)生器等等非常多的信息
G:代表一個(gè)goroutine,它有自己的棧罪裹,instruction pointer和其他信息(正在等待的channel等等)饱普,用于調(diào)度。
P:P全稱(chēng)是Processor坊谁,處理器费彼,它的主要用途就是用來(lái)執(zhí)行g(shù)oroutine的,所以它也維護(hù)了一個(gè)goroutine隊(duì)列口芍,里面存儲(chǔ)了所有需要它來(lái)執(zhí)行的goroutine
Sched:代表調(diào)度器箍铲,它維護(hù)有存儲(chǔ)M和G的隊(duì)列以及調(diào)度器的一些狀態(tài)信息等。
調(diào)度實(shí)現(xiàn)
從上圖中看鬓椭,有2個(gè)物理線(xiàn)程M颠猴,每一個(gè)M都擁有一個(gè)處理器P,每一個(gè)也都有一個(gè)正在運(yùn)行的goroutine小染。
P的數(shù)量可以通過(guò)GOMAXPROCS()來(lái)設(shè)置翘瓮,它其實(shí)也就代表了真正的并發(fā)度,即有多少個(gè)goroutine可以同時(shí)運(yùn)行裤翩。
圖中灰色的那些goroutine并沒(méi)有運(yùn)行资盅,而是出于ready的就緒態(tài),正在等待被調(diào)度踊赠。P維護(hù)著這個(gè)隊(duì)列(稱(chēng)之為runqueue)呵扛,
Go語(yǔ)言里,啟動(dòng)一個(gè)goroutine很容易:go function 就行筐带,所以每有一個(gè)go語(yǔ)句被執(zhí)行今穿,runqueue隊(duì)列就在其末尾加入一個(gè)
goroutine,在下一個(gè)調(diào)度點(diǎn)伦籍,就從runqueue中取出(如何決定取哪個(gè)goroutine蓝晒?)一個(gè)goroutine執(zhí)行。
當(dāng)一個(gè)OS線(xiàn)程M0陷入阻塞時(shí)(如下圖)帖鸦,P轉(zhuǎn)而在運(yùn)行M1芝薇,圖中的M1可能是正被創(chuàng)建,或者從線(xiàn)程緩存中取出作儿。
當(dāng)MO返回時(shí)洛二,它必須嘗試取得一個(gè)P來(lái)運(yùn)行g(shù)oroutine,一般情況下,它會(huì)從其他的OS線(xiàn)程那里拿一個(gè)P過(guò)來(lái)灭红,
如果沒(méi)有拿到的話(huà)侣滩,它就把goroutine放在一個(gè)global runqueue里,然后自己睡眠(放入線(xiàn)程緩存里)变擒。所有的P也會(huì)周期性的檢查global runqueue并運(yùn)行其中的goroutine君珠,否則global runqueue上的goroutine永遠(yuǎn)無(wú)法執(zhí)行。
另一種情況是P所分配的任務(wù)G很快就執(zhí)行完了(分配不均)娇斑,這就導(dǎo)致了這個(gè)處理器P很忙策添,但是其他的P還有任務(wù),此時(shí)如果global runqueue沒(méi)有任務(wù)G了毫缆,那么P不得不從其他的P里拿一些G來(lái)執(zhí)行唯竹。一般來(lái)說(shuō),如果P從其他的P那里要拿任務(wù)的話(huà)苦丁,一般就拿run queue的一半浸颓,這就確保了每個(gè)OS線(xiàn)程都能充分的使用,如下圖:
參考地址:http://morsmachine.dk/go-scheduler
三旺拉、使用goroutine
基本使用
設(shè)置goroutine運(yùn)行的CPU數(shù)量产上,最新版本的go已經(jīng)默認(rèn)已經(jīng)設(shè)置了。
num?:=?runtime.NumCPU()????//獲取主機(jī)的邏輯CPU個(gè)數(shù)runtime.GOMAXPROCS(num)????//設(shè)置可同時(shí)執(zhí)行的最大CPU數(shù)
使用示例
package?main
import?(????"fmt"
????"time")
func?cal(a?int?,?b?int?)??{
????c?:=?a+b
????fmt.Printf("%d?+?%d?=?%d\n",a,b,c)
}
func?main()?{
????for?i?:=0?;?i<10?;i++{
????????go?cal(i,i+1)??//啟動(dòng)10個(gè)goroutine?來(lái)計(jì)算????}
????time.Sleep(time.Second?*?2)?//?sleep作用是為了等待所有任務(wù)完成}?
//結(jié)果//8?+?9?=?17//9?+?10?=?19//4?+?5?=?9//5?+?6?=?11//0?+?1?=?1//1?+?2?=?3//2?+?3?=?5//3?+?4?=?7//7?+?8?=?15//6?+?7?=?13
goroutine異常捕捉
當(dāng)啟動(dòng)多個(gè)goroutine時(shí)蛾狗,如果其中一個(gè)goroutine異常了晋涣,并且我們并沒(méi)有對(duì)進(jìn)行異常處理,那么整個(gè)程序都會(huì)終止沉桌,所以我們?cè)诰帉?xiě)程序時(shí)候最好每個(gè)goroutine所運(yùn)行的函數(shù)都做異常處理谢鹊,異常處理采用recover
package?main
import?(????"fmt"
????"time")
func?addele(a?[]int?,i?int)??{
????defer?func()?{????//匿名函數(shù)捕獲錯(cuò)誤
????????err?:=?recover()????????if?err?!=?nil?{
????????????fmt.Println("add?ele?fail")
????????}
????}()
???a[i]=i
???fmt.Println(a)
}
func?main()?{
????Arry?:=?make([]int,4)????for?i?:=0?;?i<10?;i++{
????????go?addele(Arry,i)
????}
????time.Sleep(time.Second?*?2)
}//結(jié)果add?ele?fail
[0?0?0?0]
[0?1?0?0]
[0?1?2?0]
[0?1?2?3]
add?ele?fail
add?ele?fail
add?ele?fail
add?ele?fail
add?ele?fail
同步的goroutine
由于goroutine是異步執(zhí)行的,那很有可能出現(xiàn)主程序退出時(shí)還有g(shù)oroutine沒(méi)有執(zhí)行完留凭,此時(shí)goroutine也會(huì)跟著退出佃扼。此時(shí)如果想等到所有g(shù)oroutine任務(wù)執(zhí)行完畢才退出,go提供了sync包和channel來(lái)解決同步問(wèn)題冰抢,當(dāng)然如果你能預(yù)測(cè)每個(gè)goroutine執(zhí)行的時(shí)間松嘶,你還可以通過(guò)time.Sleep方式等待所有的groutine執(zhí)行完成以后在退出程序(如上面的列子)艘狭。
示例一:使用sync包同步goroutine
sync大致實(shí)現(xiàn)方式
WaitGroup 等待一組goroutinue執(zhí)行完畢. 主程序調(diào)用 Add 添加等待的goroutinue數(shù)量. 每個(gè)goroutinue在執(zhí)行結(jié)束時(shí)調(diào)用 Done 挎扰,此時(shí)等待隊(duì)列數(shù)量減1.,主程序通過(guò)Wait阻塞巢音,直到等待隊(duì)列為0.
package?main
import?(????"fmt"
????"sync")
func?cal(a?int?,?b?int?,n?*sync.WaitGroup)??{
????c?:=?a+b
????fmt.Printf("%d?+?%d?=?%d\n",a,b,c)
????defer?n.Done()?//goroutinue完成后,?WaitGroup的計(jì)數(shù)-1}
func?main()?{????var?go_sync?sync.WaitGroup?//聲明一個(gè)WaitGroup變量
????for?i?:=0?;?i<10?;i++{
????????go_sync.Add(1)?//?WaitGroup的計(jì)數(shù)加1
????????go?cal(i,i+1,&go_sync)??
????}
????go_sync.Wait()??//等待所有g(shù)oroutine執(zhí)行完畢}//結(jié)果9?+?10?=?192?+?3?=?53?+?4?=?74?+?5?=?95?+?6?=?111?+?2?=?36?+?7?=?137?+?8?=?150?+?1?=?18?+?9?=?17
示例二:通過(guò)channel實(shí)現(xiàn)goroutine之間的同步遵倦。
實(shí)現(xiàn)方式:通過(guò)channel能在多個(gè)groutine之間通訊,當(dāng)一個(gè)goroutine完成時(shí)候向channel發(fā)送退出信號(hào),等所有g(shù)oroutine退出時(shí)候官撼,利用for循環(huán)channe去channel中的信號(hào)梧躺,若取不到數(shù)據(jù)會(huì)阻塞原理,等待所有g(shù)oroutine執(zhí)行完畢,使用該方法有個(gè)前提是你已經(jīng)知道了你啟動(dòng)了多少個(gè)goroutine掠哥。
package?main
import?(????"fmt"
????"time")
func?cal(a?int?,?b?int?,Exitchan?chan?bool)??{
????c?:=?a+b
????fmt.Printf("%d?+?%d?=?%d\n",a,b,c)
????time.Sleep(time.Second*2)
????Exitchan?<-?true}
func?main()?{
????Exitchan?:=?make(chan?bool,10)??//聲明并分配管道內(nèi)存
????for?i?:=0?;?i<10?;i++{
????????go?cal(i,i+1,Exitchan)
????}????for?j?:=0;?j<10;?j++{???
?????????<-?Exitchan??//取信號(hào)數(shù)據(jù)巩踏,如果取不到則會(huì)阻塞????}
????close(Exitchan)?//?關(guān)閉管道}
goroutine之間的通訊
goroutine本質(zhì)上是協(xié)程,可以理解為不受內(nèi)核調(diào)度续搀,而受go調(diào)度器管理的線(xiàn)程塞琼。goroutine之間可以通過(guò)channel進(jìn)行通信或者說(shuō)是數(shù)據(jù)共享,當(dāng)然你也可以使用全局變量來(lái)進(jìn)行數(shù)據(jù)共享禁舷。
示例:使用channel模擬消費(fèi)者和生產(chǎn)者模式
package?main
import?(????"fmt"
????"sync")
func?Productor(mychan?chan?int,data?int,wait?*sync.WaitGroup)??{
????mychan?<-?data
????fmt.Println("product?data:",data)
????wait.Done()
}
func?Consumer(mychan?chan?int,wait?*sync.WaitGroup)??{
?????a?:=?<-?mychan
????fmt.Println("consumer?data:",a)
?????wait.Done()
}
func?main()?{
????datachan?:=?make(chan?int,?100)???//通訊數(shù)據(jù)管道
????var?wg?sync.WaitGroup????for?i?:=?0;?i?<?10;?i++?{
????????go?Productor(datachan,?i,&wg)?//生產(chǎn)數(shù)據(jù)
????????wg.Add(1)
????}????for?j?:=?0;?j?<?10;?j++?{
????????go?Consumer(datachan,&wg)??//消費(fèi)數(shù)據(jù)
????????wg.Add(1)
????}
????wg.Wait()
}//結(jié)果consumer?data:?4product?data:?5product?data:?6product?data:?7product?data:?8product?data:?9consumer?data:?1consumer?data:?5consumer?data:?6consumer?data:?7consumer?data:?8consumer?data:?9product?data:?2consumer?data:?2product?data:?3consumer?data:?3product?data:?4consumer?data:?0product?data:?0product?data:?1
四彪杉、channel
簡(jiǎn)介
channel俗稱(chēng)管道,用于數(shù)據(jù)傳遞或數(shù)據(jù)共享牵咙,其本質(zhì)是一個(gè)先進(jìn)先出的隊(duì)列派近,使用goroutine+channel進(jìn)行數(shù)據(jù)通訊簡(jiǎn)單高效,同時(shí)也線(xiàn)程安全洁桌,多個(gè)goroutine可同時(shí)修改一個(gè)channel渴丸,不需要加鎖。
channel可分為三種類(lèi)型:
只讀channel:只能讀channel里面數(shù)據(jù)另凌,不可寫(xiě)入
只寫(xiě)channel:只能寫(xiě)數(shù)據(jù)曙强,不可讀
一般channel:可讀可寫(xiě)
channel使用
定義和聲明
var?readOnlyChan?<-chan?int????????????//?只讀chanvar?writeOnlyChan?chan<-?int???????????//?只寫(xiě)chanvar?mychan??chan?int?????????????????????//讀寫(xiě)channel//定義完成以后需要make來(lái)分配內(nèi)存空間,不然使用會(huì)deadlockmychannel?=?make(chan?int,10)//或者read_only?:=?make?(<-chan?int,10)//定義只讀的channelwrite_only?:=?make?(chan<-?int,10)//定義只寫(xiě)的channelread_write?:=?make?(chan?int,10)//可同時(shí)讀寫(xiě)
?讀寫(xiě)數(shù)據(jù)
需要注意的是:
管道如果未關(guān)閉途茫,在讀取超時(shí)會(huì)則會(huì)引發(fā)deadlock異常
管道如果關(guān)閉進(jìn)行寫(xiě)入數(shù)據(jù)會(huì)pannic
當(dāng)管道中沒(méi)有數(shù)據(jù)時(shí)候再行讀取或讀取到默認(rèn)值碟嘴,如int類(lèi)型默認(rèn)值是0
ch?<-?"wd"??//寫(xiě)數(shù)據(jù)a?:=?<-?ch?//讀取數(shù)據(jù)a,?ok?:=?<-ch??//優(yōu)雅的讀取數(shù)據(jù)
循環(huán)管道
需要注意的是:
使用range循環(huán)管道,如果管道未關(guān)閉會(huì)引發(fā)deadlock錯(cuò)誤囊卜。
如果采用for死循環(huán)已經(jīng)關(guān)閉的管道娜扇,當(dāng)管道沒(méi)有數(shù)據(jù)時(shí)候,讀取的數(shù)據(jù)會(huì)是管道的默認(rèn)值栅组,并且循環(huán)不會(huì)退出雀瓢。
package?main
import?(????"fmt"
????"time")
func?main()?{
????mychannel?:=?make(chan?int,10)????for?i?:=?0;i?<?10;i++{
????????mychannel?<-?i
????}
????close(mychannel)??//關(guān)閉管道
????fmt.Println("data?lenght:?",len(mychannel))????for??v?:=?range?mychannel?{??//循環(huán)管道????????fmt.Println(v)
????}
????fmt.Printf("data?lenght:??%d",len(mychannel))
}
帶緩沖區(qū)channe和不帶緩沖區(qū)channel
帶緩沖區(qū)channel:定義聲明時(shí)候制定了緩沖區(qū)大小(長(zhǎng)度),可以保存多個(gè)數(shù)據(jù)玉掸。
不帶緩沖區(qū)channel:只能存一個(gè)數(shù)據(jù)刃麸,并且只有當(dāng)該數(shù)據(jù)被取出時(shí)候才能存下一個(gè)數(shù)據(jù)。
ch?:=?make(chan?int)?//不帶緩沖區(qū)ch?:=?make(chan?int?,10)?//帶緩沖區(qū)
不帶緩沖區(qū)示例:
package?main
import?"fmt"func?test(c?chan?int)?{????for?i?:=?0;?i?<?10;?i++?{
????????fmt.Println("send?",?i)
????????c?<-?i
????}
}
func?main()?{
????ch?:=?make(chan?int)
????go?test(ch)????for?j?:=?0;?j?<?10;?j++?{
????????fmt.Println("get?",?<-ch)
????}
}//結(jié)果:send??0send??1get??0get??1send??2send??3get??2get??3send??4send??5get??4get??5send??6send??7get??6get??7send??8send??9get??8get??9
channel實(shí)現(xiàn)作業(yè)池
我們創(chuàng)建三個(gè)channel司浪,一個(gè)channel用于接受任務(wù)泊业,一個(gè)channel用于保持結(jié)果,還有個(gè)channel用于決定程序退出的時(shí)候啊易。
package?main
import?(????"fmt")
func?Task(taskch,?resch?chan?int,?exitch?chan?bool)?{
????defer?func()?{???//異常處理
????????err?:=?recover()????????if?err?!=?nil?{
????????????fmt.Println("do?task?error:",?err)????????????return
????????}
????}()????for?t?:=?range?taskch?{?//??處理任務(wù)
????????fmt.Println("do?task?:",?t)
????????resch?<-?t?//????}
????exitch?<-?true?//處理完發(fā)送退出信號(hào)}
func?main()?{
????taskch?:=?make(chan?int,?20)?//任務(wù)管道
????resch?:=?make(chan?int,?20)??//結(jié)果管道
????exitch?:=?make(chan?bool,?5)?//退出管道????go?func()?{????????for?i?:=?0;?i?<?10;?i++?{
????????????taskch?<-?i
????????}
????????close(taskch)
????}()????for?i?:=?0;?i?<?5;?i++?{??//啟動(dòng)5個(gè)goroutine做任務(wù)????????go?Task(taskch,?resch,?exitch)
????}
????go?func()?{?//等5個(gè)goroutine結(jié)束
????????for?i?:=?0;?i?<?5;?i++?{????????????<-exitch
????????}
????????close(resch)??//任務(wù)處理完成關(guān)閉結(jié)果管道吁伺,不然range報(bào)錯(cuò)
????????close(exitch)??//關(guān)閉退出管道????}()????for?res?:=?range?resch{??//打印結(jié)果
????????fmt.Println("task?res:",res)
????}
}
只讀channel和只寫(xiě)channel
一般定義只讀和只寫(xiě)的管道意義不大,更多時(shí)候我們可以在參數(shù)傳遞時(shí)候指明管道可讀還是可寫(xiě)租谈,即使當(dāng)前管道是可讀寫(xiě)的篮奄。
package?main
import?(????"fmt"
????"time")//只能向chan里寫(xiě)數(shù)據(jù)func?send(c?chan<-?int)?{????for?i?:=?0;?i?<?10;?i++?{
????????c?<-?i
????}
}//只能取channel中的數(shù)據(jù)func?get(c?<-chan?int)?{????for?i?:=?range?c?{
????????fmt.Println(i)
????}
}
func?main()?{
????c?:=?make(chan?int)
????go?send(c)
????go?get(c)
????time.Sleep(time.Second*1)
}//結(jié)果0123456789
select-case實(shí)現(xiàn)非阻塞channel
原理通過(guò)select+case加入一組管道,當(dāng)滿(mǎn)足(這里說(shuō)的滿(mǎn)足意思是有數(shù)據(jù)可讀或者可寫(xiě))select中的某個(gè)case時(shí)候,那么該case返回窟却,若都不滿(mǎn)足case昼丑,則走default分支。
package?main
import?(????"fmt")
func?send(c?chan?int)??{????for?i?:=1?;?i<10?;i++??{
?????c?<-i
?????fmt.Println("send?data?:?",i)
????}
}
func?main()?{
????resch?:=?make(chan?int,20)
????strch?:=?make(chan?string,10)
????go?send(resch)
????strch?<-?"wd"
????select?{????case?a?:=?<-resch:
????????fmt.Println("get?data?:?",?a)????case?b?:=?<-strch:
????????fmt.Println("get?data?:?",?b)????default:
????????fmt.Println("no?channel?actvie")
????}
}//結(jié)果:get?data?:??wd
channel頻率控制
在對(duì)channel進(jìn)行讀寫(xiě)的時(shí)夸赫,go還提供了非常人性化的操作矾克,那就是對(duì)讀寫(xiě)的頻率控制,通過(guò)time.Ticke實(shí)現(xiàn)
示例:
package?main
import?(????"time"
????"fmt")
func?main(){
????requests:=?make(chan?int?,5)????for?i:=1;i<5;i++{
????????requests<-i
????}
????close(requests)
????limiter?:=?time.Tick(time.Second*1)????for?req:=range?requests{????????<-limiter
????????fmt.Println("requets",req,time.Now())?//執(zhí)行到這里憔足,需要隔1秒才繼續(xù)往下執(zhí)行胁附,time.Tick(timer)上面已定義????}
}//結(jié)果:requets?1?2018-07-06?10:17:35.98056403?+0800?CST?m=+1.004248763requets?2?2018-07-06?10:17:36.978123472?+0800?CST?m=+2.001798205requets?3?2018-07-06?10:17:37.980869517?+0800?CST?m=+3.004544250requets?4?2018-07-06?10:17:38.976868836?+0800?CST?m=+4.000533569