VB.NET 메인 스레드와 다른 스레드와 동일한 객체를 SyncLock 할 때 조심하자.
소개
병렬 처리라든지 상당히 전부터 일반적으로 되어 왔습니다만,
가끔 SyncLock을 이용한 복수 thread간의 처리 동기를 하고 있는 것을 보여줍니다.
SyncLock… 편리합니다만.
사용법을 잘못하면 교착 상태 일어나 버립니다.
요전날도 있는 최종 사용자로부터
「가끔 화면이 반응하지 않게 되어 1회 PC를 강제 종료하지 않으면 사용할 수 없게 되어 버리는데 」
라는 문의를 받았습니다.
(불행하게도, 타이틀 바가 없는 리사이즈 불가의 화면 전체에 퍼지는 어플리케이션이었다!)
타인이 쓴 소스 코드를 확인해 가는 것이 억권인 사람은 상당히 많을 것 (나도 그 쿠치입니다).
※비즈니스상 이용되는 것은 상당히 레거시인 소스인 것은 많고.
아니 레거시인 것이 나쁘다고 하는 것은 아닙니다만…
그렇기 때문에 이하 가능한 한 간략화한 샘플 코드와 함께, 잘못된 예를 하면.
※실제는 더 낫습니다. 데드락은 드물게 밖에 발생하지 않았기 때문에 만약을 위해.
샘플 코드
코드는 MainForm이라는 Form에 MessageTextBox라는 TextBox를 붙여 넣은 것뿐입니다.
TextBox는 MultiLine을 True로.
솔루션에 프로젝트를 추가하고 Form에 코드를 붙여넣고 빌드하고 실행하면,
그 중 교착 상태로 응용 프로그램을 강제 종료합니다.
뭐, 본 것만으로 위험성은 느껴질까라고는 생각합니다만…
MainForm.vb
Imports System.ComponentModel
Imports System.Threading
''' <summary>
''' 複数スレッドで同一オブジェクトに対するSyncLock利用時のデッドロックサンプル
''' </summary>
Public Class MainForm
''' <summary>
''' メインスレッドとワーカースレッドとでロック対象となるオブジェクト
''' </summary>
Private ReadOnly LockHandler As New Object()
''' <summary>
''' 1秒間隔でSyncLockして画面へログ出力の委譲を試みるワーカー
''' </summary>
Private WithEvents IntervalLockWorker As New BackgroundWorker() With {
.WorkerSupportsCancellation = True
}
''' <summary>
''' 0.5秒間隔でSyncLockして画面へログ出力するMain Threadで動作するタイマー
''' </summary>
Private WithEvents IntervalLockTimer As New Windows.Forms.Timer() With {
.Interval = 500
}
''' <summary>
''' Formロード
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
' 定期的にバックグラウンドでロックさせます。
Me.IntervalLockWorker.RunWorkerAsync()
' タイマーも開始させます。
Me.IntervalLockTimer.Start()
End Sub
''' <summary>
''' 画面のTextBoxへ引数で指定された文字列をログとして追加する
''' </summary>
''' <param name="message"></param>
Private Sub AppendLog(message As String)
Me.MessageTextBox.Text = String.Format(
"{0}{1}{2}{3}{4}",
Now.ToString("yyyy-MM-dd HH:mm:ss.fff"),
vbTab,
message,
vbNewLine,
Me.MessageTextBox.Text
)
End Sub
''' <summary>
''' バックグランドで1秒ごとにSyncLockしつつUI更新を試みるDoWorkイベント
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub IntervalLockWorker_DoWork(sender As Object, e As DoWorkEventArgs) Handles IntervalLockWorker.DoWork
While (True)
SyncLock Me.LockHandler
If (Me.IntervalLockWorker.CancellationPending) Then
' キャンセル処理は決して到達しないダミーです。
' 無限ループって怖いね。
e.Cancel = True
Return
' NOTREACHED
End If
' ロック中にMain ThreadへUI更新を要求します。
' Main Threadがロック解除待ちなら、ここでデッドロック!!!(当たり前だよなぁ? )
Me.Invoke(New Action(Of String)(AddressOf Me.AppendLog), "IntervalLockWorker_DoWork")
Thread.Sleep(1000)
End SyncLock
End While
End Sub
''' <summary>
''' Main Threadで一定間隔でSyncLockしつつUI更新を試みるTimerイベント
''' </summary>
''' <param name="sender"></param>
''' <param name="e"></param>
Private Sub IntervalLockTimer_Tick(sender As Object, e As EventArgs) Handles IntervalLockTimer.Tick
SyncLock Me.LockHandler
' IntervalLockWorkerがLock中なら、Lock解除後に以下のステップが実行される。
Call Me.AppendLog("IntervalLockTimer_Tick")
End SyncLock
End Sub
End Class
【대략 33초 후에 데드락이 일어나는 예】※17:17:13경에 발생합니다.
해설
더 이상 필요하지 않습니다.
서로가 서로를 기다리고 있다는 약간의 애수를 느끼지 않고는 있을 수 없는 문제였습니다.
해결책
IntervalLockTimer_Tick 이벤트에서 SyncLock을 수행하지 않도록 합니다.
다만, 경우에 따라서는 그것만으로는 해결할 수 없는 것이 있기 때문에, 실현해야 하는 요건과 함께 요확인, 주의를.
원래 Invoke를 다른 스레드에서하는 것은 SyncLock 밖으로 낸다는 것도 한 방안일지도 모릅니다.
※BeginInvoke를 실시하면 상기 샘플 코드에서는 데드락은 일어나지 않습니다.
…하지만, 다른 이유로 데드락을 유발하고 있는 소스 코드도 있을지도…
어쨌든 멀티스레드로의 자원 액세스 관리는 요주의군요!
어쨌든, 이번에는 특별한 SyncLock을 할 필요가없는 것이 판명되어,
Main Thread의 SyncLock을 삭제하기 때문에 문제는 해결되었습니다.
교훈
도구는 사용법을 확실히 확인하고 올바르게 사용합시다.
추가
소스 코드를 붙여 움직이는 것도 귀찮은 사람도 있을지도 모르기 때문에 실제의 거동을 gif로서 붙여 넣습니다.
Reference
이 문제에 관하여(VB.NET 메인 스레드와 다른 스레드와 동일한 객체를 SyncLock 할 때 조심하자.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://qiita.com/Toshino3/items/2b3ac430b40097247f45텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)