Мультипоточное программирование
На этой страничке рассмотрена одна моя небольшая, но интересная прога, которая одновременно является и запускаюшим классом для моей проги прокси-сервера и классом, запускающим из консольного приложения 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 уже своем потоке.
Вот такие бывают чудеса. Это четвертая техника мультипоточного программирования...
|