2013-10-15 174 views
2

我對併發編程非常陌生,所以我遇到了一個需要解決的死鎖問題。F#解決死鎖問題

因此,對於下面的代碼,它不打印任何我認爲肯定會出現死鎖的東西,儘管我不太確定它是如何發生的。

let sleepMaybe() = if (random 4) = 1 then Thread.Sleep 5 

type account(name:string) = 
    let balance = ref 1000 
    member this.Balance = lock this <| fun() -> !balance 
    member this.Name = name 
    member this.Withdraw amount = balance := !balance - (sleepMaybe(); amount) 
    member this.Deposit amount = balance := !balance + (sleepMaybe(); amount) 

    member this.Transfer (toAcc:account) amount = 
     lock this <| fun() -> lock toAcc <| fun() -> toAcc.Deposit amount 
           this.Withdraw amount 


let doTransfers (acc:account) (toAcc:account)() = 
    for i in 1..100 do acc.Transfer toAcc 100 
    printfn "%s balance: %d Other balance: %d" acc.Name acc.Balance toAcc.Balance 

let q2main() = 
    let acc1=account("Account1") 
    let acc2=account("Account2") 

    startThread (doTransfers acc1 acc2) 
    startThread (doTransfers acc2 acc1) 

q2main()   
+0

你需要發佈你的實際代碼。此代碼不起作用。 –

+0

例如,它不是隨機的,而是隨機的(並且F#區分大小寫)。另外,我沒有知道的「startThread」,但有一個Thread.Start。 –

+1

正如您從答案中看到的一樣,使用正確的鎖定是一項相當大的挑戰。作爲C#濫用者,我推薦學習如何正確使用鎖,但是儘可能地避免它們。通過傳遞消息而不是共享狀態可以更容易地解決大多數問題。查看F#中的MailboxProcessor。 – stmax

回答

3

您正在鎖定實例本身並要求鎖定兩個實例才能傳輸內容。這是死鎖的祕訣。

  • 線程ACC1 1把門鎖上開始ACC2轉移
  • 線程2個鎖開始轉移
  • 線程1個等待對ACC2的鎖被釋放,因此它可以完成其傳輸
  • 線程2等待acc1上的鎖被釋放,以便它可以完成其傳輸

他們將各自等待對方無限期地釋放它們的鎖。

如果一次獲得多個鎖,總是獲取相同的順序的鎖。也就是說,儘量不要通過改變你的對象職責來同時需要多個鎖。

例如,提款和存款是兩個不相關的獨立操作,但它們修改餘額。您正試圖用鎖鎖定餘額。一旦帳戶的餘額發生變化,保留該鎖就沒有意義了。另外,我建議賬戶的責任不在於知道如何轉移到其他賬戶。

考慮到這一點,以下是消除死鎖的變化。

type Account(name:string) = 
    let mutable balance = 1000 
    let accountSync = new Object() 

    member x.Withdraw amount = lock accountSync 
            (fun() -> balance <- balance - amount) 
    member x.Deposit amount = lock accountSync 
            (fun() -> balance <- balance + amount) 

let transfer amount (fromAccount:Account) (toAccount:Account) = 
    fromAccount.Withdraw(amount) 
    toAccount.Deposit(amount) 
+0

我很困惑,爲什麼你使用一個新的對象進行鎖定,而我們只能鎖定類對象本身(即鎖定x)。我知道你的代碼有效,但我不完全確定爲什麼。 – user2431438

+1

@ user2431438這是一項預防措施。就功能而言,只要它對所有影響給定事件(在這種情況下爲餘額)的操作都是相同的實例,那麼鎖定哪個實例並不重要。然而,鎖定類實例本身允許_anyone能夠看到instance_將其鎖定,可能會影響Account類型有效管理自身的內部能力(例如,某人在該實例上出現死鎖並且現在該帳戶被凍結)。在類型中創建一個私有實例確保只有Account類型可以使用它。 –

+1

@ user2431438:您不應該鎖定外部可見對象,因爲您的庫的用戶也可以。因此,像克里斯那樣使用私人對象是一種好習慣。 –

2

克里斯解釋了死鎖的原因,但該解決方案必須包括用於傳輸(假定一個存款可能失敗由於透支等)的整體鎖定兩個帳戶。您正在努力爭取一種形式的事務內存。以下是一種方法:

open System 
open System.Threading 
open System.Threading.Tasks 

type Account(name) = 
    let mutable balance = 1000 
    member val Name = name 
    member __.Balance = balance 
    member private __.Deposit amount = 
    balance <- balance + amount 
    member val private Lock = obj() 
    member this.Transfer (toAccount: Account) amount = 
    let rec loop() = 
     let mutable retry = true 
     if Monitor.TryEnter(this.Lock) then 
     if Monitor.TryEnter(toAccount.Lock) then 
      this.Deposit(-amount) 
      toAccount.Deposit(amount) 
      Monitor.Exit(toAccount.Lock) 
      retry <- false 
     Monitor.Exit(this.Lock) 
     if retry then loop() 
    loop() 

let printLock = obj() 

let doTransfers (acc:Account) (toAcc:Account) threadName = 
    for i in 1..100 do 
     acc.Transfer toAcc 100 
     lock printLock (fun() -> 
     printfn "%s - %s: %d, %s: %d" threadName acc.Name acc.Balance toAcc.Name toAcc.Balance) 

[<EntryPoint>] 
let main _ = 
    let acc1 = Account("Account1") 
    let acc2 = Account("Account2") 
    Task.WaitAll [| 
     Task.Factory.StartNew(fun() -> doTransfers acc1 acc2 "Thread 1") 
     Task.Factory.StartNew(fun() -> doTransfers acc2 acc1 "Thread 2") 
    |] 
    printfn "\nDone." 
    Console.Read() 
+0

感謝丹尼爾,但我儘量不要使用輪詢或郵箱來完成此任務:) – user2431438