(NET) NET (2005 год)

Мультипоточное программирование

На этой страничке рассмотрена одна моя небольшая, но интересная прога, которая одновременно является и запускаюшим классом для моей проги прокси-сервера и классом, запускающим из консольного приложения Windows-Форму и классом, маршализирущим многопоточный вывод в поток формы. В общем-то в этой небольшой проге применены два основных приема работы с потоками, почему я и решил описать здесь именно этот небольшой фрагмент огромной системы...

Когда я только начал писать этот прокси-сервер, его журнал выглядел вот так. Такой простенький вывод формировался всего несколькими строками именно засчет того, что консоль Windows заведомо устойчива к многопоточной работе:

00001: Module StartProxyFromConsole
00002:     Dim PROXY As New PROXY.NET_Listener()
00003:     Sub Main1()
00004:         Console.WriteLine("Корневой поток : " & AppDomain.GetCurrentThreadId.ToString & "  Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString)
00005:         Console.WriteLine(PROXY.SQL_Start & vbCrLf)
00006:         Do While True
00007:             System.Threading.Thread.Sleep(5000)
00008:             Console.WriteLine("Очередь передачи : " & PROXY.GetQueue)
00009:             Console.WriteLine("Потоки передачи : " & PROXY.GetThread)
00010:             Console.WriteLine()
00011:         Loop
00012:     End Sub
00013: End Module
00001: Module Diagnostic
00002:     Public PROXY As NET_Listener = New PROXY.NET_Listener()
00003:     '
00004:     Public Sub Error_Message(ByVal Message As String) 'диагностика
00005:         System.Windows.Forms.MessageBox.Show(Message & vbCrLf & _
00006:         "Thread : " & AppDomain.GetCurrentThreadId.ToString & " (" & _
00007:         System.Threading.Thread.CurrentThread.ApartmentState.ToString & ")", _
00008:         "ProxyServer", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Stop, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00009:     End Sub
00010:     '
00011:     Public Sub Normal_Message(ByVal Message As String)
00012:         'Console.WriteLine(Message & vbCrLf & _
00013:         '"Thread : " & AppDomain.GetCurrentThreadId.ToString & " (" & _
00014:         'System.Threading.Thread.CurrentThread.ApartmentState.ToString & ")" & vbCrLf)
00015:     End Sub
00016: End Module

Понятно, что такой вариант работы в практической жизни не применяется, из-за чего мне пришлось написать нижеследующаю прогу, которая бы формировала вывод в более приличном виде.

00001: Module Diagnostic
00002:     'Так хитро засвечивается форма, чтобы получить ссылку на ее экземпляр
00003:     Dim MyForm As System.Windows.Forms.Form
00004:     Public PROXY As NET_Listener = New PROXY.NET_Listener()
00005:     '
00006:     Public Sub Error_Message(ByVal Message As String) 'диагностика
00007:         System.Windows.Forms.MessageBox.Show(Message & vbCrLf & _
00008:         "Thread : " & AppDomain.GetCurrentThreadId.ToString & " (" & _
00009:         System.Threading.Thread.CurrentThread.ApartmentState.ToString & ")", _
00010:         "ProxyServer", Windows.Forms.MessageBoxButtons.OK, Windows.Forms.MessageBoxIcon.Stop, Windows.Forms.MessageBoxDefaultButton.Button1, Windows.Forms.MessageBoxOptions.ServiceNotification)
00011:     End Sub
00012:     '
00013:     'Тут простой способ вывести журнал как в устойчивую к многопоточности консоль, так и на форму
00014:     '
00015:     Public Sub Normal_Message(ByVal Message As String)
00016:         'Вариант журналирования при запуске прокси из консоли
00017:         'Console.WriteLine(Message & vbCrLf & _
00018:         '"Thread : " & AppDomain.GetCurrentThreadId.ToString & " (" & _
00019:         'System.Threading.Thread.CurrentThread.ApartmentState.ToString & ")" & vbCrLf)
00020:         Dim X As New MultiThreadWriter( _
00021:         Message & vbCrLf & _
00022:         "Thread : " & AppDomain.GetCurrentThreadId.ToString & " (" & _
00023:         System.Threading.Thread.CurrentThread.ApartmentState.ToString & ")" & vbCrLf & vbCrLf)
00024:         X.WriteMessage()
00025:     End Sub
00026:     '
00027:     'Этот класс имеет доступ к обьектам формы и экземпляр 
00028:     'этого класса выполняет модификацию формы в потоке формы
00029:     '
00030:     Public Class MultiThreadWriter
00031:         '
00032:         Dim MyForm1 As frmMain
00033:         Private NewMessage As String
00034:         '
00035:         Public Sub New(ByVal Message As String)
00036:             'конструктор просто запомнит чего надо записать
00037:             NewMessage = Message
00038:             MyForm1 = MyForm
00039:         End Sub
00040:         '
00041:         Private Sub Write()
00042:             'это просто запись в потоке формы
00043:             MyForm1.WriteLog(NewMessage)
00044:             'MyForm.txLog.Text &= NewMessage
00045:         End Sub
00046:         '
00047:         Public Sub WriteMessage()
00048:             'это маршалинг из вызывающего потока в поток формы
00049:             Dim UpdateDelegate As System.Windows.Forms.MethodInvoker = New System.Windows.Forms.MethodInvoker(AddressOf Write)
00050:             MyForm1.Invoke(UpdateDelegate)
00051:         End Sub
00052:         '
00053:     End Class
00054:     '
00055:     Private FormThread As String
00056:     '
00057:     Public Sub Main()
00058:         MyForm = New frmMain()
00059:         FormThread = AppDomain.GetCurrentThreadId.ToString & "  Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString
00060:         Dim ListenerThread As System.Threading.Thread = New Threading.Thread(AddressOf StartListener)
00061:         ListenerThread.Start()
00062:         MyForm.ShowDialog()
00063:         'Здесь этот поток навсегда повиснет в обработке пользовательского ввода
00064:     End Sub
00065:     '
00066:     Private Sub StartListener()
00067:         Call Normal_Message("Поток формы: " & FormThread)
00068:         Call Normal_Message("Корневой поток сервера: " & AppDomain.GetCurrentThreadId.ToString & "  Thread Apartment State: " & System.Threading.Thread.CurrentThread.ApartmentState.ToString)
00069:         Dim x As String = PROXY.SQL_Start
00070:         Call Normal_Message("Конфигурация прослушивания SQL-сервера  " & x)
00071:     End Sub
00072:     '
00073: End Module

Как видите это вроде бы консольное приложение, точка входа в которое находится в строке 57. Однако, оно выводит форму. Как видите экземпляр формы создан в строке 58, после чего в строке 60 создается дополнительный корневой поток прокси сервера (строки 66-71) и форма вывешивается на цикл ввода пользовательского ввода. Если бы написали просто SHOW, то форма бы моргнула на экране, поток, начавшийся в строке 57, в строке 64 бы закончился. И дочерний поток просто бы закрыла система. Насладится работой проги мы бы не смогли.

Вот это один из широко распространенных приемов работы с потоками, в принципе чем-то похож на метод один, рассмотренный здесь, за исключением того, что основной поток подвешивается, а не возвращется в вызывающему приложению и все это происходит в модуле, уже заранее созданном экземпляре класса.

В этой моей проге использована довольно обычная форма, отличающаяся лишь тем, что она незакрываемая, как вы видите в строке 158, активируется она по щелчку на иконке в системном трее и в своем потоке (в потоке чтения пользовательского ввода) обращается к обьектам NET_Listener, запрашивая там данные и слушая команды юзера на удаление потоков в прокси-сервере. В классе формы есть также метод WriteLog, который обновляет TextBox на форме. Вот только вызвать напрямую из потоков прокси-сервера мы этот метод не можем - все повиснет сразу и только волшебная кнопка RESET сможет оживить компец...

00001: Public Class frmMain
00002:     Inherits System.Windows.Forms.Form
......
00150:     Private Sub NotifyIcon1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles NotifyIcon1.Click
00151:         Me.WindowState = Windows.Forms.FormWindowState.Normal
00152:         Me.Visible = True
00153:     End Sub
00154:     '
00155:     Private Sub frmMain_Closing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles MyBase.Closing
00156:         Me.WindowState = Windows.Forms.FormWindowState.Normal
00157:         Me.Visible = True
00158:         e.Cancel = True
00159:     End Sub
00160:     '
.....
00167:     Public Sub WriteLog(ByVal str1 As String)
00168:         If Len(txLog.Text) > 1000 Then
00169:             txLog.Text = Microsoft.VisualBasic.Right(txLog.Text, 1000)
00170:         End If
00171:         Me.txLog.Text &= str1
00172:     End Sub
00173:     '
00174:     Private Sub frmMain_Activated(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Activated
00175:         Me.WindowState = Windows.Forms.FormWindowState.Normal
00176:         Me.Refresh()
00177:     End Sub
00178:     '
00179:     Private Sub frmMain_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles MyBase.Load
00180:         Timer1.Start()
00181:     End Sub
00182:     '
00183:     'Обновление очереди и списка прослушиваемых портов в потоке формы
00184:     Private Sub Timer1_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Timer1.Tick
00185:         Dim txQue As String = PROXY.GetQueue
00186:         Call ParseString(txQue, ListBox2)
00187:         Dim txProc As String = PROXY.GetThread
00188:         Call ParseString(txProc, ListBox1)
00189:         btStop.Enabled = False
00190:     End Sub
00191:     '
00192:     Private Sub ParseString(ByVal Str1 As String, ByVal ListBox As System.Windows.Forms.ListBox)
00193:         ListBox.Items.Clear()
00194:         Dim Start, Stop1, i As Int16
00195:         Start = 1
00196:         For i = 1 To 100
00197:             Stop1 = InStr(Start, Str1, ",")
00198:             If Stop1 = 0 Then
00199:                 ListBox.Items.Add(Mid(Str1, Start, Len(Str1) - Start + 1))
00200:                 Exit For
00201:             Else
00202:                 ListBox.Items.Add(Mid(Str1, Start, Stop1 - Start))
00203:                 Start = Stop1 + 1
00204:             End If
00205:         Next
00206:     End Sub
00207:     '
00208:     Private Sub btStop_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles btStop.Click
00209:         If ListBox1.SelectedIndex = -1 Then Exit Sub
00210:         PROXY.Abort_SVA_Thread(ListBox1.Items(ListBox1.SelectedIndex))
00211:     End Sub
00212:     '
00213:     Private Sub ListBox1_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles ListBox1.SelectedIndexChanged
00214:         btStop.Enabled = True
00215:     End Sub
00216: End Class

Можно удивится такому хитрому запуску формы из-под вроде-бы консольного приложения, однако к сожалению это единственный известный мне способ получить в модуле адрес ЭКЗЕМПЛЯРА ФОРМЫ, а не просто ссылку в никуда. При запуске наоборот (модуля из формы) мы всегда будем получать в модуле сообщение - отсутсвует ссылка на обьект (экземпляр формы). Поэтому форму надо так хитро запускать как дочерний обьект модуля и видеть в классе MultiThreadWriter ее реальный адрес из-вне формы. Вот это как бы первая фишка этой небольшой проги.

Ну и, наконец, основная фишка этой проги - проброс данных из одного потока в другой с помощью специально созданного класса MultiThreadWriter, который заполнит один поток (в конструкторе), и асинхронно прочитает другой (начиная с метода Write). Делается это так. Поток в строке 20 создает экземпляр класса, который хранит у себя, чего именно надо вывести на форму. Вот здесь-то в переменную MyForm1 мы прячем адрес реального экземпляра формы. Затем в строке 24 вызывается в потоке прокси-сервера метод X.WriteMessage() созданного нами экземпляра класса. И вот здесь вся изюминка всей этой проги. Метод Write (строки 41-45 уже будут выполнятся в потоке формы). Этот запрос на выполнение в другом потоке создается в строке 49. После чего в строке 50 происходит расщепление потоков - вызывающий поток возвращается в строку 51->25 и в вызывающую прогу. А созданный нами класс продолжает жить своей жизнью. Похожую технику я применял еще в 2001 году для захвата адресов переходов в контекстном меню здесь.

Поток ввода с экрана (поток формы) увидит созданный нами экземпляр класса MultiThreadWriter (после того, как мы на конкретном экземпляре формы в строке 50 запустили метод INVOKE) и начнет исполнять этот же экземпляр MultiThreadWriter начиная с метода Write уже своем потоке.

Вот такие бывают чудеса. Это четвертая техника мультипоточного программирования...



Comments ( )
Link to this page: //www.vb-net.com/dotnet/tour17/index.htm
< THANKS ME>