8、9萬買這3款合資車回家,連父母長輩都說好

4{icon} {views}

98-10。48萬豐田YARiS L致炫上市以來爭議最大的可能就是車頭的那兩撇“八字胡”,但是外觀這玩意,蘿蔔白菜各有所愛吧,只要是帶回了家的車,怎麼看都美。YARiS L致炫雖然沒有飛度那麼看似激進的動力表現,而且外觀爭議也一直存在,但是不得不說在配置方面還是很厚道的,全系標配ESp、上坡輔助、自動啟停,用車什麼最重要,肯定是安全啊,所以安全配置方面可不能沒有,我們在外拼搏,不能長期守在父母身邊,所以給爸媽選車安全要放在第一位,YARiS L致炫加上豐田的可靠口碑,買這車給父母用,就倆字,放心。

前两天五一小長假,小編還想提前買票回老家陪陪老爹老媽,哪知二老早就有了出遊計劃,而且特地交代不許我打擾他們的二人世界,說來也是,我們年輕人在外拼搏,一年到頭回不了幾趟家,父母喜歡旅遊是個好事,於是想着給二老添置一輛代步小車,既能遮風擋雨,想去周邊自駕也方便,跟二老商量后將車輛預算定位10萬元落地,最後選出了這三款合資小車,一起來看看。

廣汽本田-飛度

指導價:7.38-10.28萬

提起10萬元左右的代步小車,可能沒人不知道“國民超跑GK5”飛度了,上市至今更是得到了一大票年輕人的喜愛,但是誰說青春只是年輕人的專利呢,老媽偏偏最中意飛度。

這代飛度憑藉其出色的動力、同級內近乎無敵的空間、以及多樣的可玩性,在小型車陣營中可謂是鮮有敵手,每每提起10萬元購車清單中,總能見到飛度的身影。飛度小巧的車身,讓各種女神級的女司機也可以輕鬆地駕馭它,毫無壓力,本田家已經將空間玩到極致,自駕游路上需要的那些吃的喝的一股腦全裝進車廂,都說女人是視覺動物,一點都不假,小巧精緻的外觀加上青春的車身配色也最是討人歡心,開上它去自駕游,不怕老爹老媽不開心!

廣汽豐田-YARiS L致炫

指導價:6.98-10.48萬

豐田YARiS L致炫上市以來爭議最大的可能就是車頭的那兩撇“八字胡”,但是外觀這玩意,蘿蔔白菜各有所愛吧,只要是帶回了家的車,怎麼看都美!

YARiS L致炫雖然沒有飛度那麼看似激進的動力表現,而且外觀爭議也一直存在,但是不得不說在配置方面還是很厚道的,全系標配ESp、上坡輔助、自動啟停,用車什麼最重要,肯定是安全啊,所以安全配置方面可不能沒有,我們在外拼搏,不能長期守在父母身邊,所以給爸媽選車安全要放在第一位,YARiS L致炫加上豐田的可靠口碑,買這車給父母用,就倆字,放心!

東風日產-經典軒逸

指導價:9.98-11.48萬

看完前兩款年輕小鮮肉,最後這個算是中年大叔車型了,東風日產經典軒逸,從09款一直賣到現在的2018款,外觀幾乎沒有太大改變,但是都說了是給父母選車,它的出現也算符合老爸的氣質,而且老一輩人就覺得轎車沒有個車屁股怎麼能叫轎車呢!

2018款的軒逸終於擺脫了老舊的4AT自動變速箱,換裝了CVT變速箱,環保標準也從國四升到了國五,而且日產一貫的“大沙發”座椅以及發動機的靜音特性,所以舒適度方面表現還是不錯的,加上現在經典軒逸的優惠力度不小,是在父母10萬元購車單三款車中唯一的緊湊型三廂車,造型也更符合老一輩人對於轎車的定義,買給老爸開也就倆字,適合!

總結

給父母選車,更多的是從省心、舒適、燃油經濟性上面選擇,三款小車雖然沒有多強勁的動力表現,沒有太過驚艷的外觀設計,甚至有些車型還有一絲老氣,但是能夠為老爸老媽遮風擋雨,他們開着舒心就是最好的選擇,在外拼搏的小夥伴們,抽空多回家,買台舒心的小車送他們,陪他們一起自駕游才是他們最需要的!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

15萬買啥車?有轎車有SUV,這2款合資車可以放心買

8{icon} {views}

勁客的優勢在於成熟可靠的動力總成,採用1。5L自然吸氣發動機+CVT無級變速器的搭配能夠帶來非常平順線性的輸出,油門調校靈敏,能夠很好地應付日常市區內行駛,並且也非常省油,可靠性較高。不過其檔次感與配置水平不如哈弗H2。

15萬左右比較推薦的是本田XR-V與豐田卡羅拉,這兩款車目前在市場上都有着不錯的銷量、保值率以及口碑,XR-V在小型SUV市場混得風生水起,靠的就是其成熟的動力系統和越級的空間表現,家用是個非常棒的選擇。而卡羅拉乃全球銷量神車,中庸就是其最大的武器,除了汽油版本外還有雙擎版本可選,擁有非常優秀的燃油經濟性,能夠滿足許許多多的消費者,除了隔音濾振差點,其它方面的表現都非常不錯,乃家用車的不二之選。

一輛是接近中型車的緊湊型車,一輛是標準的中型車,凌渡280DSG豪華版擁有更加豐富的配置,不過因為其比較扁平的設計所以頭部空間表現一般,而君威中型車的身份自然擁有更大的空間以及更高的檔次感,同時全系標配9AT也讓其競爭力進一步上升,採用的1.5T發動機也有着不錯的動力輸出,因此更推薦君威1.5T中配。

510採用的是6擋手動變速器和5擋AMT變速器,假設會開手動擋的話更加推薦手動車型,6個擋位在高速行駛時把轉速壓得更低,能帶來更好的燃油經濟性,同時擁有更高的傳動效率及可靠性,510的手動擋換擋手感不錯,有吸入感並且行程不長,離合器的力度也不沉,沒那麼容易疲勞。而AMT變速器雖然說省去了踩離合器的麻煩,但是其換擋邏輯不清晰,而且頓挫比較嚴重,尤其是在起步階段或者是在堵車狀況下,因此更加推薦510的手動版車型。

兩車都是定位小型SUV,哈弗H2擁有更加親民的售價,更加大氣上檔次的外觀內飾設計,同時配置更加豐富,採用的1.5T發動機+7擋雙離合的搭配,擁有不錯的爆發力,但是雙離合變速器的邏輯有待提高,並且油耗也會偏高。

勁客的優勢在於成熟可靠的動力總成,採用1.5L自然吸氣發動機+CVT無級變速器的搭配能夠帶來非常平順線性的輸出,油門調校靈敏,能夠很好地應付日常市區內行駛,並且也非常省油,可靠性較高。不過其檔次感與配置水平不如哈弗H2。

綜上,假如你預算充足的話,更加建議購買勁客的中配以上車型,而預算不是很足的話,那麼家用選擇H2是一個非常具有性價比的選擇。

2018款飛度新增了運動套件車型,不過在配置方面還是一如既往的寒酸,比較推薦指導價為8.88萬的1.5L CVT潮跑版,在配置上面擁有主/副駕駛座安全氣囊、運動外觀套件、行車電腦显示屏、前霧燈等,類似比較常用的电子車身穩定系統、駐車雷達要到頂配才配備,但是飛度的動力和空間還是非常不錯的,1.5L自吸發動機就能爆發出131匹馬力,日常市區駕駛毫無壓力,加上本田“MM”理念,讓它的空間能夠滿足大多數人的使用需求,市面上大量的改裝件也能讓每位飛度車主把愛車改成獨一無二的樣子。

以上就是本期網友問答欄目的全部內容,假如你也想上牆的話,點擊下方留言留下你的問題並且點個贊,就有機會在下期欄目看見你的身影!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

「世界糧食計劃署」獲諾貝爾和平獎 致力解決糧食危機

3{icon} {views}

摘錄自2020年10月11日TVBS新聞網報導

2020年諾貝爾和平獎,得主是「世界糧食計劃署」,這個隸屬聯合國組織,負責解救饑荒,是全球規模最大的人道救援團體之一;只是今年全球陸續出現疫情、天災和戰亂,聯合國秘書長古特瑞斯就說全球貧窮人口在30年內第一次增加,全球糧食安全正備受考驗。

2020年諾貝爾和平獎,給了聯合國旗下的世界糧食計劃署,挪威諾貝爾委員會指出,世糧署致力對抗飢餓問題,也為改善戰亂地區的和平做出貢獻。

只是今年不僅碰上百年大疫情,更是極端天災、蝗蟲肆虐的一年,糧食供應面臨前所未有的危機,聯合國警告,今年恐怕會有上億人餓死。

大陸長江流域,今年暴雨洪水災情不斷,讓「魚米之鄉」的南方農田泡水,小麥欠收1000萬噸,影響大陸4成人口主食。美國從今年8月以來,西岸野火延燒超過500萬英畝土地,相當於燒掉75個台北市,規模空前。

挪威諾貝爾委員會主席瑞斯.安德森:「促使我們今年選擇,讓世糧署獲獎的因素之一,是因為飢餓人口正在升高,和戰爭與衝突有關的飢餓在增加。」

循環經濟
國際新聞
聯合國
世界糧食計劃署
諾貝爾和平獎

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

中國特供不能買?14萬起這幾款SUV比途觀、漢蘭達更值!

4{icon} {views}

98萬將空間玩到了極致本田聞名於世的除了讓人津津樂道的性能與紅頭髮動機之外,對於車內空間的營造有着極為深度的造詣也是鍾愛本田品牌的家用車車主們非常欣賞的一點品牌特性。而這一點在冠道上被發揮得淋漓盡致。當所有主機廠都在用相似的尺寸做7座SUV的時候,冠道依舊是以5座布局應對市場競爭,而也正是這5座的座椅布局讓它成為了合資品牌中型SUV陣列里後排空間鮮有對手的存在,寬敞的後排甚至可以用誇張來形容,這對於後排有着執念的中國消費者來說,冠道的乘坐空間是非常具有誘惑力的存在。

提到“中國特供”,似乎很多人對這個字眼都頗有微詞;很大一部分原因都是因為中國汽車消費市場上銷量高的車型有不少的身份是“中國特供”,而某些車型,由於技術平台較老的緣故讓部分消費者將它們扣上了一些“高價低能”的帽子。

其實特供車型是每一個成型的汽車消費市場中都存在的一個現象;例如日本的K-car,小尺寸低排量的設定與方盒子的造型,便是為了日本那寸土寸金的地方量身打造的代步工具。

又例如美國的皮卡和大尺寸、大排量的SUV以及轎車,廣袤的美利堅平原土地上的牧場主們更需要一款能載貨的車型為自己的畜牧事業運輸物料,加之他們的汽車文化發展就是讓多缸數大排量的車型唱着主調,所以美系車自然成了“大尺寸”的汽車代名詞。

所以不同品牌的特供車不過是主機廠為了可以更好的開拓一方水土,鞏固自己品牌效應的一種商業手段,其實某種情況上看,做好一款特供車並不簡單,畢竟需要花大量的時間進行市場調研和消費需求的考究,才可以做出一款能幫助主機廠在某個特定的市場環境下能帶來豐厚利潤的車型。

近年來中國市場上的特供新車越來越多,而以下這些中國特供的SUV可以說是每一台都非常值得推薦的車型,它們在不同的產品定位下保持了品牌在各自擅長領域中的優勢,極大程度的滿足了中國消費者的用車需求。

寶馬X1

指導售價:28.38-43.90萬

空間與品牌信仰為它贏得了市場

寶馬X1現如今的銷量能有個平均月銷量6000輛以上的水平,豪華品牌緊湊型SUV有這個數據已經相當不錯,而且現款的X1比奧迪Q3賣得要更好,也側面印證了它的市場效應已經成型,消費者對於這款UKL前置前驅平台下的SUV已經有了極大的包容度。

起初,UKL這三個字母幾乎讓寶馬成為了眾矢之的,認為寶馬標的汽車就不應該出現前置前驅這種喪失操控源泉的驅動形式,但其實大多數人對於寶馬的定義或許更多的是停留在寶馬的品牌效應上,其次,UKL前驅平台的寶馬X1不僅做出了比老款X1更大的尺寸,更挖掘出了相當寬敞的後排空間,讓X1的形象更接近一個買菜家用的角色定位,對於大多數人來說,這樣的寶馬X1其實更實在。

馬自達CX-4

指導售價:14.08-21.58萬

年輕人的需求得以很好的滿足

嚴格意義上說,CX-4應該是一款中國首發的SUV;這款運動風格濃郁的跨界車型讓它看上去並不像傳統意義上的SUV那般厚重,靈動的造型和飄逸的魂動設計線條讓它在年輕人心目中有着極高的認同度。

在產品力層面,CX-4保持了馬自達品牌一貫出色的操控性,雖然空間並不是強項,但是作為一般年輕家庭來說空間實用性已然足夠,而且在市場終端显示出了一個接受度非常高的售價,這讓想要購買一款高顏值SUV的年輕人看到了希望,也讓不少在轎車與SUV中找不到平衡點的消費者們有了一個十分合適的折中選擇。

冠道

指導售價:22.00-32.98萬

將空間玩到了極致

本田聞名於世的除了讓人津津樂道的性能與紅頭髮動機之外,對於車內空間的營造有着極為深度的造詣也是鍾愛本田品牌的家用車車主們非常欣賞的一點品牌特性;而這一點在冠道上被發揮得淋漓盡致。

當所有主機廠都在用相似的尺寸做7座SUV的時候,冠道依舊是以5座布局應對市場競爭,而也正是這5座的座椅布局讓它成為了合資品牌中型SUV陣列里後排空間鮮有對手的存在,寬敞的後排甚至可以用誇張來形容,這對於後排有着執念的中國消費者來說,冠道的乘坐空間是非常具有誘惑力的存在。

昂科威

指導售價:21.99-31.99萬

將豪華融進了平民層次

二十來萬要買合資SUV買出豪華質感?除了昂科威之外可能能叫得上號的車型並不多,別克品牌近年來在內飾檔次感的提升工程上可謂是下足了功夫,而昂科威這款誕生於中國返銷至美國的SUV更是如此,內飾氛圍無論是在設計或是用料層面都在同價位車型中處於上乘水準,並且擁有主動降噪技術以進一步提升昂科威在行駛狀況下的高檔質感。

將豪華融進二三十萬級別車型,這一舉措無疑是成功的,昂科威上市以後便獲得了非常可觀的銷量數據,敦實厚重的外觀設計符合大多數人對於家用SUV的認知,而實際使用體驗越級的檔次感又能讓消費者感受到相對優越的用車氛圍,也正是昂科威出色的產品力體現,所以它一直處於十分值得推薦購買的SUV名錄里。

大指揮官

指導售價:27.98-40.98萬

極大程度上滿足了國人喜好

大指揮官剛剛上市,從理性角度上說,並不應該這麼快就下定論它是一款值得推薦的車型,但是它呈現出來的市場決心非常明顯,上市之前便已經放出“我便是為中國市場而來”的消息,直擊漢蘭達,銳界,途昂的細分定位。

大指揮官的產品力幾乎滿足了國人對於一款35萬左右能買到的合資SUV應該呈現出來的產品力該有的認知:接近5米的車長,魁梧的身軀,平實近人的外觀設計,7座的布局,以及幾乎全面的配置水平,這些都符合一个中國家庭用戶對於中型SUV的嚴苛需求,起碼從產品力上看,大指揮官交出的答卷絕對值高分。

結語:就普通消費者而言,一輛車是否特供並不是衡量這輛車好與壞的標準;而是要看綜合產品力的體現是否符合它所特供的市場環境;中國汽車市場對於SUV的空間與配置需求看得更重,而對動力、操控等机械品質的要求所佔比重則要低於前者。

以上車型除了馬自達CX-4是一款更講究操控性能的SUV以外,其餘車型幾乎都是將空間與配置放在了車型研發時的首位,從關注度和銷量上看,這些車型無疑是成功的代表;所以特供車不值得買嗎?我看未必。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

深度解密 Go 語言之 sync.map

3{icon} {views}

工作中,經常會碰到併發讀寫 map 而造成 panic 的情況,為什麼在併發讀寫的時候,會 panic 呢?因為在併發讀寫的情況下,map 里的數據會被寫亂,之後就是 Garbage in, garbage out,還不如直接 panic 了。

目錄

  • 是什麼
  • 有什麼用
  • 如何使用
  • 源碼分析
    • 數據結構
    • Store
    • Load
    • Delete
    • LoadOrStore
    • Range
  • 其他
  • 總結
  • 參考資料

是什麼

Go 語言原生 map 並不是線程安全的,對它進行併發讀寫操作的時候,需要加鎖。而 sync.map 則是一種併發安全的 map,在 Go 1.9 引入。

sync.map 是線程安全的,讀取,插入,刪除也都保持着常數級的時間複雜度。
sync.map 的零值是有效的,並且零值是一個空的 map。在第一次使用之後,不允許被拷貝。

有什麼用

一般情況下解決併發讀寫 map 的思路是加一把大鎖,或者把一個 map 分成若干個小 map,對 key 進行哈希,只操作相應的小 map。前者鎖的粒度比較大,影響效率;後者實現起來比較複雜,容易出錯。

而使用 sync.map 之後,對 map 的讀寫,不需要加鎖。並且它通過空間換時間的方式,使用 read 和 dirty 兩個 map 來進行讀寫分離,降低鎖時間來提高效率。

如何使用

使用非常簡單,和普通 map 相比,僅遍歷的方式略有區別:

package main

import (
	"fmt"
	"sync"
)

func main()  {
	var m sync.Map
	// 1. 寫入
	m.Store("qcrao", 18)
	m.Store("stefno", 20)

	// 2. 讀取
	age, _ := m.Load("qcrao")
	fmt.Println(age.(int))

	// 3. 遍歷
	m.Range(func(key, value interface{}) bool {
		name := key.(string)
		age := value.(int)
		fmt.Println(name, age)
		return true
	})

	// 4. 刪除
	m.Delete("qcrao")
	age, ok := m.Load("qcrao")
	fmt.Println(age, ok)

	// 5. 讀取或寫入
	m.LoadOrStore("stefno", 100)
	age, _ = m.Load("stefno")
	fmt.Println(age)
}

第 1 步,寫入兩個 k-v 對;

第 2 步,使用 Load 方法讀取其中的一個 key;

第 3 步,遍歷所有的 k-v 對,並打印出來;

第 4 步,刪除其中的一個 key,再讀這個 key,得到的就是 nil;

第 5 步,使用 LoadOrStore,嘗試讀取或寫入 “Stefno”,因為這個 key 已經存在,因此寫入不成功,並且讀出原值。

程序輸出:

18
stefno 20
qcrao 18
<nil> false
20

sync.map 適用於讀多寫少的場景。對於寫多的場景,會導致 read map 緩存失效,需要加鎖,導致衝突變多;而且由於未命中 read map 次數過多,導致 dirty map 提升為 read map,這是一個 O(N) 的操作,會進一步降低性能。

源碼分析

數據結構

先來看下 map 的數據結構。去掉大段的註釋后:

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry
	misses int
}

互斥量 mu 保護 read 和 dirty。

read 是 atomic.Value 類型,可以併發地讀。但如果需要更新 read,則需要加鎖保護。對於 read 中存儲的 entry 字段,可能會被併發地 CAS 更新。但是如果要更新一個之前已被刪除的 entry,則需要先將其狀態從 expunged 改為 nil,再拷貝到 dirty 中,然後再更新。

dirty 是一個非線程安全的原始 map。包含新寫入的 key,並且包含 read 中的所有未被刪除的 key。這樣,可以快速地將 dirty 提升為 read 對外提供服務。如果 dirty 為 nil,那麼下一次寫入時,會新建一個新的 dirty,這個初始的 dirtyread 的一個拷貝,但除掉了其中已被刪除的 key。

每當從 read 中讀取失敗,都會將 misses 的計數值加 1,當加到一定閾值以後,需要將 dirty 提升為 read,以期減少 miss 的情形。

read mapdirty map 的存儲方式是不一致的。
前者使用 atomic.Value,後者只是單純的使用 map。
原因是 read map 使用 lock free 操作,必須保證 load/store 的原子性;而 dirty map 的 load+store 操作是由 lock(就是 mu)來保護的。

真正存儲 key/value 的是 read 和 dirty 字段。read 使用 atomic.Value,這是 lock-free 的基礎,保證 load/store 的原子性。dirty 則直接用了一個原始的 map,對於它的 load/store 操作需要加鎖。

read 字段里實際上是存儲的是:

// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
	m       map[interface{}]*entry
	amended bool // true if the dirty map contains some key not in m.
}

注意到 read 和 dirty 里存儲的東西都包含 entry,來看一下:

type entry struct {
	p unsafe.Pointer // *interface{}
}

很簡單,它是一個指針,指向 value。看來,read 和 dirty 各自維護一套 key,key 指向的都是同一個 value。也就是說,只要修改了這個 entry,對 read 和 dirty 都是可見的。這個指針的狀態有三種:

p == nil 時,說明這個鍵值對已被刪除,並且 m.dirty == nil,或 m.dirty[k] 指向該 entry。

p == expunged 時,說明這條鍵值對已被刪除,並且 m.dirty != nil,且 m.dirty 中沒有這個 key。

其他情況,p 指向一個正常的值,表示實際 interface{} 的地址,並且被記錄在 m.read.m[key] 中。如果這時 m.dirty 不為 nil,那麼它也被記錄在 m.dirty[key] 中。兩者實際上指向的是同一個值。

當刪除 key 時,並不實際刪除。一個 entry 可以通過原子地(CAS 操作)設置 p 為 nil 被刪除。如果之後創建 m.dirty,nil 又會被原子地設置為 expunged,且不會拷貝到 dirty 中。

如果 p 不為 expunged,和 entry 相關聯的這個 value 可以被原子地更新;如果 p == expunged,那麼僅當它初次被設置到 m.dirty 之後,才可以被更新。

整體用一張圖來表示:

Store

先來看 expunged:

var expunged = unsafe.Pointer(new(interface{}))

它是一個指向任意類型的指針,用來標記從 dirty map 中刪除的 entry。

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
	// 如果 read map 中存在該 key  則嘗試直接更改(由於修改的是 entry 內部的 pointer,因此 dirty map 也可見)
	read, _ := m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok && e.tryStore(&value) {
		return
	}

	m.mu.Lock()
	read, _ = m.read.Load().(readOnly)
	if e, ok := read.m[key]; ok {
		if e.unexpungeLocked() {
			// 如果 read map 中存在該 key,但 p == expunged,則說明 m.dirty != nil 並且 m.dirty 中不存在該 key 值 此時:
			//    a. 將 p 的狀態由 expunged  更改為 nil
			//    b. dirty map 插入 key
			m.dirty[key] = e
		}
		// 更新 entry.p = value (read map 和 dirty map 指向同一個 entry)
		e.storeLocked(&value)
	} else if e, ok := m.dirty[key]; ok {
		// 如果 read map 中不存在該 key,但 dirty map 中存在該 key,直接寫入更新 entry(read map 中仍然沒有這個 key)
		e.storeLocked(&value)
	} else {
		// 如果 read map 和 dirty map 中都不存在該 key,則:
		//	  a. 如果 dirty map 為空,則需要創建 dirty map,並從 read map 中拷貝未刪除的元素到新創建的 dirty map
		//    b. 更新 amended 字段,標識 dirty map 中存在 read map 中沒有的 key
		//    c. 將 kv 寫入 dirty map 中,read 不變
		if !read.amended {
		    // 到這裏就意味着,當前的 key 是第一次被加到 dirty map 中。
			// store 之前先判斷一下 dirty map 是否為空,如果為空,就把 read map 淺拷貝一次。
			m.dirtyLocked()
			m.read.Store(readOnly{m: read.m, amended: true})
		}
		// 寫入新 key,在 dirty 中存儲 value
		m.dirty[key] = newEntry(value)
	}
	m.mu.Unlock()
}

整體流程:

  1. 如果在 read 里能夠找到待存儲的 key,並且對應的 entry 的 p 值不為 expunged,也就是沒被刪除時,直接更新對應的 entry 即可。
  2. 第一步沒有成功:要麼 read 中沒有這個 key,要麼 key 被標記為刪除。則先加鎖,再進行後續的操作。
  3. 再次在 read 中查找是否存在這個 key,也就是 double check 一下,這也是 lock-free 編程里的常見套路。如果 read 中存在該 key,但 p == expunged,說明 m.dirty != nil 並且 m.dirty 中不存在該 key 值 此時: a. 將 p 的狀態由 expunged 更改為 nil;b. dirty map 插入 key。然後,直接更新對應的 value。
  4. 如果 read 中沒有此 key,那就查看 dirty 中是否有此 key,如果有,則直接更新對應的 value,這時 read 中還是沒有此 key。
  5. 最後一步,如果 read 和 dirty 中都不存在該 key,則:a. 如果 dirty 為空,則需要創建 dirty,並從 read 中拷貝未被刪除的元素;b. 更新 amended 字段,標識 dirty map 中存在 read map 中沒有的 key;c. 將 k-v 寫入 dirty map 中,read.m 不變。最後,更新此 key 對應的 value。

再來看一些子函數:

// 如果 entry 沒被刪,tryStore 存儲值到 entry 中。如果 p == expunged,即 entry 被刪,那麼返回 false。
func (e *entry) tryStore(i *interface{}) bool {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
			return true
		}
	}
}

tryStore 在 Store 函數最開始的時候就會調用,是比較常見的 for 循環加 CAS 操作,嘗試更新 entry,讓 p 指向新的值。

unexpungeLocked 函數確保了 entry 沒有被標記成已被清除:

// unexpungeLocked 函數確保了 entry 沒有被標記成已被清除。
// 如果 entry 先前被清除過了,那麼在 mutex 解鎖之前,它一定要被加入到 dirty map 中
func (e *entry) unexpungeLocked() (wasExpunged bool) {
	return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

Load

func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 如果沒在 read 中找到,並且 amended 為 true,即 dirty 中存在 read 中沒有的 key
	if !ok && read.amended {
		m.mu.Lock() // dirty map 不是線程安全的,所以需要加上互斥鎖
		// double check。避免在上鎖的過程中 dirty map 提升為 read map。
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		// 仍然沒有在 read 中找到這個 key,並且 amended 為 true
		if !ok && read.amended {
			e, ok = m.dirty[key] // 從 dirty 中找
			// 不管 dirty 中有沒有找到,都要"記一筆",因為在 dirty 提升為 read 之前,都會進入這條路徑
			m.missLocked()
		}
		m.mu.Unlock()
	}
	if !ok { // 如果沒找到,返回空,false
		return nil, false
	}
	return e.load()
}

處理路徑分為 fast path 和 slow path,整體流程如下:

  1. 首先是 fast path,直接在 read 中找,如果找到了直接調用 entry 的 load 方法,取出其中的值。
  2. 如果 read 中沒有這個 key,且 amended 為 fase,說明 dirty 為空,那直接返回 空和 false。
  3. 如果 read 中沒有這個 key,且 amended 為 true,說明 dirty 中可能存在我們要找的 key。當然要先上鎖,再嘗試去 dirty 中查找。在這之前,仍然有一個 double check 的操作。若還是沒有在 read 中找到,那麼就從 dirty 中找。不管 dirty 中有沒有找到,都要”記一筆”,因為在 dirty 被提升為 read 之前,都會進入這條路徑

這裏主要看下 missLocked 的函數的實現:

func (m *Map) missLocked() {
	m.misses++
	if m.misses < len(m.dirty) {
		return
	}
	// dirty map 晉陞
	m.read.Store(readOnly{m: m.dirty})
	m.dirty = nil
	m.misses = 0
}

直接將 misses 的值加 1,表示一次未命中,如果 misses 值小於 m.dirty 的長度,就直接返回。否則,將 m.dirty 晉陞為 read,並清空 dirty,清空 misses 計數值。這樣,之前一段時間新加入的 key 都會進入到 read 中,從而能夠提升 read 的命中率。

再來看下 entry 的 load 方法:

func (e *entry) load() (value interface{}, ok bool) {
	p := atomic.LoadPointer(&e.p)
	if p == nil || p == expunged {
		return nil, false
	}
	return *(*interface{})(p), true
}

對於 nil 和 expunged 狀態的 entry,直接返回 ok=false;否則,將 p 轉成 interface{} 返回。

Delete

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
	read, _ := m.read.Load().(readOnly)
	e, ok := read.m[key]
	// 如果 read 中沒有這個 key,且 dirty map 不為空
	if !ok && read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		e, ok = read.m[key]
		if !ok && read.amended {
			delete(m.dirty, key) // 直接從 dirty 中刪除這個 key
		}
		m.mu.Unlock()
	}
	if ok {
		e.delete() // 如果在 read 中找到了這個 key,將 p 置為 nil
	}
}

可以看到,基本套路還是和 Load,Store 類似,都是先從 read 里查是否有這個 key,如果有則執行 entry.delete 方法,將 p 置為 nil,這樣 read 和 dirty 都能看到這個變化。

如果沒在 read 中找到這個 key,並且 dirty 不為空,那麼就要操作 dirty 了,操作之前,還是要先上鎖。然後進行 double check,如果仍然沒有在 read 里找到此 key,則從 dirty 中刪掉這個 key。但不是真正地從 dirty 中刪除,而是更新 entry 的狀態。

來看下 entry.delete 方法:

func (e *entry) delete() (hadValue bool) {
	for {
		p := atomic.LoadPointer(&e.p)
		if p == nil || p == expunged {
			return false
		}
		if atomic.CompareAndSwapPointer(&e.p, p, nil) {
			return true
		}
	}
}

它真正做的事情是將正常狀態(指向一個 interface{})的 p 設置成 nil。沒有設置成 expunged 的原因是,當 p 為 expunged 時,表示它已經不在 dirty 中了。這是 p 的狀態機決定的,在 tryExpungeLocked 函數中,會將 nil 原子地設置成 expunged。

tryExpungeLocked 是在新創建 dirty 時調用的,會將已被刪除的 entry.p 從 nil 改成 expunged,這個 entry 就不會寫入 dirty 了。

func (e *entry) tryExpungeLocked() (isExpunged bool) {
	p := atomic.LoadPointer(&e.p)
	for p == nil {
		// 如果原來是 nil,說明原 key 已被刪除,則將其轉為 expunged。
		if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
			return true
		}
		p = atomic.LoadPointer(&e.p)
	}
	return p == expunged
}

注意到如果 key 同時存在於 read 和 dirty 中時,刪除只是做了一個標記,將 p 置為 nil;而如果僅在 dirty 中含有這個 key 時,會直接刪除這個 key。原因在於,若兩者都存在這個 key,僅做標記刪除,可以在下次查找這個 key 時,命中 read,提升效率。若只有在 dirty 中存在時,read 起不到“緩存”的作用,直接刪除。

LoadOrStore

這個函數結合了 Load 和 Store 的功能,如果 map 中存在這個 key,那麼返回這個 key 對應的 value;否則,將 key-value 存入 map。這在需要先執行 Load 查看某個 key 是否存在,之後再更新此 key 對應的 value 時很有效,因為 LoadOrStore 可以併發執行。

具體的過程不再一一分析了,可參考 Load 和 Store 的源碼分析。

Range

Range 的參數是一個函數:

f func(key, value interface{}) bool

由使用者提供實現,Range 將遍歷調用時刻 map 中的所有 k-v 對,將它們傳給 f 函數,如果 f 返回 false,將停止遍歷。

func (m *Map) Range(f func(key, value interface{}) bool) {
	read, _ := m.read.Load().(readOnly)
	if read.amended {
		m.mu.Lock()
		read, _ = m.read.Load().(readOnly)
		if read.amended {
			read = readOnly{m: m.dirty}
			m.read.Store(read)
			m.dirty = nil
			m.misses = 0
		}
		m.mu.Unlock()
	}

	for k, e := range read.m {
		v, ok := e.load()
		if !ok {
			continue
		}
		if !f(k, v) {
			break
		}
	}
}

當 amended 為 true 時,說明 dirty 中含有 read 中沒有的 key,因為 Range 會遍歷所有的 key,是一個 O(n) 操作。將 dirty 提升為 read,會將開銷分攤開來,所以這裏直接就提升了。

之後,遍歷 read,取出 entry 中的值,調用 f(k, v)。

其他

關於為何 sync.map 沒有 Len 方法,參考資料里給出了 issue,bcmills 認為對於併發的數據結構和非併發的數據結構並不一定要有相同的方法。例如,map 有 Len 方法,sync.map 卻不一定要有。就像 sync.map 有 LoadOrStore 方法,map 就沒有一樣。

有些實現增加了一個計數器,並原子地增加或減少它,以此來表示 sync.map 中元素的個數。但 bcmills 提出這會引入競爭:atomic 並不是 contention-free 的,它只是把競爭下沉到了 CPU 層級。這會給其他不需要 Len 方法的場景帶來負擔。

總結

  1. sync.map 是線程安全的,讀取,插入,刪除也都保持着常數級的時間複雜度。

  2. 通過讀寫分離,降低鎖時間來提高效率,適用於讀多寫少的場景。

  3. Range 操作需要提供一個函數,參數是 k,v,返回值是一個布爾值:f func(key, value interface{}) bool

  4. 調用 Load 或 LoadOrStore 函數時,如果在 read 中沒有找到 key,則會將 misses 值原子地增加 1,當 misses 增加到和 dirty 的長度相等時,會將 dirty 提升為 read。以期減少“讀 miss”。

  5. 新寫入的 key 會保存到 dirty 中,如果這時 dirty 為 nil,就會先新創建一個 dirty,並將 read 中未被刪除的元素拷貝到 dirty。

  6. 當 dirty 為 nil 的時候,read 就代表 map 所有的數據;當 dirty 不為 nil 的時候,dirty 才代表 map 所有的數據。

參考資料

【德志大佬-設計併發安全的 map】https://halfrost.com/go_map_chapter_one/

【德志大佬-設計併發安全的 map】https://halfrost.com/go_map_chapter_two/

【關於 sync.map 為什麼沒有 len 方法的 issue】https://github.com/golang/go/issues/20680

【芮神增加了 len 方法】http://xiaorui.cc/archives/4972

【圖解 map 操作】https://wudaijun.com/2018/02/go-sync-map-implement/

【從一道面試題開始】https://segmentfault.com/a/1190000018657984

【源碼分析】https://zhuanlan.zhihu.com/p/44585993

【行文通暢,流程圖清晰】https://juejin.im/post/5d36a7cbf265da1bb47da444

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

日再傳禽流感疫情 東香川市將撲殺4.8萬隻雞

1{icon} {views}

摘錄自2020年11月08日中央通訊社日本報導

日本香川縣養雞場5日發生高致死率的H5N8高病原性禽流感疫情,正在撲殺場內約33萬隻雞,今天又傳出同縣另一處養雞場也發現H5高病原性禽流感,將撲殺場內約4萬8000隻雞。

專家研判病毒可能經由西伯利亞進入日本,現階段傳染給人類的可能性很低。由於短期內出現第2例禽流感疫情,縣府為避免疫情擴散,已將東香川市這處養雞場半徑3公里範圍,列為禁止移動雞隻與雞蛋的「移動限制區域」,並將半徑10公里內列為禁止出貨的「搬出限制區域」。

東香川市這處養雞場,距三豐市發生疫情養雞場約50公里,農林水產省今天(8日)已派遣專家小組前往東香川市,與地方政府就傳染途徑等進行詳細調查。

生物多樣性
國際新聞
香川縣
日本
H5N8
禽流感

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

東協化學品法規因應策略與近況更新

2{icon} {views}

轉載自2018年11月12日國際化學品政策宣導網

自台灣新南向政策上路以來,對於東協的投資持續增加,根據經濟部國貿局近5年(至2016年)的資料顯示,我國對新加坡的投資占比最高(47%),其次為越南(32%)。

根據2017年的財政部統計資料,東協為台灣化學品及塑膠與橡膠相關製品世界出口第二大市場,而化學品管理為國際各產業貿易中須關注的重要的法規,今年10月22日經濟部工業局主辦之「東協化學品管理因應策略宣導會」,特別邀請到新加坡化學工業產業協會(Singapore Chemical Industry Council Limited,SCIC)兩位資深業界講師,針對新加坡及東協國家化學品管理法規的進展及化學品全球調和制度(GHS)之管理機制進行說明。

新加坡以生命週期觀念建立化學管理框架

講師分享新加坡化學管理框架,主要由各主管機關分別對管轄範圍內危險化學品進行管理,包含化學品生命週期的不同階段,針對產品初期發展、製造、運輸、儲存、販賣、使用、以及最後廢棄的生命週期進行管理。

例如進口時,儲存和運輸皆須進行申報及許可申請,且連結海關的進口資料作必要的管控和追蹤。而針對危害性化學品,新加坡民防部隊即要求在進口、儲存、運輸以及建造用途時申請執照。而新加坡國家環境局(National Environment Agency)也要求購買危害性化學品時須申請許可,並進行正確的標示。

另外為符合水俣公约的要求,新加坡在2020年起也將禁止特定含水銀產品的製造、輸入及出口,例如電池、化妝品、節能燈泡和某些醫​​療設備等。

新加坡GHS制度落實與業界合作 在GHS制度落實方面,講師除了說明新加坡對於GHS標示的要求,也提到當廠場現場中完整的GHS標示在實務上不可行時,應將標示資訊簡化,降低過多複雜的資訊展示,但此規定並不適用需要運送到其他廠場時的標示。

另外在出口方面,進口後再重新出口、以及儲存將出口的國內製造之危害性化學品時,都依相關規定進行標示,並符合出口目的國家之法規要求。其中更令人驚豔的是講師分享之業界與新加坡政府於法規制訂時之技術合作,彼此透過密集的諮詢與業界經驗分享,取得法規與經濟發展之平衡之作法,亦是台灣可積極參考之作法。

其他東協國家管理規範持續發展

講師也針對其他東協國家化學品管理規範最新進度進行介紹,摘要如下:泰國正在建立第一份既有化學物質清單,預計會於2020年公布; 越南於2017年11月已正式實施化學法法令,透過清單進行工業及前驅化學品的管理規範,目前也正在建立化學物質清單; 柬埔寨、寮國、緬甸目前仍著重於GHS的執行; 菲律賓則持續檢視預製造及預進口的新化學物質通報規範,以及其他管制清單; 馬來西亞與印尼目前持續評估及調整其化學品管理框架。

東協國家是我國進來貿易往來的重要區域,經濟部工業局提醒我國廠商應隨時掌握東協各國化學品管理法規的近況,採取因應法規需要的措施與行動,並及早準備未來法規發展所需的合規相關資料,確保出口貿易活動能符合當地的相關規範。  

參考資料

※ 轉載自

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

java併發編程 –併發問題的根源及主要解決方法

4{icon} {views}

目錄

  • 併發問題的根源在哪
    • 緩存導致的可見性
    • 線程切換帶來的原子性
    • 編譯器優化帶來的有序性
  • 主要解決辦法
    • 避免共享
    • Immutability(不變性)
    • 管程及其他工具

併發問題的根源在哪

首先,我們要知道併發要解決的是什麼問題?併發要解決的是單進程情況下硬件資源無法充分利用的問題。而造成這一問題的主要原因是CPU-內存-磁盤三者之間速度差異實在太大。如果將CPU的速度比作火箭的速度,那麼內存的速度就像火車,而最慘的磁盤,基本上就相當於人雙腿走路。

這樣造成的一個問題,就是CPU快速執行完它的任務的時候,很長時間都會在等待磁盤或是內存的讀寫。

計算機的發展有一部分就是如何重複利用資源,解決硬件資源之間效率的不平衡,而後就有了多進程,多線程的發展。並且演化出了各種為多進程(線程)服務的東西:

  • CPU增加緩存機制,平衡與內存的速度差異
  • 增加了多個概念,CPU時間片,程序計數器,線程切換等,用以更好得服務併發場景
  • 編譯器的指令優化,希望在內部充分利用硬件資源

但是這樣一來,也會帶來新的併發問題,歸結起來主要有三個。

  • 由於緩存導致的可見性問題
  • 線程切換帶來的原子性問題
  • 編譯器優化帶來的有序性問題

我們分別介紹這幾個:

緩存導致的可見性

CPU為了平衡與內存之間的性能差異,引入了CPU緩存,這樣CPU執行指令修改數據的時候就可以批量直接讀寫CPU緩存的內存,一個階段后再將數據寫回到內存。

但由於現在多核CPU技術的發展,各個線程可能運行在不同CPU核上面,每個CPU核各有各自的CPU緩存。前面說到對變量的修改通常都會先寫入CPU緩存,再寫回內存。這就會出現這樣一種情況,線程1修改了變量A,但此時修改后的變量A只存儲在CPU緩存中。這時候線程B去內存中讀取變量A,依舊只讀取到舊的值,這就是可見性問題。

線程切換帶來的原子性

為了更充分得利用CPU,引入了CPU時間片時間片的概念。進程或線程通過爭用CPU時間片,讓CPU可以更加充分得利用。

比如在進行讀寫磁盤等耗時高的任務時,就可以將寶貴的CPU資源讓出來讓其他線程去獲取CPU並執行任務。

但這樣的切換也會導致問題,那就是會破壞線程某些任務的原子性。比如java中簡單的一條語句count += 1。

映射到CPU指令有三條,讀取count變量指令,變量加1指令,變量寫回指令。雖然在高級語言(java)看來它就是一條指令,但實際上確是三條CPU指令,並且這三條指令的原子性無法保證。也就是說,可能在執行到任意一條指令的時候被打斷,CPU被其他線程搶佔了。而這個期間變量值可能會被修改,這裏就會引發數據不一致的情況了。所以高併發場景下,很多時候都會通過鎖實現原子性。而這個問題也是很多併發問題的源頭。

編譯器優化帶來的有序性

因為現在程序員編寫的都是高級語言,編譯器需要將用戶的代碼轉成CPU可以執行的指令。

同時,由於計算機領域的不斷髮展,編譯器也越來越智能,它會自動對程序員編寫的代碼進行優化,而優化中就有可能出現實際執行代碼順序和編寫的代碼順序不一樣的情況。

而這種破壞程序有序性的行為,在有些時候會出現一些非常微妙且難以察覺的併發編程bug。

舉個簡單的例子,我們常見的單例模式是這樣的:

public class Singleton {
 
 private Singleton() {}

 private static Singleton sInstance;

 public static Singleton getInstance() {

    if (sInstance == null) {	//第一次驗證是否為null
      synchronized (Singleton.class) {   //加鎖
        if (sInstance == null) {	  //第二次驗證是否為null
          sInstance = new Singleton();  //創建對象
                 }
             }
         }
    return sInstance;
    }

}

即通過兩段判斷加鎖來保證單例的成功生成,但在極小的概率下,可能會出現異常情況。原因就出現在sInstance = new Singleton();這一行代碼上。這行代碼,我們理解的執行順序應該是這樣:

  1. 為Singleton象分配一個內存空間。
  2. 在分配的內存空間實例化對象。
  3. 把Instance 引用地址指向內存空間。

但在實際編譯的過程中,編譯器有可能會幫我們進行優化,優化完它的順序可能變成如下:

  1. 為Singleton對象分配一個內存空間。
  2. 把instance 引用地址指向內存空間。
  3. 在分配的內存空間實例化對象。

按照優化完的順序,當併發訪問的時候,可能會出現這樣的情況

  1. A線程進入方法進行第1次instance == null判斷。
  2. 此時A線程發現instance 為null 所以對Singleton.class加鎖。
  3. 然後A線程進入方法進行第2次instance == null判斷。
  4. 然後A線程發現instance 為null,開始進行對象實例化。
  5. 為對象分配一個內存空間。
    6.把Instance 引用地址指向內存空間(而就在這個指令完成后,線程B進入了方法)。
  6. B線程首先進入方法進行第1次instance == null判斷。
  7. B線程此時發現instance 不為null ,所以它會直接返回instance (而此時返回的instance 是A線程還沒有初始化完成的對象)

最終線程B拿到的instance 是一個沒有實例化對象的空內存地址,所以導致instance使用的過程中造成程序錯誤。解決辦法很簡單,可以給sInstance對象加上一個關鍵字,volatile,這樣編譯器就不會亂優化,有關volatile的具體內容後續再細說。

主要解決辦法

通過上面的介紹,其實可以歸納無論是CPU緩存,線程切換還是編譯器優化亂序,出現問題的核心都是因為多個線程要併發讀寫某個變量或併發執行某段代碼。那麼我們可以控制,一次只讓一個線程執行變量讀寫就可以了,這就是互斥

而在某些時候,互斥還不夠,還需要一定的條件。比如一個生產者一個消費者併發,生產者向隊列存東西,消費者向隊列拿東西。那麼生產者寫的時候要保證存的時候隊列不是滿的,消費者要保證拿的時候隊列非空。這種線程與線程間需要通信協作的情況,稱為同步同步可以說是更複雜的互斥

既然知道了併發編程的根源以及同步和互斥,那我們來看看有哪些解決的思路。其實一共也就三種:

  • 避免共享
  • Immutability(不變性)
  • 管程及其他工具

下面我們分別說說這三種方案的優缺點

避免共享

我們先來說說避免共享,其實避免共享說是線程本地存儲技術,在java中指的一般就是Threadlocal。ThreadLocal會為每個線程提供一個本地副本,每個線程都只會修改自己的ThreadLocal變量。這樣一來就不會出現共享變量,也就不會出現衝突了。

其實現原理是在ThreadLocal內部維護一個ThreadLocalMap,每次有線程要獲取對應變量的時候,先獲取當前線程,然後根據不同線程取不同的值,典型的以空間換時間。

所以ThreadLocal還是比較適用於需要共享資源,且資源佔用空間不大的情況。比如一些連接的session啊等等。但是這種模式應用場景也較為有限,比如需要同步情況就難以勝任。

Immutability(不變性)

Immutability在函數式中用得比較多,函數式編程的一個主要目的是要寫出無副作用的代碼,有關什麼是無副作用可以參考我以前的文章Scala函數式編程指南(一) 函數式思想介紹。而無副作用的一個主要特點就是變量都是Immutability即不可變的,即創建對象后不會再修改對象,比如scala默認的變量和數據結構都是不可變的。而在java中,不變性變量即通過final修飾的變量,如String,Long,Double等類型都是Immutability的,它們的內部實現都是基於final關鍵字的。

那這又和併發編程有什麼關係呢?其實啊,併發問題很大部分原因就是因為線程切換破壞了原子性,這又導致線程隨意對變量的讀寫破壞了數據的一致性。而不變性就不必擔心這個問題,因為變量都是不變,不可寫只能讀的。在這種編程模式下,你要修改一個變量,那麼只能新生成一個。這樣做的好處很明顯,但壞處也是顯而易見,那就是引入了額外的編程複雜度,喪失了代碼的可讀性和易用性。

因為如此,不變性的併發解決方案其實相對而已沒那麼廣泛,其中比較有代表性的算是Actor併發編程模型,我以前也有討論過,有興趣可以看看Actor模型淺析 一致性和隔離性,這種編程模型和常規併發解決方案有很顯著的差異。按我的了解,Acctor模式多用在分佈式系統的一些協調功能,比如維持集群中多個機器的心跳通信等等。如果在單機併發環境下,還是下面要介紹的管程類工具才是利器。

管程及其他工具

其實最早的操作系統中,解決併發問題用的是信號量,信號量通過兩個原子操作wait(S),和signal(S)(俗稱P,V操作)來實現訪問資源互斥和同步。比如下面這個小例子:

//整型信號量定義
int S;

//P操作
wait(S){
  while(S<=0);
  S--;
}

//V操作
signal(S){
  S++;
}

雖然信號量方便有效,但信號量要對每個共享資源都實現對應的P和V操作,這使得併發編程中可能要出現大量的P,V操作,並且這部分內容難以抽象出來。

為了更好地實現同步互斥,於是就產生了管程(即Monitor,也有翻譯為監視器),值得一提的是,管程也有幾種模型,分別是:Hasen模型,Hoare模型和MESA模型。其中MESA模型應用最廣泛,java也是參考自MESA模型。這裏簡單介紹下管程的理論知識,這部分內容參考自進程同步機制—–為進程併發執行保駕護航,希望了解更多管程理論知識的童鞋可以看看。

我們來通過一個經典的生產-消費隊列來解釋,如下圖

我們先解釋下圖中右半部分的內容,右上角有一個等待調用的線程隊列,管程中每次只能有一個線程在執行任務,所以多個任務需要等待。然後是各個名詞的意思,生產-消費需要往隊列寫入和取出東西,這裏的隊列就是共享變量對共享資源進行操作稱之為過程(入隊和出隊兩個過程)。而向隊列寫入和取出是有條件的,寫入的時候隊列必須是非滿的,取出的時候隊列必須是非空的,這兩個條件被稱為條件變量

然後再來看看左半部分的內容,假設線程T1讀取共享變量(即隊列),此時發現隊列為空(條件變量之一),那麼T1此時需要等待,去哪裡等呢?去條件變量隊列不能為空對應的隊列中去等待。此時另一個線程T2向共享變量隊列寫數據,通過了條件變量隊列不能滿,那麼寫完后就會通知線程T1。但因為管程的限制,管程中只能有一個線程在執行,所以T1線程不能立即執行,它會回到右上角的線程等待隊列等待(不同的管程模型在這裡是有分歧的,比如Hasen模型是立即中斷T2線程讓隊列中下一個線程執行)。

解釋完這個圖,管程的概念也就呼之欲出了,

hansen對管程的定義如下:一個管程定義了一個數據結構和能力為併發進程所執行(在該數據結構上)的一組操作,這組操作能同步進程和改變管程中的數據。

本質上,管程是對共享資源以及對共享資源的操作抽象成變量和方法,要操作共享變量僅能通過管程提供的方法(比如上面的入隊和出隊)間接訪問。所以你會發現管程其實和面向對象的理念是十分相近的,在java中,主要提供了低層次了synchronized關鍵字和wait(),notify()等方法。同時還提供了高層次的ReenTrantLock和Condition來實現管程模型。

以上~

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)

2{icon} {views}

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了標籤模塊和友情鏈接模塊的所有功能,本篇來繼續完成博客最後的模塊,文章的管理。

文章列表 & 刪除

先將分頁查詢的列表給整出來,這塊和首頁的分頁列表是類似的,就是多了個Id字段。

先添加兩條路由規則。

@page "/admin/posts"
@page "/admin/posts/{page:int}"

新建返回數據默認QueryPostForAdminDto.cs

//QueryPostForAdminDto.cs
using System.Collections.Generic;

namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryPostForAdminDto
    {
        /// <summary>
        /// 年份
        /// </summary>
        public int Year { get; set; }

        /// <summary>
        /// Posts
        /// </summary>
        public IEnumerable<PostBriefForAdminDto> Posts { get; set; }
    }
}

//PostBriefForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class PostBriefForAdminDto : PostBriefDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}

然後添加所需的參數:當前頁碼、限制條數、總頁碼、文章列表返回數據模型。

/// <summary>
/// 當前頁碼
/// </summary>
[Parameter]
public int? page { get; set; }

/// <summary>
/// 限制條數
/// </summary>
private int Limit = 15;

/// <summary>
/// 總頁碼
/// </summary>
private int TotalPage;

/// <summary>
/// 文章列表數據
/// </summary>
private ServiceResult<PagedList<QueryPostForAdminDto>> posts;

然後在初始化函數OnInitializedAsync()中調用API獲取文章數據.

/// <summary>
/// 初始化
/// </summary>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    // 設置默認值
    page = page.HasValue ? page : 1;

    await RenderPage(page);
}

/// <summary>
/// 點擊頁碼重新渲染數據
/// </summary>
/// <param name="page"></param>
/// <returns></returns>
private async Task RenderPage(int? page)
{
    // 獲取數據
    posts = await Http.GetFromJsonAsync<ServiceResult<PagedList<QueryPostForAdminDto>>>($"/blog/admin/posts?page={page}&limit={Limit}");

    // 計算總頁碼
    TotalPage = (int)Math.Ceiling((posts.Result.Total / (double)Limit));
}

在初始化中判斷page參數,如果沒有值給他設置一個默認值1。RenderPage(int? page)方法是調用API返回數據,並計算出總頁碼值。

最後在頁面上進行數據綁定。

<AdminLayout>
    @if (posts == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap archive">
            <NavLink style="float:right" href="/admin/post"><h3>~~~ 新增文章 ~~~</h3></NavLink>
            @if (posts.Success && posts.Result.Item.Any())
            {
                @foreach (var item in posts.Result.Item)
                {
                    <h3>@item.Year</h3>
                    @foreach (var post in item.Posts)
                    {
                        <article class="archive-item">
                            <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(post.Id))"></NavLink>
                            <NavLink title="編輯" @onclick="@(async () => await Common.NavigateTo($"/admin/post/{post.Id}"))"></NavLink>
                            <NavLink target="_blank" class="archive-item-link" href="@("/post" + post.Url)">@post.Title</NavLink>
                            <span class="archive-item-date">@post.CreationTime</span>
                        </article>
                    }
                }
                <nav class="pagination">
                    @for (int i = 1; i <= TotalPage; i++)
                    {
                        var _page = i;

                        if (page == _page)
                        {
                            <span class="page-number current">@_page</span>
                        }
                        else
                        {
                            <a class="page-number" @onclick="@(() => RenderPage(_page))" href="/admin/posts/@_page">@_page</a>
                        }
                    }
                </nav>
            }
            else
            {
                <ErrorTip />
            }
        </div>
    }
</AdminLayout>

HTML內容放在組件AdminLayout中,當 posts 沒加載完數據的時候显示加載組件<Loading />

在頁面上循環遍歷文章數據和翻頁頁碼,每篇文章標題前面添加兩個按鈕刪除和編輯,同時單獨加了一個新增文章的按鈕。

刪除文章調用DeleteAsync(int id)方法,需要傳遞參數,當前文章的id。

新增和編輯按鈕都跳轉到”/admin/post”頁面,當編輯的時候將id也傳過去即可,路由規則為:”/admin/post/{id}”。

刪除文章“方法如下:

/// <summary>
/// 刪除文章
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
    // 彈窗確認
    bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這篇該死的文章嗎");

    if (confirmed)
    {
        var response = await Http.DeleteAsync($"/blog/post?id={id}");

        var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

        if (result.Success)
        {
            await RenderPage(page);
        }
    }
}

刪除之前進行二次確認,避免誤刪,當確認刪除之後調用刪除文章API,最後重新渲染數據即可。

新增 & 更新文章

完成了後台文章列表的查詢和刪除,現在整個博客模塊功能就差新增和更新文章了,勝利就在前方,沖啊。

這塊的開發工作耗費了我太多時間,因為想使用 markdown 來寫文章,找了一圈下來沒有一個合適的組件,所以退而求次只能選擇現有的markdown編輯器來實現了。

我這裏選擇了開源的編輯器Editor.md,有需要的可以去 Github 自己下載,https://github.com/pandao/editor.md 。

將下載的資源包解壓放在 wwwroot 文件夾下,默認是比較大的,而且還有很多示例文件,我已經將其精簡了一番,可以去我 Github 下載使用。

先來看下最終的成品效果吧。

是不是感覺還可以,廢話不多說,接下里告訴大家如何實現。

在 Admin 文件夾下添加post.razor組件,設置路由,並且引用一個樣式文件,在頁面中引用樣式文件好像不太符合標準,不過無所謂了,這個後台就自己用,而且還就這一個頁面用得到。

@page "/admin/post"
@page "/admin/post/{id:int}"

<link href="./editor.md/css/editormd.css" rel="stylesheet" />

<AdminLayout>
    ...
</AdminLayout>

把具體HTML內容放在組件AdminLayout中。

因為新增和編輯放在同一個頁面上,所以當id參數不為空的時候需要添加一個id參數,同時默認一進來就讓頁面显示加載中的組件,當頁面和數據加載完成后在显示具體的內容,所以在指定一個布爾類型的是否加載參數isLoading

我們的編輯器主要依賴JavaScript實現的,所以這裏不可避免要使用到JavaScript了。

app.js中添加幾個全局函數。

switchEditorTheme: function () {
    editor.setTheme(localStorage.editorTheme || 'default');
    editor.setEditorTheme(localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default');
    editor.setPreviewTheme(localStorage.editorTheme || 'default');
},
renderEditor: async function () {
	await this._loadScript('./editor.md/lib/zepto.min.js').then(function () {
	    func._loadScript('./editor.md/editormd.js').then(function () {
	        editor = editormd("editor", {
	            width: "100%",
	            height: 700,
	            path: './editor.md/lib/',
	            codeFold: true,
	            saveHTMLToTextarea: true,
	            emoji: true,
	            atLink: false,
	            emailLink: false,
	            theme: localStorage.editorTheme || 'default',
	            editorTheme: localStorage.editorTheme === 'dark' ? 'pastel-on-dark' : 'default',
	            previewTheme: localStorage.editorTheme || 'default',
	            toolbarIcons: function () {
	                return ["bold", "del", "italic", "quote", "ucwords", "uppercase", "lowercase", "h1", "h2", "h3", "h4", "h5", "h6", "list-ul", "list-ol", "hr", "link", "image", "code", "preformatted-text", "code-block", "table", "datetime", "html-entities", "emoji", "watch", "preview", "fullscreen", "clear", "||", "save"]
	            },
	            toolbarIconsClass: {
	                save: "fa-check"
	            },
	            toolbarHandlers: {
	                save: function () {
	                    func._shoowBox();
	                }
	            },
	            onload: function () {
	                this.addKeyMap({
	                    "Ctrl-S": function () {
	                        func._shoowBox();
	                    }
	                });
	            }
	        });
	    });
	});
},
_shoowBox: function () {
    DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');
},
_loadScript: async function (url) {
    let response = await fetch(url);
    var js = await response.text();
    eval(js);
}

renderEditor主要實現了動態加載JavaScript代碼,將markdown編輯器渲染出來。這裏不多說,都是Editor.md示例裏面的代碼。

為了兼容暗黑色主題,這裏還加了一個切換編輯器主題的JavaScript方法,switchEditorTheme

_shoowBox就厲害了,這個方法是調用的.NET組件中的方法,前面我們用過了在Blazor中調用JavaScript,這裏演示了JavaScript中調用Blazor中的組件方法。

現在將所需的幾個參數都添加到代碼中。

/// <summary>
/// 定義一個委託方法,用於組件實例方法調用
/// </summary>
private static Func<Task> action;

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 修改時的文章Id
/// </summary>
[Parameter]
public int? Id { get; set; }

/// <summary>
/// 格式化的標籤
/// </summary>
private string tags { get; set; }

/// <summary>
/// 默認显示加載中
/// </summary>
private bool isLoading = true;

/// <summary>
/// 文章新增或者修改輸入參數
/// </summary>
private PostForAdminDto input;

/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

大家看看註釋就知道參數是做什麼的了。

現在我們在初始化函數中將所需的數據通過API獲取到。

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    action = ChangeOpenStatus;

    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    if (Id.HasValue)
    {
        var post = await Http.GetFromJsonAsync<ServiceResult<PostForAdminDto>>($"/blog/admin/post?id={Id}");

        if (post.Success)
        {
            var _post = post.Result;
            input = new PostForAdminDto
            {
                Title = _post.Title,
                Author = _post.Author,
                Url = _post.Url,
                Html = _post.Html,
                Markdown = _post.Markdown,
                CategoryId = _post.CategoryId,
                Tags = _post.Tags,
                CreationTime = _post.CreationTime
            };

            tags = string.Join(",", input.Tags);
        }
    }
    else
    {
        input = new PostForAdminDto()
        {
            Author = "阿星Plus",
            CreationTime = DateTime.Now
        };
    }

    categories = await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");

    // 渲染編輯器
    await Common.InvokeAsync("window.func.renderEditor");

    // 關閉加載
    isLoading = !isLoading;
}

action是一個異步的委託,在初始化中執行了ChangeOpenStatus方法,這個方法等會說,然後獲取localStorage中token的值。

通過參數Id是否有值來判斷當前是新增文章還是更新文章,如果有值就是更新文章,這時候需要根據id去將文章的數據拿到賦值給PostForAdminDto對象展示在頁面上,如果沒有可以添加幾個默認值給PostForAdminDto對象。

因為文章需要分類和標籤的數據,同時這裏將分類的數據也查出來,標籤默認是List列表,將其轉換成字符串類型。

但完成上面操作后,調用JavaScript方法renderEditor渲染渲染編輯器,最後關閉加載,显示頁面。

現在來看看頁面。

<AdminLayout>
    @if (isLoading)
    {
        <Loading />
    }
    else
    {
        <div class="post-box">
            <div class="post-box-item">
                <input type="text" placeholder="標題" autocomplete="off" @bind="@input.Title" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="作者" autocomplete="off" @bind="@input.Author" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div class="post-box-item">
                <input type="text" placeholder="URL" autocomplete="off" @bind="@input.Url" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
                <input type="text" placeholder="時間" autocomplete="off" @bind="@input.CreationTime" @bind:format="yyyy-MM-dd HH:mm:sss" @bind:event="oninput" @onclick="@(() => { Open = false; })" />
            </div>
            <div id="editor">
                <textarea style="display:none;">@input.Markdown</textarea>
            </div>

            <Box OnClickCallback="@SubmitAsync" Open="@Open" ButtonText="發布">
                <div class="box-item">
                    <b>分類:</b>
                    @if (categories.Success && categories.Result.Any())
                    {
                        @foreach (var item in categories.Result)
                        {
                            <label><input type="radio" name="category" value="@item.Id" @onchange="@(() => { input.CategoryId = item.Id; })" checked="@(item.Id == input.CategoryId)" />@item.CategoryName</label>
                        }
                    }
                </div>
                <div class="box-item"></div>
                <div class="box-item">
                    <b>標籤:</b>
                    <input type="text" @bind="@tags" @bind:event="oninput" />
                </div>
            </Box>
        </div>
    }
</AdminLayout>

添加了四個input框,分別用來綁定標題、作者、URL、時間,<div id="editor"></div>中為編輯器所需。

然後我這裏還是把之前的彈窗組件搞出來了,執行邏輯不介紹了,在彈窗組件中自定義显示分類和標籤的內容,將獲取到的分類和標籤綁定到具體位置。

每個分類都是一個radio標籤,並且對應一個點擊事件,點哪個就把當前分類的Id賦值給PostForAdminDto對象。

所有的input框都使用@bind@bind:event綁定數據和獲取數據。

Box彈窗組件這裏自定義了按鈕文字,ButtonText="發布"

/// <summary>
/// 改變Open狀態,通知組件渲染
/// </summary>
private async Task ChangeOpenStatus()
{
    Open = true;

    var markdown = await Common.InvokeAsync<string>("editor.getMarkdown");
    var html = await Common.InvokeAsync<string>("editor.getHTML");

    if (string.IsNullOrEmpty(input.Title) || string.IsNullOrEmpty(input.Url) ||
        string.IsNullOrEmpty(input.Author) || string.IsNullOrEmpty(markdown) ||
        string.IsNullOrEmpty(html))
    {
        await Alert();
    }

    input.Html = html;
    input.Markdown = markdown;

    StateHasChanged();
}

/// <summary>
/// 暴漏給JS執行,彈窗確認框
/// </summary>
[JSInvokable("showbox")]
public static void ShowBox()
{
    action.Invoke();
}
/// <summary>
/// alert提示
/// </summary>
/// <returns></returns>
private async Task Alert()
{
    Open = false;

    await Common.InvokeAsync("alert", "\n好像漏了點什麼吧");
    return;
}

現在可以來看看ChangeOpenStatus方法了,這個是改變當前彈窗狀態的一個方法。為什麼需要這個方法呢?

因為在Blazor中JavaScript想要調用組件內的方法,方法必須是靜態的,那麼只能通過這種方式去實現了,在靜態方法是不能夠直接改變彈窗的狀態值的。

其實也可以不用這麼麻煩,因為我在編輯器上自定義了一個按鈕,為了好看一些所以只能曲折一點,嫌麻煩的可以直接在頁面上搞個按鈕執行保存數據邏輯也是一樣的。

使用JSInvokableAttribute需要在_Imports.razor中添加命名空間@using Microsoft.JSInterop

ChangeOpenStatus中獲取到文章內容:HTML和markdown,賦值給PostForAdminDto對象,要先進行判斷頁面上的幾個參數是否有值,沒值的話給出提示執行Alert()方法,最後使用StateHasChanged()通知組件其狀態已更改。

Alert方法就是調用原生的JavaScriptalert方法,給出一個提示。

ShowBox就是暴漏給JavaScript的方法,使用DotNet.invokeMethodAsync('Meowv.Blog.BlazorApp', 'showbox');進行調用。

那麼現在一切都正常進行的情況下,點擊編輯器上自定義的保存按鈕,頁面上值不為空的情況下就會彈出我們的彈窗組件Box

最後在彈窗組件的回調方法中執行新增文章還是更新文章。

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    if (string.IsNullOrEmpty(tags) || input.CategoryId == 0)
    {
        await Alert();
    }

    input.Tags = tags.Split(",");

    var responseMessage = new HttpResponseMessage();

    if (Id.HasValue)
        responseMessage = await Http.PutAsJsonAsync($"/blog/post?id={Id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/post", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        await Common.NavigateTo("/admin/posts");
    }
}

打開彈窗后執行回調事件之前還是要判斷值是否為空,為空的情況下還是給出alert提示,此時將tags標籤還是轉換成List列表,根據Id是否有值去執行新增數據或者更新數據,最終成功后跳轉到文章列表頁。

本片到這裏就結束了,主要攻克了在Blazor中使用Markdown編輯器實現新增和更新文章,這個系列差不多就快結束了,預計還有2篇的樣子,感謝各位的支持。

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化

全球首創 東京用碳交易替大型建築減碳 六年減下1/4

2{icon} {views}

環境資訊中心記者 陳文姿報導

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

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

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

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

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化