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경에 발생합니다.


해설



더 이상 필요하지 않습니다.
  • 타이머의 500밀리초 간격으로 발생하는 Tick 이벤트가 LockHandler를 SyncLock하면서 UI 갱신(Main Thread)
  • BackGround에서 1000 밀리 초 간격으로 LockHandler를 SyncLock하면서 UI 업데이트 (Main Thread에 업데이트 위양)
  • 2. SyncLock 중에 1. SyncLock이 발생하면 Main Thread가 UI 업데이트를 시도합니다.

    서로가 서로를 기다리고 있다는 약간의 애수를 느끼지 않고는 있을 수 없는 문제였습니다.

    해결책



    IntervalLockTimer_Tick 이벤트에서 SyncLock을 수행하지 않도록 합니다.
    다만, 경우에 따라서는 그것만으로는 해결할 수 없는 것이 있기 때문에, 실현해야 하는 요건과 함께 요확인, 주의를.
    원래 Invoke를 다른 스레드에서하는 것은 SyncLock 밖으로 낸다는 것도 한 방안일지도 모릅니다.
    ※BeginInvoke를 실시하면 상기 샘플 코드에서는 데드락은 일어나지 않습니다.
     …하지만, 다른 이유로 데드락을 유발하고 있는 소스 코드도 있을지도…
    어쨌든 멀티스레드로의 자원 액세스 관리는 요주의군요!

    어쨌든, 이번에는 특별한 SyncLock을 할 필요가없는 것이 판명되어,
    Main Thread의 SyncLock을 삭제하기 때문에 문제는 해결되었습니다.

    교훈



    도구는 사용법을 확실히 확인하고 올바르게 사용합시다.

    추가



    소스 코드를 붙여 움직이는 것도 귀찮은 사람도 있을지도 모르기 때문에 실제의 거동을 gif로서 붙여 넣습니다.
  • 좋은 웹페이지 즐겨찾기