小藍(lán)談線程

近期跑步的時(shí)間少了,寫作靈感盡失店归,已經(jīng)好久沒有更新文章了。所幸前段時(shí)間看了一些關(guān)于多線程的資料酪我,故而有了這篇文章消痛,也算是一個(gè)階段性的總結(jié)吧。

Programming

一. 先普及一下知識(shí)

開始扯淡之前先來貼一些關(guān)于線程的文字都哭。

1)進(jìn)程與線程有何區(qū)別

有人在StackOverflow概括得比較全面秩伞,線程其實(shí)就是輕量級(jí)的進(jìn)程逞带。一般進(jìn)程都有自己的一部分獨(dú)立的系統(tǒng)資源,彼此是隔離的纱新。為了能使不同的進(jìn)程之間能夠互相訪問資源并進(jìn)行協(xié)調(diào)工作展氓,則需要通過進(jìn)程間的通信。而線程則采用共享內(nèi)存空間的形式脸爱,多個(gè)線程可以共享同一份內(nèi)存空間遇汞。相比起進(jìn)程,雖然線程看起來占用內(nèi)存空間少了簿废,但是卻會(huì)出現(xiàn)資源競(jìng)爭(zhēng)的情況空入。

2)并行與并發(fā)

采用多線程或者多進(jìn)程的方式進(jìn)行開發(fā)的方式稱為并發(fā),有些語言甚至可以充分利用電腦CPU的多核特征實(shí)現(xiàn)并行捏鱼。那么并行與并發(fā)又有何區(qū)別执庐?這里有比較有意思的解答Github

簡(jiǎn)單來講导梆,并行就是多個(gè)任務(wù)同時(shí)執(zhí)行轨淌,而并發(fā)則是同一時(shí)間段多個(gè)任務(wù)交替執(zhí)行。并行強(qiáng)調(diào)的是同一時(shí)間點(diǎn)兩個(gè)任務(wù)同時(shí)執(zhí)行看尼,而并發(fā)強(qiáng)調(diào)的是同一時(shí)間段兩個(gè)任務(wù)同時(shí)執(zhí)行递鹉。

二. Ruby中的高并發(fā)

傳統(tǒng)意義上的Ruby--MRI,有Thread這個(gè)類藏斩,這樣看來它是支持多線程的躏结,我們可以用這個(gè)類來創(chuàng)建多個(gè)線程實(shí)例。然而在MRI里面我們卻只能實(shí)現(xiàn)并發(fā)狰域,它并不能活用電腦的CPU多核特征實(shí)現(xiàn)并行操作媳拴。下面我分場(chǎng)景來講解一下相關(guān)的概念。

1) GIL的約束

得益于GIL(全局解析器鎖)的存在兆览,MRI只能夠?qū)崿F(xiàn)并發(fā)屈溉,并無法充分利用CPU的多核特征,實(shí)現(xiàn)并行任務(wù)抬探,進(jìn)而減少程序運(yùn)行時(shí)間子巾。

MRI里面線程只有在拿到GIL鎖的時(shí)候才能夠運(yùn)行,即便我們創(chuàng)建了多個(gè)線程小压,本質(zhì)上也就只有一個(gè)線程實(shí)例能夠拿到GIL线梗,如此看來某一時(shí)刻便只能有一個(gè)線程在運(yùn)行。

可以考慮下面這樣的場(chǎng)景:

老師給小藍(lán)安排了除草任務(wù)怠益,小藍(lán)為了加快速度呼喚了好友小張仪搔,然而除草任務(wù)需要有鋤頭才能進(jìn)行。為此溉痢,即便有好友相助但鋤頭卻只有一把所以兩個(gè)人無法同時(shí)完成除草的任務(wù)僻造,只有拿到鋤頭使用權(quán)的一方才能夠進(jìn)行除草憋他。這把鋤頭就像是解析器中的GIL,把小張跟小藍(lán)想象成被創(chuàng)建的兩個(gè)線程髓削,當(dāng)兩個(gè)人的工作效率一樣的時(shí)候竹挡,受限于鋤頭這個(gè)約束并無法同時(shí)進(jìn)行除草任務(wù),只能夠交替使用鋤頭立膛,本質(zhì)上并不會(huì)減少工作時(shí)間揪罕,反而會(huì)在換人的時(shí)候(上下文切換)耗費(fèi)掉一定的時(shí)間。

在一些場(chǎng)景下創(chuàng)建更多的線程并不能真正地減少程序的運(yùn)行時(shí)間宝泵,反而有可能會(huì)隨著進(jìn)程數(shù)量的增加而增加切換上下文的開銷好啰,從而導(dǎo)致程序變得更慢。

2) 多線程的作用

從前面的故事可以看出儿奶,由于GIL的存在框往,多線程并不能減少程序運(yùn)行的時(shí)間,反而會(huì)因?yàn)殚_的線程太多闯捎,導(dǎo)致上下文切換開銷變大椰弊,從而增加程序的運(yùn)行時(shí)間。那么多線程是否就沒有作用了瓤鼻?是不是Ruby的Thread類在實(shí)際場(chǎng)景下它就是個(gè)擺設(shè)秉版?

當(dāng)然不是,請(qǐng)大家看下面兩個(gè)場(chǎng)景:

場(chǎng)景1: 單線程坦克大戰(zhàn)

在不能運(yùn)用計(jì)算機(jī)多核的程序設(shè)計(jì)語言里面茬祷,我們每個(gè)時(shí)間點(diǎn)只有一個(gè)線程在工作清焕。回想我們小時(shí)后玩的坦克大戰(zhàn)的游戲祭犯。假設(shè)我們有坦克A秸妥, 坦克B。坦克A需要向上移動(dòng)100px沃粗,坦克B需要向右移動(dòng)300px筛峭。如果我們是單線程的話,那么只能夠等坦克A向上移動(dòng)100px之后陪每,坦克B才能向右移動(dòng)300px,這樣的游戲體驗(yàn)肯定是很糟糕的镰吵,這就是我們的串行開發(fā)所不能適用的場(chǎng)景檩禾,它的特點(diǎn)是必須要先完成一個(gè)任務(wù)然后再開始其他任務(wù)。

運(yùn)行起來大概就像下面這樣:

模擬單線程
場(chǎng)景2: 多線程坦克大戰(zhàn)

如果我們采用多線程實(shí)現(xiàn)方式便能夠很好地解決這種問題了疤祭。線程默認(rèn)由操作系統(tǒng)進(jìn)行調(diào)度盼产,在某個(gè)時(shí)間段內(nèi)這些線程幾乎是交替使用CPU。這樣就可能實(shí)現(xiàn)勺馆,在坦克A向右移動(dòng)一小段位置(可能是1~2px)后坦克B的線程占用了CPU戏售,坦克B得以向上移動(dòng)一小段位置侨核,如此循環(huán)反復(fù)直到任務(wù)完成。另外灌灾,由于兩個(gè)線程之間上下文切換是很快的用戶幾乎察覺不到中間的停頓搓译,因此可以給用戶造成一種“兩輛坦克同時(shí)移動(dòng)”的假象。

感覺就像下面這種行為:

模擬多線程

3) 多線程需要額外保障

我們前面也說了線程是輕量級(jí)的進(jìn)程锋喜,并且他們共享內(nèi)存空間些己,這會(huì)造成什么問題呢?

考慮下面這種場(chǎng)景:

假設(shè)如果我們?yōu)槟硞€(gè)操作創(chuàng)建了10個(gè)線程嘿般,而每個(gè)線程都會(huì)依賴于前一個(gè)線程的值段标。如果在一個(gè)線程任務(wù)處理過程中,系統(tǒng)把CPU讓給了另外線程炉奴,然而前面的線程任務(wù)只是處理了一半逼庞,其他線程無法享受這個(gè)線程的任務(wù)成果,這便可能導(dǎo)致最終結(jié)果與我們期望不符瞻赶。

這樣說可能有點(diǎn)迷糊赛糟,我舉個(gè)Ruby例子來說明一下這一點(diǎn)水援。

a = 0

threads = (1..10).map do |i|
  Thread.new(i) do |i|
    c = a
    sleep(rand(0..1))
    c += 10
    sleep(rand(0..1))
    a = c
  end
end

threads.each { |t| t.join }

puts a

這段代碼要實(shí)現(xiàn)的功能很簡(jiǎn)單筹淫,只是把變量a累加10次浮梢,每次加10拇颅,并且開了10個(gè)線程去完成這個(gè)任務(wù)校读。正常情況下我們期望的值是a == 100再登,然而事實(shí)卻是

> ruby a.rb
10

> ruby a.rb
10

> ruby a.rb
30

> ruby a.rb
20

怎么可能有這種隨機(jī)的值耕陷?如果不信的話你可以把程序拷貝到自己的電腦去運(yùn)行一下谜慌。出現(xiàn)這種情況的原因是字旭,當(dāng)我們的操作執(zhí)行到一半的時(shí)候其他線程介入了对湃,導(dǎo)致了數(shù)據(jù)混亂。這里為了突出問題遗淳,我們采用了sleep方法來把控制權(quán)讓給其他線程拍柒,而在現(xiàn)實(shí)中,線程間的上下文切換是由操作系統(tǒng)來調(diào)度屈暗,我們很難分析出它的具體行為拆讯。

我們減少線程數(shù)量來分析一下:

線程1執(zhí)行了c = a之后讓出了系統(tǒng)控制權(quán),然后線程二執(zhí)行了c = a; c += 10; a = c养叛,我們得到a == 10 && a == c种呐。 然而這個(gè)時(shí)候控制權(quán)讓回給線程1,它繼續(xù)往下執(zhí)行c += 10, a = c弃甥。因?yàn)樵诰€程1的上下文中c還是原來的數(shù)值0爽室,所以執(zhí)行這個(gè)操作之后我們會(huì)得到a == 10 && a == c,并且覆蓋了線程二的操作結(jié)果淆攻。因此最終我們無法得到我們心目中的值20阔墩。而且線程越多這個(gè)過程可能會(huì)越亂嘿架,更加難以分析。

為了解決這種問題啸箫,我們常用的方法是給某個(gè)代碼塊或者變量加鎖耸彪。它使得加鎖部分被一個(gè)線程訪問的時(shí)候不允許其他線程介入。修改后的代碼如下

a = 0
mutex = Mutex.new

threads = (1..10).map do |i|
  Thread.new(i) do |i|
    # 加鎖
    mutex.synchronize do
      c = a
      sleep(rand(0..1))
      c += 10
      sleep(rand(0..1))
      a = c
    end
  end
end

threads.each { |t| t.join }

puts a

加上鎖之后我們的運(yùn)行結(jié)果就能得到保證了

> ruby a.rb
100

> ruby a.rb
100

PS: 從前面的知識(shí)可以知道筐高,如果是只能運(yùn)用計(jì)算機(jī)單核所形成的并發(fā)操作其實(shí)并不能真正提高程序的運(yùn)行效率搜囱,反而會(huì)因?yàn)樯舷挛牡那袚Q而拖慢程序運(yùn)行速度,如果再加上鎖機(jī)制的話就更會(huì)增加程序運(yùn)行的開銷柑土。因此當(dāng)我們想使用多線程的時(shí)候要考慮到使用場(chǎng)景中并發(fā)的必要性蜀肘。這里是否真的需要多線程?我們需要多少線程稽屏?是否需要加鎖扮宠?會(huì)不會(huì)有更好的解決方案?

4) 協(xié)程

上面說了線程的種種問題狐榔,它是由系統(tǒng)調(diào)度的坛增,我們很難控制并預(yù)測(cè)它的行為。在不能夠充分利用計(jì)算機(jī)多核的情況下它們運(yùn)行起來可能比串行程序還要慢薄腻。那現(xiàn)在說一個(gè)高端點(diǎn)的線程-協(xié)程收捣。

協(xié)程也是線程的一種,但是它與傳統(tǒng)的線程還是有點(diǎn)區(qū)別庵楷,Ruby在1.9之后開始支持Fiber罢艾,使用它就可以很容易地寫出協(xié)程的程序。在某些場(chǎng)景下(如Web領(lǐng)域)協(xié)程會(huì)比線程更加適用尽纽。

我舉個(gè)比較簡(jiǎn)單的場(chǎng)景的去聊聊這個(gè)事情

小藍(lán)被布置了需要完成語文咐蚯,數(shù)學(xué),英語三門功課弄贿,預(yù)計(jì)每門功課需要一個(gè)小時(shí)的時(shí)間去完成春锋。那完成3門功課則大約需要3個(gè)小時(shí)。小藍(lán)可以采用下面兩種工作方式

系統(tǒng)調(diào)度線程的方式

如果使用系統(tǒng)默認(rèn)線程調(diào)度的方式來工作差凹,我們以5分鐘作為一個(gè)時(shí)間片段期奔,每5分鐘小藍(lán)就需要換一門功課去做,比如正在做著語文危尿,然后5分鐘之后會(huì)切換到數(shù)學(xué)能庆,或者英語,也有一定幾率繼續(xù)做語文脚线。做5分鐘后,又再次進(jìn)行切換弥搞。如此反復(fù)邮绿,直到3門功課都完成之后小藍(lán)就可以休息了渠旁。

這看起來有點(diǎn)瘋狂,如果你讓小藍(lán)按這樣的方式去寫作業(yè)的話船逮,估計(jì)不到3個(gè)小時(shí)他就已經(jīng)瘋了顾腊。另外,如此頻繁的上下文切換挖胃,最終所耗費(fèi)的時(shí)間肯定會(huì)大于3個(gè)小時(shí)杂靶。它的任務(wù)流程大概就像這樣子

多線程工作方式
協(xié)程的工作方式

從上面的工作方式可以看出,系統(tǒng)默認(rèn)的線程調(diào)度方式在有些場(chǎng)景下會(huì)過度消耗計(jì)算資源酱鸭,無故增加運(yùn)行時(shí)間吗垮,并且有點(diǎn)反人類。從人類的角度去考慮如何完成這3門功課凹髓,更人性化的做法會(huì)是先完成一門然后再去完成下一門烁登。我們或許可以把這個(gè)過程想象成一隊(duì)列。

這個(gè)過程就有點(diǎn)像線程之間可以相互協(xié)作來完成任務(wù)蔚舀,避免不必要的上下文切換饵沧,具體是否把控制權(quán)讓給其他線程,交給哪個(gè)線程赌躺,將由當(dāng)前這個(gè)線程來決定狼牺,在某種程度上可以減少了不必要的線程切換。工作流程大概像這樣

協(xié)程工作方式

Ruby1.9引入了Fiber后使得我們可以在代碼中使用協(xié)程礼患。下面是一個(gè)簡(jiǎn)單的例子

require 'fiber'
fiber1 = Fiber.new do
  puts "In Fiber 1"
  Fiber.yield
end

fiber2 = Fiber.new do
  puts "In Fiber 2"
  fiber1.transfer
  puts "Never see this message"
end

fiber3 = Fiber.new do
  puts "In Fiber 3"
end

fiber2.resume
fiber3.resume

Fiber是Ruby用于創(chuàng)建協(xié)程的類是钥,在正式調(diào)度之前我們先創(chuàng)建fiber1~3三個(gè)協(xié)程用例,然后在后面的代碼中對(duì)實(shí)例進(jìn)行調(diào)度讶泰。首先是fiber2被調(diào)度咏瑟,然后從fiber2內(nèi)部去把控制權(quán)轉(zhuǎn)移給fiber1, 故而"Never see this message"這條信息不會(huì)被打印痪署。最后fiber1在內(nèi)部把控制權(quán)轉(zhuǎn)交給主線程码泞,這個(gè)時(shí)候主線程繼續(xù)往下執(zhí)行,開始調(diào)度fiber3狼犯。最后的輸出結(jié)果是

In Fiber 2
In Fiber 1
In Fiber 3

咋一看這個(gè)例子似乎沒有什么特別余寥,雖然它可以讓我們很方便地調(diào)度三個(gè)協(xié)程實(shí)例,但是實(shí)際上這種控制流我們即便使用平時(shí)的串行程序也能夠?qū)崿F(xiàn)悯森,只需要定義三個(gè)方法宋舷,然后按照上面的順序去調(diào)用就行了。那我再舉一個(gè)例子來突出一下協(xié)程

fiber1 = Fiber.new do
  puts "fiber1 first resume"
  Fiber.yield
  puts "fiber1 second resume"
  Fiber.yield
  puts "fiber1 third resume"
end


fiber2 = Fiber.new do
  puts "fiber2 first resume"
  Fiber.yield
  puts "fiber2 second resume"
end

fiber1.resume #1
fiber2.resume #2
fiber2.resume #3
fiber1.resume #4
fiber1.resume #5

fiber1.resume #6

以上這段代碼會(huì)輸出什么瓢姻?答案是

fiber1 first resume
fiber2 first resume
fiber2 second resume
fiber1 second resume
fiber1 third resume
tread.rb:22:in `resume': dead fiber called (FiberError)
    from tread.rb:22:in `<main>'

代碼分析: 我們首先創(chuàng)建fiber1, fiber2這兩個(gè)實(shí)例祝蝠,先在主線程調(diào)度fiber1(代碼#1),fiber1輸出了第一次被調(diào)度的信息,然后通過Fiber#yield讓出控制權(quán)給主線程绎狭,主線程便繼續(xù)執(zhí)行代碼细溅,調(diào)度fiber2(代碼#2)。fiber2輸出了自己第一次被調(diào)度的信息之后就通過Fiber#yield讓出控制權(quán)給主線程儡嘶,主線程便繼續(xù)往下執(zhí)行代碼#3喇聊。fiber2再次被調(diào)度后輸出了自己第二次被調(diào)度的信息,同時(shí)它也運(yùn)行到代碼塊的末尾蹦狂,讓出了控制權(quán)誓篱。主線程接著執(zhí)行代碼#4, #5, #6。而#6這段代碼我特地空了一行凯楔,因?yàn)樵?5的時(shí)候fiber1已經(jīng)完成所有任務(wù)了窜骄,如果再次調(diào)度的話,我們會(huì)收到錯(cuò)誤信息啼辣,警告我們當(dāng)前被調(diào)度的協(xié)程已經(jīng)dead了啊研。

如果用串行代碼來實(shí)現(xiàn)上面這么詭異的控制流會(huì)有點(diǎn)困難吧,這也是協(xié)程的可貴之處鸥拧,線程之間似乎是在相互協(xié)作著一起工作党远。得益于這種特性我們可以寫出非租塞的,吞吐量更高的Web服務(wù)富弦。在Python領(lǐng)域就有一款叫Tornado的框架非彻涤椋火熱,便是以這種機(jī)制來實(shí)現(xiàn)的腕柜。

5) 真并行

既然官方版本的Ruby(MRI)會(huì)加上GIL導(dǎo)致我們沒有辦法活用CPU的多核济似,使得我們只能夠?qū)崿F(xiàn)并發(fā)操作,沒有辦法實(shí)現(xiàn)并行盏缤。既然GIL這么礙事砰蠢,拿掉不就好了?

回到最初的故事唉铜,如果小藍(lán)跟小張的除草任務(wù)不需要用鋤頭就能夠完成台舱,或者人手都有一把鋤頭的話,那么兩個(gè)人便能夠同時(shí)進(jìn)行除草任務(wù)了潭流,而不需要交替執(zhí)行竞惋。只要安排合理,勢(shì)必會(huì)比一個(gè)人完成除草任務(wù)更節(jié)省時(shí)間灰嫉,這便是并行的工作方式拆宛。

在Ruby的世界中確實(shí)存在一些已經(jīng)去除了GIL的實(shí)現(xiàn),其中包括Rubinius 以及 jruby他們的底層分別用的是C++以及Java實(shí)現(xiàn)的讼撒,除了去除GIL鎖之外浑厚,他們還做了其他方面的優(yōu)化股耽。某些場(chǎng)景下他們都有著比MRI更好的性能,更詳細(xì)比較可以猛戳這里钳幅。目前比較火的Rails服務(wù)器Puma也說到

Today, Puma runs on all Ruby implementations, but will always run best on any implementation that provides true parallelism.

或許得益于這些Ruby的實(shí)現(xiàn)豺谈,像Rails這些Web框架將會(huì)有更好的性能吧。

三. 尾聲

以上是我對(duì)線程高并發(fā)這些概念的一些簡(jiǎn)單總結(jié)贡这,如果有誤解的還望指正。

haha

PS: 所有的動(dòng)畫例子為了方便起見我都用JavaScript實(shí)現(xiàn)厂榛,代碼托管到這里盖矫,前端初學(xué)者可以上去看看具體實(shí)現(xiàn),前端高手請(qǐng)忽略击奶。

很感謝你能讀到這里辈双。

Happy Coding and writing!!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市柜砾,隨后出現(xiàn)的幾起案子湃望,更是在濱河造成了極大的恐慌,老刑警劉巖痰驱,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件证芭,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡担映,警方通過查閱死者的電腦和手機(jī)废士,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝇完,“玉大人官硝,你說我怎么就攤上這事《掏桑” “怎么了氢架?”我有些...
    開封第一講書人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)朋魔。 經(jīng)常有香客問我岖研,道長(zhǎng),這世上最難降的妖魔是什么铺厨? 我笑而不...
    開封第一講書人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任缎玫,我火速辦了婚禮,結(jié)果婚禮上解滓,老公的妹妹穿的比我還像新娘赃磨。我一直安慰自己,他們只是感情好洼裤,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開白布邻辉。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪值骇。 梳的紋絲不亂的頭發(fā)上莹菱,一...
    開封第一講書人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音吱瘩,去河邊找鬼道伟。 笑死,一個(gè)胖子當(dāng)著我的面吹牛使碾,可吹牛的內(nèi)容都是我干的蜜徽。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼票摇,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼拘鞋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起矢门,我...
    開封第一講書人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤盆色,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后祟剔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體隔躲,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年峡扩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蹭越。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡教届,死狀恐怖响鹃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情案训,我是刑警寧澤买置,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站强霎,受9級(jí)特大地震影響忿项,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜城舞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一轩触、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧家夺,春花似錦脱柱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽惨好。三九已至,卻和暖如春随闺,著一層夾襖步出監(jiān)牢的瞬間日川,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工矩乐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留龄句,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓散罕,卻偏偏與公主長(zhǎng)得像撒璧,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子笨使,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容