摘要:用于展示一種簡(jiǎn)易的且更加寬松的互斥鎖的模擬。由于這里的原子操作函數(shù)只支持非常有限的數(shù)據(jù)類(lèi)型,所以在很多應(yīng)用場(chǎng)景下,互斥鎖往往是更加適合的。這主要是因?yàn)樵硬僮骱瘮?shù)的執(zhí)行速度要比互斥鎖快得多。
我們接著上一篇文章的內(nèi)容繼續(xù)聊,上一篇我們提到了,sync/atomic包中的函數(shù)可以做的原子操作有:加法(add)、比較并交換(compare and swap,簡(jiǎn)稱(chēng) CAS)、加載(load)、存儲(chǔ)(store)和交換(swap)。并且以此衍生出了兩個(gè)問(wèn)題。
今天我們繼續(xù)來(lái)看第三個(gè)衍生問(wèn)題: 比較并交換操作與交換操作相比有什么不同??jī)?yōu)勢(shì)在哪里?
回答是:比較并交換操作即 CAS 操作,是有條件的交換操作,只有在條件滿(mǎn)足的情況下才會(huì)進(jìn)行值的交換。
所謂的交換指的是,把新值賦給變量,并返回變量的舊值。
在進(jìn)行 CAS 操作的時(shí)候,函數(shù)會(huì)先判斷被操作變量的當(dāng)前值,是否與我們預(yù)期的舊值相等。如果相等,它就把新值賦給該變量,并返回true以表明交換操作已進(jìn)行;否則就忽略交換操作,并返回false。
可以看到,CAS 操作并不是單一的操作,而是一種操作組合。這與其他的原子操作都不同。正因?yàn)槿绱?,它的用途要更廣泛一些。例如,我們將它與for語(yǔ)句聯(lián)用就可以實(shí)現(xiàn)一種簡(jiǎn)易的自旋鎖(spinlock)。
for { if atomic.CompareAndSwapInt32(&num2, 10, 0) { fmt.Println("The second number has gone to zero.") break } time.Sleep(time.Millisecond * 500)}
在for語(yǔ)句中的 CAS 操作可以不停地檢查某個(gè)需要滿(mǎn)足的條件,一旦條件滿(mǎn)足就退出for循環(huán)。這就相當(dāng)于,只要條件未被滿(mǎn)足,當(dāng)前的流程就會(huì)被一直“阻塞”在這里。
這在效果上與互斥鎖有些類(lèi)似。不過(guò),它們的適用場(chǎng)景是不同的。我們?cè)谑褂没コ怄i的時(shí)候,總是假設(shè)共享資源的狀態(tài)會(huì)被其他的 goroutine 頻繁地改變。
而for語(yǔ)句加 CAS 操作的假設(shè)往往是:共享資源狀態(tài)的改變并不頻繁,或者,它的狀態(tài)總會(huì)變成期望的那樣。這是一種更加樂(lè)觀(guān),或者說(shuō)更加寬松的做法。
package mainimport ( "fmt" "sync/atomic" "time")func main() { // 第三個(gè)衍生問(wèn)題的示例。 forAndCAS1() fmt.Println() forAndCAS2()}// forAndCAS1 用于展示簡(jiǎn)易的自旋鎖。func forAndCAS1() { sign := make(chan struct{}, 2) num := int32(0) fmt.Printf("The number: %d/n", num) go func() { // 定時(shí)增加num的值。 defer func() { sign <- struct{}{} }() for { time.Sleep(time.Millisecond * 500) newNum := atomic.AddInt32(&num, 2) fmt.Printf("The number: %d/n", newNum) if newNum == 10 { break } } }() go func() { // 定時(shí)檢查num的值,如果等于10就將其歸零。 defer func() { sign <- struct{}{} }() for { if atomic.CompareAndSwapInt32(&num, 10, 0) { fmt.Println("The number has gone to zero.") break } time.Sleep(time.Millisecond * 500) } }() <-sign <-sign}// forAndCAS2 用于展示一種簡(jiǎn)易的(且更加寬松的)互斥鎖的模擬。func forAndCAS2() { sign := make(chan struct{}, 2) num := int32(0) fmt.Printf("The number: %d/n", num) max := int32(20) go func(id int, max int32) { // 定時(shí)增加num的值。 defer func() { sign <- struct{}{} }() for i := 0; ; i++ { currNum := atomic.LoadInt32(&num) if currNum >= max { break } newNum := currNum + 2 time.Sleep(time.Millisecond * 200) if atomic.CompareAndSwapInt32(&num, currNum, newNum) { fmt.Printf("The number: %d [%d-%d]/n", newNum, id, i) } else { fmt.Printf("The CAS operation failed. [%d-%d]/n", id, i) } } }(1, max) go func(id int, max int32) { // 定時(shí)增加num的值。 defer func() { sign <- struct{}{} }() for j := 0; ; j++ { currNum := atomic.LoadInt32(&num) if currNum >= max { break } newNum := currNum + 2 time.Sleep(time.Millisecond * 200) if atomic.CompareAndSwapInt32(&num, currNum, newNum) { fmt.Printf("The number: %d [%d-%d]/n", newNum, id, j) } else { fmt.Printf("The CAS operation failed. [%d-%d]/n", id, j) } } }(2, max) <-sign <-sign}
第四個(gè)衍生問(wèn)題:假設(shè)我已經(jīng)保證了對(duì)一個(gè)變量的寫(xiě)操作都是原子操作,比如:加或減、存儲(chǔ)、交換等等,那我對(duì)它進(jìn)行讀操作的時(shí)候,還有必要使用原子操作嗎?
回答是:很有必要。其中的道理你可以對(duì)照一下讀寫(xiě)鎖。為什么在讀寫(xiě)鎖保護(hù)下的寫(xiě)操作和讀操作之間是互斥的?這是為了防止讀操作讀到?jīng)]有被修改完的值,對(duì)嗎?
如果寫(xiě)操作還沒(méi)有進(jìn)行完,讀操作就來(lái)讀了,那么就只能讀到僅修改了一部分的值。這顯然破壞了值的完整性,讀出來(lái)的值也是完全錯(cuò)誤的。
所以,一旦你決定了要對(duì)一個(gè)共享資源進(jìn)行保護(hù),那就要做到完全的保護(hù)。不完全的保護(hù)基本上與不保護(hù)沒(méi)有什么區(qū)別。
好了,上面的主問(wèn)題以及相關(guān)的衍生問(wèn)題涉及了原子操作函數(shù)的用法、原理、對(duì)比和一些最佳實(shí)踐,希望你已經(jīng)理解了。
由于這里的原子操作函數(shù)只支持非常有限的數(shù)據(jù)類(lèi)型,所以在很多應(yīng)用場(chǎng)景下,互斥鎖往往是更加適合的。
不過(guò),一旦我們確定了在某個(gè)場(chǎng)景下可以使用原子操作函數(shù),比如:只涉及并發(fā)地讀寫(xiě)單一的整數(shù)類(lèi)型值,或者多個(gè)互不相關(guān)的整數(shù)類(lèi)型值,那就不要再考慮互斥鎖了。
這主要是因?yàn)樵硬僮骱瘮?shù)的執(zhí)行速度要比互斥鎖快得多。而且,它們使用起來(lái)更加簡(jiǎn)單,不會(huì)涉及臨界區(qū)的選擇,以及死鎖等問(wèn)題。當(dāng)然了,在使用 CAS 操作的時(shí)候,我們還是要多加注意的,因?yàn)樗梢员挥脕?lái)模仿鎖,并有可能“阻塞”流程。
問(wèn)題:怎樣用好sync/atomic.Value?
為了擴(kuò)大原子操作的適用范圍,Go 語(yǔ)言在 1.4 版本發(fā)布的時(shí)候向sync/atomic包中添加了一個(gè)新的類(lèi)型Value。此類(lèi)型的值相當(dāng)于一個(gè)容器,可以被用來(lái)“原子地”存儲(chǔ)和加載任意的值。
atomic.Value類(lèi)型是開(kāi)箱即用的,我們聲明一個(gè)該類(lèi)型的變量(以下簡(jiǎn)稱(chēng)原子變量)之后就可以直接使用了。這個(gè)類(lèi)型使用起來(lái)很簡(jiǎn)單,它只有兩個(gè)指針?lè)椒ǎ篠tore和Load。不過(guò),雖然簡(jiǎn)單,但還是有一些值得注意的地方的。
首先一點(diǎn),一旦atomic.Value類(lèi)型的值(以下簡(jiǎn)稱(chēng)原子值)被真正使用,它就不應(yīng)該再被復(fù)制了。什么叫做“真正使用”呢?
我們只要用它來(lái)存儲(chǔ)值了,就相當(dāng)于開(kāi)始真正使用了。atomic.Value類(lèi)型屬于結(jié)構(gòu)體類(lèi)型,而結(jié)構(gòu)體類(lèi)型屬于值類(lèi)型。
所以,復(fù)制該類(lèi)型的值會(huì)產(chǎn)生一個(gè)完全分離的新值。這個(gè)新值相當(dāng)于被復(fù)制的那個(gè)值的一個(gè)快照。之后,不論后者存儲(chǔ)的值怎樣改變,都不會(huì)影響到前者,反之亦然。
另外,關(guān)于用原子值來(lái)存儲(chǔ)值,有兩條強(qiáng)制性的使用規(guī)則。第一條規(guī)則,不能用原子值存儲(chǔ)nil。
也就是說(shuō),我們不能把nil作為參數(shù)值傳入原子值的Store方法,否則就會(huì)引發(fā)一個(gè) panic。
這里要注意,如果有一個(gè)接口類(lèi)型的變量,它的動(dòng)態(tài)值是nil,但動(dòng)態(tài)類(lèi)型卻不是nil,那么它的值就不等于nil。我在前面講接口的時(shí)候和你說(shuō)明過(guò)這個(gè)問(wèn)題。正因?yàn)槿绱?,這樣一個(gè)變量的值是可以被存入原子值的。
第二條規(guī)則,我們向原子值存儲(chǔ)的第一個(gè)值,決定了它今后能且只能存儲(chǔ)哪一個(gè)類(lèi)型的值。
例如,我第一次向一個(gè)原子值存儲(chǔ)了一個(gè)string類(lèi)型的值,那我在后面就只能用該原子值來(lái)存儲(chǔ)字符串了。如果我又想用它存儲(chǔ)結(jié)構(gòu)體,那么在調(diào)用它的Store方法的時(shí)候就會(huì)引發(fā)一個(gè) panic。這個(gè) panic 會(huì)告訴我,這次存儲(chǔ)的值的類(lèi)型與之前的不一致。
你可能會(huì)想:我先存儲(chǔ)一個(gè)接口類(lèi)型的值,然后再存儲(chǔ)這個(gè)接口的某個(gè)實(shí)現(xiàn)類(lèi)型的值,這樣是不是可以呢?
很可惜,這樣是不可以的,同樣會(huì)引發(fā)一個(gè) panic。因?yàn)樵又祪?nèi)部是依據(jù)被存儲(chǔ)值的實(shí)際類(lèi)型來(lái)做判斷的。所以,即使是實(shí)現(xiàn)了同一個(gè)接口的不同類(lèi)型,它們的值也不能被先后存儲(chǔ)到同一個(gè)原子值中。
遺憾的是,我們無(wú)法通過(guò)某個(gè)方法獲知一個(gè)原子值是否已經(jīng)被真正使用,并且,也沒(méi)有辦法通過(guò)常規(guī)的途徑得到一個(gè)原子值可以存儲(chǔ)值的實(shí)際類(lèi)型。這使得我們誤用原子值的可能性大大增加,尤其是在多個(gè)地方使用同一個(gè)原子值的時(shí)候。
下面,我給你幾條具體的使用建議。
1、不要把內(nèi)部使用的原子值暴露給外界。比如,聲明一個(gè)全局的原子變量并不是一個(gè)正確的做法。這個(gè)變量的訪(fǎng)問(wèn)權(quán)限最起碼也應(yīng)該是包級(jí)私有的。
2、如果不得不讓包外,或模塊外的代碼使用你的原子值,那么可以聲明一個(gè)包級(jí)私有的原子變量,然后再通過(guò)一個(gè)或多個(gè)公開(kāi)的函數(shù),讓外界間接地使用到它。注意,這種情況下不要把原子值傳遞到外界,不論是傳遞原子值本身還是它的指針值。
3、如果通過(guò)某個(gè)函數(shù)可以向內(nèi)部的原子值存儲(chǔ)值的話(huà),那么就應(yīng)該在這個(gè)函數(shù)中先判斷被存儲(chǔ)值類(lèi)型的合法性。若不合法,則應(yīng)該直接返回對(duì)應(yīng)的錯(cuò)誤值,從而避免 panic 的發(fā)生。
4、如果可能的話(huà),我們可以把原子值封裝到一個(gè)數(shù)據(jù)類(lèi)型中,比如一個(gè)結(jié)構(gòu)體類(lèi)型。這樣,我們既可以通過(guò)該類(lèi)型的方法更加安全地存儲(chǔ)值,又可以在該類(lèi)型中包含可存儲(chǔ)值的合法類(lèi)型信息。
除了上述使用建議之外,我還要再特別強(qiáng)調(diào)一點(diǎn):盡量不要向原子值中存儲(chǔ)引用類(lèi)型的值。因?yàn)檫@很容易造成安全漏洞。請(qǐng)看下面的代碼:
var box6 atomic.Valuev6 := []int{1, 2, 3}box6.Store(v6)v6[1] = 4 // 注意,此處的操作不是并發(fā)安全的!
我把一個(gè)[]int類(lèi)型的切片值v6, 存入了原子值box6。注意,切片類(lèi)型屬于引用類(lèi)型。所以,我在外面改動(dòng)這個(gè)切片值,就等于修改了box6中存儲(chǔ)的那個(gè)值。這相當(dāng)于繞過(guò)了原子值而進(jìn)行了非并發(fā)安全的操作。那么,應(yīng)該怎樣修補(bǔ)這個(gè)漏洞呢?可以這樣做:
store := func(v []int) { replica := make([]int, len(v)) copy(replica, v) box6.Store(replica)}store(v6)v6[2] = 5 // 此處的操作是安全的。
我先為切片值v6創(chuàng)建了一個(gè)完全的副本。這個(gè)副本涉及的數(shù)據(jù)已經(jīng)與原值毫不相干了。然后,我再把這個(gè)副本存入box6。如此一來(lái),無(wú)論我再對(duì)v6的值做怎樣的修改,都不會(huì)破壞box6提供的安全保護(hù)。
以上,就是我要告訴你的關(guān)于atomic.Value的注意事項(xiàng)和使用建議。你可以在 demo64.go 文件中看到相應(yīng)的示例。
package mainimport ( "errors" "fmt" "io" "os" "reflect" "sync/atomic")func main() { // 示例1。 var box atomic.Value fmt.Println("Copy box to box2.") box2 := box // 原子值在真正使用前可以被復(fù)制。 v1 := [...]int{1, 2, 3} fmt.Printf("Store %v to box./n", v1) box.Store(v1) fmt.Printf("The value load from box is %v./n", box.Load()) fmt.Printf("The value load from box2 is %v./n", box2.Load()) fmt.Println() // 示例2。 v2 := "123" fmt.Printf("Store %q to box2./n", v2) box2.Store(v2) // 這里并不會(huì)引發(fā)panic。 fmt.Printf("The value load from box is %v./n", box.Load()) fmt.Printf("The value load from box2 is %q./n", box2.Load()) fmt.Println() // 示例3。 fmt.Println("Copy box to box3.") box3 := box // 原子值在真正使用后不應(yīng)該被復(fù)制! fmt.Printf("The value load from box3 is %v./n", box3.Load()) v3 := 123 fmt.Printf("Store %d to box3./n", v3) //box3.Store(v3) // 這里會(huì)引發(fā)一個(gè)panic,報(bào)告存儲(chǔ)值的類(lèi)型不一致。 _ = box3 fmt.Println() // 示例4。 var box4 atomic.Value v4 := errors.New("something wrong") fmt.Printf("Store an error with message %q to box4./n", v4) box4.Store(v4) v41 := io.EOF fmt.Println("Store a value of the same type to box4.") box4.Store(v41) v42, ok := interface{}(&os.PathError{}).(error) if ok { fmt.Printf("Store a value of type %T that implements error interface to box4./n", v42) //box4.Store(v42) // 這里會(huì)引發(fā)一個(gè)panic,報(bào)告存儲(chǔ)值的類(lèi)型不一致。 } fmt.Println() // 示例5。 box5, err := NewAtomicValue(v4) if err != nil { fmt.Printf("error: %s/n", err) } fmt.Printf("The legal type in box5 is %s./n", box5.TypeOfValue()) fmt.Println("Store a value of the same type to box5.") err = box5.Store(v41) if err != nil { fmt.Printf("error: %s/n", err) } fmt.Printf("Store a value of type %T that implements error interface to box5./n", v42) err = box5.Store(v42) if err != nil { fmt.Printf("error: %s/n", err) } fmt.Println() // 示例6。 var box6 atomic.Value v6 := []int{1, 2, 3} fmt.Printf("Store %v to box6./n", v6) box6.Store(v6) v6[1] = 4 // 注意,此處的操作不是并發(fā)安全的! fmt.Printf("The value load from box6 is %v./n", box6.Load()) // 正確的做法如下。 v6 = []int{1, 2, 3} store := func(v []int) { replica := make([]int, len(v)) copy(replica, v) box6.Store(replica) } fmt.Printf("Store %v to box6./n", v6) store(v6) v6[2] = 5 // 此處的操作是安全的。 fmt.Printf("The value load from box6 is %v./n", box6.Load())}type atomicValue struct { v atomic.Value t reflect.Type}func NewAtomicValue(example interface{}) (*atomicValue, error) { if example == nil { return nil, errors.New("atomic value: nil example") } return &atomicValue{ t: reflect.TypeOf(example), }, nil}func (av *atomicValue) Store(v interface{}) error { if v == nil { return errors.New("atomic value: nil value") } t := reflect.TypeOf(v) if t != av.t { return fmt.Errorf("atomic value: wrong type: %s", t) } av.v.Store(v) return nil}func (av *atomicValue) Load() interface{} { return av.v.Load()}func (av *atomicValue) TypeOfValue() reflect.Type { return av.t}
我們把這兩篇文章一起總結(jié)一下。相對(duì)于原子操作函數(shù),原子值類(lèi)型的優(yōu)勢(shì)很明顯,但它的使用規(guī)則也更多一些。首先,在首次真正使用后,原子值就不應(yīng)該再被復(fù)制了。
其次,原子值的Store方法對(duì)其參數(shù)值(也就是被存儲(chǔ)值)有兩個(gè)強(qiáng)制的約束。一個(gè)約束是,參數(shù)值不能為nil。另一個(gè)約束是,參數(shù)值的類(lèi)型不能與首個(gè)被存儲(chǔ)值的類(lèi)型不同。也就是說(shuō),一旦一個(gè)原子值存儲(chǔ)了某個(gè)類(lèi)型的值,那它以后就只能存儲(chǔ)這個(gè)類(lèi)型的值了。
基于上面這幾個(gè)注意事項(xiàng),我提出了幾條使用建議,包括:不要對(duì)外暴露原子變量、不要傳遞原子值及其指針值、盡量不要在原子值中存儲(chǔ)引用類(lèi)型的值,等等。與之相關(guān)的一些解決方案我也一并提出了。希望你能夠受用。
原子操作明顯比互斥鎖要更加輕便,但是限制也同樣明顯。所以,我們?cè)谶M(jìn)行二選一的時(shí)候通常不會(huì)太困難。但是原子值與互斥鎖之間的選擇有時(shí)候就需要仔細(xì)的考量了。不過(guò),如果你能牢記我今天講的這些內(nèi)容的話(huà),應(yīng)該會(huì)有很大的助力。
今天的思考題只有一個(gè),那就是:如果要對(duì)原子值和互斥鎖進(jìn)行二選一,你認(rèn)為最重要的三個(gè)決策條件應(yīng)該是什么?
https://github.com/MingsonZheng/go-core-demo
本作品采用知識(shí)共享署名-非商業(yè)性使用-相同方式共享 4.0 國(guó)際許可協(xié)議進(jìn)行許可。
歡迎轉(zhuǎn)載、使用、重新發(fā)布,但務(wù)必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商業(yè)目的,基于本文修改后的作品務(wù)必以相同的許可發(fā)布。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/123978.html
摘要:除此之外,把并發(fā)安全字典封裝在一個(gè)結(jié)構(gòu)體類(lèi)型中,往往是一個(gè)很好的選擇。請(qǐng)看下面的代碼如上所示,我編寫(xiě)了一個(gè)名為的結(jié)構(gòu)體類(lèi)型,它代表了鍵類(lèi)型為值類(lèi)型為的并發(fā)安全字典。在這個(gè)結(jié)構(gòu)體類(lèi)型中,只有一個(gè)類(lèi)型的字段。34 | 并發(fā)安全字典sync.Map (上)我們今天再來(lái)講一個(gè)并發(fā)安全的高級(jí)數(shù)據(jù)結(jié)構(gòu):sync.Map。眾所周知,Go 語(yǔ)言自帶的字典類(lèi)型map并不是并發(fā)安全的。前導(dǎo)知識(shí):并發(fā)安全字典誕生...
摘要:在第二種方案中,我們封裝的結(jié)構(gòu)體類(lèi)型的所有方法,都可以與類(lèi)型的方法完全一致包括方法名稱(chēng)和方法簽名。所以在設(shè)計(jì)這樣的結(jié)構(gòu)體類(lèi)型的時(shí)候,只包含類(lèi)型的字段就不夠了。當(dāng)參數(shù)或的實(shí)際類(lèi)型不符合要求時(shí),方法會(huì)立即引發(fā)。35 | 并發(fā)安全字典sync.Map (下)我們?cè)谏弦黄恼轮姓劦搅?,由于并發(fā)安全字典提供的方法涉及的鍵和值的類(lèi)型都是interface{},所以我們?cè)谡{(diào)用這些方法的時(shí)候,往往還需要對(duì)鍵...
摘要:想提升自己,還得多看書(shū)多看書(shū)多看書(shū)下面是我收集到的一些程序員應(yīng)該看得書(shū)單及在線(xiàn)教程,自己也沒(méi)有全部看完。共勉吧當(dāng)然,如果你有好的書(shū)想分享給大家的或者覺(jué)得書(shū)單不合理,可以去通過(guò)進(jìn)行提交。講師溫銘,軟件基金會(huì)主席,最佳實(shí)踐作者。 想提升自己,還得多看書(shū)!多看書(shū)!多看書(shū)!下面是我收集到的一些PHP程序員應(yīng)該看得書(shū)單及在線(xiàn)教程,自己也沒(méi)有全部看完。共勉吧!當(dāng)然,如果你有好的書(shū)想分享給大家的或者...
摘要:入門(mén),第一個(gè)這是一門(mén)很新的語(yǔ)言,年前后正式公布,算起來(lái)是比較年輕的編程語(yǔ)言了,更重要的是它是面向程序員的函數(shù)式編程語(yǔ)言,它的代碼運(yùn)行在之上。它通過(guò)編輯類(lèi)工具,帶來(lái)了先進(jìn)的編輯體驗(yàn),增強(qiáng)了語(yǔ)言服務(wù)。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不覺(jué)已經(jīng)到來(lái)了,總結(jié)過(guò)去的 2017,相信小伙們一定有很多收獲...
摘要:入門(mén),第一個(gè)這是一門(mén)很新的語(yǔ)言,年前后正式公布,算起來(lái)是比較年輕的編程語(yǔ)言了,更重要的是它是面向程序員的函數(shù)式編程語(yǔ)言,它的代碼運(yùn)行在之上。它通過(guò)編輯類(lèi)工具,帶來(lái)了先進(jìn)的編輯體驗(yàn),增強(qiáng)了語(yǔ)言服務(wù)。 showImg(https://segmentfault.com/img/bV1xdq?w=900&h=385); 新的一年不知不覺(jué)已經(jīng)到來(lái)了,總結(jié)過(guò)去的 2017,相信小伙們一定有很多收獲...
閱讀 2244·2021-11-19 09:55
閱讀 2729·2021-11-11 16:55
閱讀 3256·2021-09-28 09:36
閱讀 2037·2021-09-22 16:05
閱讀 3391·2019-08-30 15:53
閱讀 1856·2019-08-30 15:44
閱讀 2975·2019-08-29 13:10
閱讀 1410·2019-08-29 12:30