VB.NET-Tipp 0003: Asynchrones Streaming
von Spatzenkanonier
Beschreibung
Zeitaufwändige Stream-Operationen können zum Einfrieren der Benutzeroberfläche führen. Daher hat Microsoft die Stream-Klasse mit asynchronen Lese- und Schreibfunktionen ausgestattet. Die Verwendung dieser Funktionen ist allerdings wesentlich umständlicher als selbst einen Nebenthread zu erstellen (explizites Threading) oder einen Delegaten asynchron aufzurufen und dann (im Nebenthread) synchron zu operieren.
Dieser Tipp stellt die oben genannten Varianten des Threadings einander gegenüber: Stream.BeginRead, Delegate.BeginInvoke, explizites Threading und Backgroundworker.
Schwierigkeitsgrad: | Framework-Version(en): .NET Framework 2.0, .NET Framework 3.0, .NET Framework 3.5 | .NET-Version(en): Visual Basic 2005, Visual Basic 2008 | Download: |
' Dieser Quellcode stammt von http://www.activevb.de ' und kann frei verwendet werden. Für eventuelle Schäden ' wird nicht gehaftet. ' Um Fehler oder Fragen zu klären, nutzen Sie bitte unser Forum. ' Ansonsten viel Spaß und Erfolg mit diesem Source! ' Projektversion: Visual Studio 2005 ' Option Strict: An ' Option Infer: An ' ' Referenzen: ' - System ' - System.Drawing ' - System.Windows.Forms ' ' Imports: ' - Microsoft.VisualBasic ' - Microsoft.VisualBasic.ControlChars ' - System ' - System.Windows.Forms ' ' ############################################################################## ' ############################## CrossThread.vb ################################ ' ############################################################################## ''' <summary> ''' Stellt Methoden bereit, mit denen ein beliebiger Methoden-Aufruf mit bis zu ''' 3 Argumenten in einen Nebenthread verlegt werden kann, bzw. aus einem ''' Nebenthread in den Hauptthread ''' </summary> Public Class CrossThread Public Shared Sub RunAsync(Of T1, T2, T3)( _ ByVal Action As Action(Of T1, T2, T3), ByVal Arg1 As T1, _ ByVal Arg2 As T2, ByVal Arg3 As T3) ' Aufruf von Action.EndInvoke() gewährleisten, indem er als Callback- ' Argument mitgegeben wird Action.BeginInvoke(Arg1, Arg2, Arg3, _ New AsyncCallback(AddressOf Action.EndInvoke), Nothing) End Sub Public Shared Sub RunAsync(Of T1, T2)( _ ByVal Action As Action(Of T1, T2), ByVal Arg1 As T1, ByVal Arg2 As T2) Action.BeginInvoke(Arg1, Arg2, _ New AsyncCallback(AddressOf Action.EndInvoke), Nothing) End Sub Public Shared Sub RunAsync(Of T1)(ByVal Action As Action(Of T1), _ ByVal Arg1 As T1) Action.BeginInvoke(Arg1, _ New AsyncCallback(AddressOf Action.EndInvoke), Nothing) End Sub Public Shared Sub RunAsync(ByVal Action As Action) Action.BeginInvoke(New AsyncCallback(AddressOf Action.EndInvoke), _ Nothing) End Sub Private Shared Function GuiCrossInvoke(ByVal Action As [Delegate], _ ByVal ParamArray Args() As Object) As Boolean If Application.OpenForms.Count = 0 Then ' Wenn keine Form mehr da ist, so tun, als ob das Invoking ' ausgeführt wäre Return True End If If Application.OpenForms(0).InvokeRequired Then Application.OpenForms(0).BeginInvoke(Action, Args) Return True End If End Function Public Shared Sub RunGui(Of T1, T2, T3)( _ ByVal Action As Action(Of T1, T2, T3), ByVal Arg1 As T1, _ ByVal Arg2 As T2, ByVal Arg3 As T3) ' Wenn kein Invoking erforderlich ist, den Action-Delegaten ' direkt ausführen If Not GuiCrossInvoke(Action, Arg1, Arg2, Arg3) Then _ Action(Arg1, Arg2, Arg3) End Sub Public Shared Sub RunGui(Of T1, T2)(ByVal Action As Action(Of T1, T2), _ ByVal Arg1 As T1, ByVal Arg2 As T2) If Not GuiCrossInvoke(Action, Arg1, Arg2) Then Action(Arg1, Arg2) End Sub Public Shared Sub RunGui(Of T1)(ByVal Action As Action(Of T1), _ ByVal Arg1 As T1) If Not GuiCrossInvoke(Action, Arg1) Then Action(Arg1) End Sub Public Shared Sub RunGui(ByVal Action As Action) If Not GuiCrossInvoke(Action) Then Action() End Sub End Class ' ############################################################################## ' ########################### frmAsyncStreaming.vb ############################# ' ############################################################################## Imports System.IO Public Class frmAsyncStreaming Private Const _Output As String = "Output" Private Const _Src As String = "Output\TestSource.Data" Private Const _Dest As String = "Output\TestDest" Private Sub CreateTestSource() Me.RichTextBox1.AppendText("erstelle TestSource - bitte warten" & _ Lf & Lf) ' Der Using-Block ist zeitaufwändig und friert das Gui ein. Vorher ' die Neuzeichnung der Richtextbox explizit erzwingen Me.RichTextBox1.Refresh() If Not Directory.Exists(_Output) Then Directory.CreateDirectory(_Output) ' Output-Ordner im Browser anzeigen Diagnostics.Process.Start(_Output) Using _ Reader As New FileStream(Application.ExecutablePath, _ FileMode.Open, FileAccess.Read), _ Writer As New FileStream(_Src, FileMode.Create) ' Große Datei generieren ( > 30MB) For I As Integer = 0 To 999 Reader.Position = 0 StreamX.WriteTo(Reader, Writer) Next End Using End Sub Private Sub Button_Click(ByVal sender As Object, ByVal e As EventArgs) _ Handles btStreamBegin.Click, btDelegatBegin.Click, _ btExplicitThread.Click, btCopySynchron.Click, btBackgroundworker.Click Static ID As Integer ID += 1 Dim Dest As String = _Dest & ID Dim Cause As String = "" Dim sFormat As String = "{0,-14:HH:mm:ss.FFF} starte {1} -> {2}" If Not File.Exists(_Src) Then CreateTestSource() Select Case True Case sender Is btStreamBegin Dest &= ".StreamBeginRead" Cause = "Stream.BeginRead()" CopyFileMS(_Src, Dest) Case sender Is btDelegatBegin ' Mein Favorit - keine zusätzlichen Methoden erforderlich ' (bzw. die zusätzlichen Methoden sind allgemein ' wiederverwendbar) Cause = "Delegate-Invoking" Dest &= ".Invoking" CrossThread.RunAsync(AddressOf CopyFile, Cause, _Src, Dest) Case sender Is btExplicitThread Cause = "explicit Threading" Dest &= ".Threading" Dim Tr As New Threading.Thread( _ AddressOf ExplicitThreadStart) Tr.Start(New String() {Cause, _Src, Dest}) Case sender Is btBackgroundworker Dest &= ".Backgroundworker" ' BackgroundWorker hat die Eigenart, daß er nur einen Job ' erledigen kann If BackgroundWorker1.IsBusy Then Cause = "BackgroundWorker1.IsBusy = True " & _ "- Kann grad nicht" Exit Select End If Cause = "Backgroundworker" BackgroundWorker1.RunWorkerAsync(New String() _ {Cause, _Src, Dest}) Case sender Is btCopySynchron Cause = "synchrones kopieren" Dest &= ".Synchron" WriteLine(String.Format(sFormat, Now, _ Cause, Path.GetFileName(Dest))) CopyFile(Cause, _Src, Dest) Return End Select ' Diese Meldung wird **vor** den Erfolgsmeldungen aus den ' Nebenthreads angezeigt. WriteLine(String.Format(sFormat, Now, Cause, Path.GetFileName(Dest))) End Sub Private Sub ExplicitThreadStart(ByVal Obj As Object) Dim Args As String() = DirectCast(Obj, String()) CopyFile(Args(0), Args(1), Args(2)) End Sub Private Sub BackgroundWorker1_DoWork(ByVal sender As Object, _ ByVal e As System.ComponentModel.DoWorkEventArgs) _ Handles BackgroundWorker1.DoWork ' Der Vollständigkeit halber hier auch die Verwendung des ' BackgroundWorkers. Dieser bietet noch zusätzlich das Event ' _RunWorkerCompleted, welches hier aber nicht genutzt wird, da ' CopyFile schon einen threadsicheren Mechanismus implementiert, um die ' Erfolgsmeldung anzuzeigen Dim Args As String() = DirectCast(e.Argument, String()) CopyFile(Args(0), Args(1), Args(2)) End Sub Private Sub CopyFile(ByVal Cause As String, ByVal Src As String, _ ByVal Dest As String) ' Nur das Threading mit Stream.BeginRead kann diese Methode in dieser ' Form nicht verwenden Using Reader As New FileStream(Src, _ FileMode.Open, FileAccess.Read, FileShare.Read), _ Writer As New FileStream(Dest, FileMode.Create) StreamX.WriteTo(Reader, Writer) End Using WriteLine(String.Format("{0,-14:HH:mm:ss.FFF} beende {1} ({2})", _ Now, Cause, Path.GetFileName(Dest))) End Sub Private Sub WriteLine(ByVal Text As String) CrossThread.RunGui(AddressOf Me.RichTextBox1.AppendText, Text & Lf) End Sub #Region "Asynchroner Kopiervorgang in der von MS vorgesehenen Umständlichkeit" Private Structure AsyncInfo Public Reader As Stream Public Buffer As Byte() Public WriterName As String Public Sub New(ByVal Reader As Stream, _ ByVal Buffer As Byte(), ByVal WriterName As String) Me.Reader = Reader Me.Buffer = Buffer Me.WriterName = WriterName End Sub End Structure Private Sub CopyFileMS(ByVal Src As String, ByVal Dest As String) Dim Buf(Short.MaxValue - 1) As Byte Dim Reader As New FileStream( _ Src, FileMode.Open, FileAccess.Read, FileShare.Read, _ Buf.Length, FileOptions.Asynchronous) Reader.BeginRead( _ Buf, 0, Buf.Length, AddressOf Callback, _ New AsyncInfo(Reader, Buf, Dest)) End Sub Private Sub Callback(ByVal Ar As IAsyncResult) With DirectCast(Ar.AsyncState, AsyncInfo) ' Suboptimales Design: Der Read-Stream muss in einer anderen ' Methode und in einem anderen Thread disposed werden, als in dem ' er erstellt wurde Using Reader As Stream = _ .Reader, Writer As New FileStream(.WriterName, FileMode.Create) Dim Portion As Integer = Reader.EndRead(Ar) Writer.Write(.Buffer, 0, Portion) While Portion = .Buffer.Length Portion = Reader.Read(.Buffer, 0, .Buffer.Length) Writer.Write(.Buffer, 0, Portion) End While End Using End With WriteLine(String.Format("{0,-14:HH:mm:ss.FFF} beende " & _ "Stream.EndRead()", Now)) End Sub 'Asynchroner Kopiervorgang in der von MS vorgesehenen Umständlichkeit #End Region Private Sub frmAsyncStreaming_FormClosed(ByVal sender As Object, _ ByVal e As FormClosedEventArgs) Handles MyBase.FormClosed ' Das schließt kurioserweise den Browser (ich glaub, der stürzt ab) Directory.Delete(_Output, True) End Sub End Class ' ############################################################################## ' ################################ StreamX.vb ################################## ' ############################################################################## Imports System.Runtime.CompilerServices Imports System.IO Public Module StreamX ' Die Warnung ist zwar keine Summary, aber so wichtig, dass sie ' in der Intellisense angezeigt werden soll ''' <summary> ''' Achtung! Bei endlosem ReadStream (z.B. System.Net.Sockets.NetworkStream) ''' ist die Angabe von Count **erforderlich**, nicht optional! ''' </summary> Public Sub WriteTo( _ ByVal ReadStream As Stream, _ ByVal WriteStream As Stream, _ Optional ByVal Count As Long = Long.MaxValue, _ Optional ByVal Bufsize As Integer = Short.MaxValue) ' Hinweis für VB2008-Nutzer: Diese Sub ist eigentlich als ' Extension der Stream-Klasse gedacht Dim Buf(Bufsize - 1) As Byte Dim ReadCount As Long = 0 While Count > ReadCount Dim Portion As Integer = _ ReadStream.Read(Buf, 0, _ CInt(Math.Min(Count - ReadCount, Bufsize))) If Portion = 0 Then If Count = Long.MaxValue Then Return Throw New ArgumentException(String.Concat( _ "StreamX.WriteTo(ReadStream, WriteStream, Count:=", _ Count, "): ReadStream konnte ", "nur ", ReadCount, _ " Bytes lesen")) End If ReadCount += Portion WriteStream.Write(Buf, 0, Portion) End While End Sub End Module ' ############################################################################## ' ################################# System.vb ################################## ' ############################################################################## Namespace System ' Hinweis für VB2008-Nutzer: Diese 3 Delegaten sind in ' System.Core bereits vordefiniert Public Delegate Sub Action() Public Delegate Sub Action(Of T1, T2) _ (ByVal Arg1 As T1, ByVal Arg2 As T2) Public Delegate Sub Action(Of T1, T2, T3) _ (ByVal Arg1 As T1, ByVal Arg2 As T2, ByVal Arg3 As T3) End Namespace
Ihre Meinung
Falls Sie Fragen zu diesem Artikel haben oder Ihre Erfahrung mit anderen Nutzern austauschen möchten, dann teilen Sie uns diese bitte in einem der unten vorhandenen Themen oder über einen neuen Beitrag mit. Hierzu können sie einfach einen Beitrag in einem zum Thema passenden Forum anlegen, welcher automatisch mit dieser Seite verknüpft wird.
Archivierte Nutzerkommentare
Klicken Sie diesen Text an, wenn Sie die 4 archivierten Kommentare ansehen möchten.
Diese stammen noch von der Zeit, als es noch keine direkte Forenunterstützung für Fragen und Kommentare zu einzelnen Artikeln gab.
Aus Gründen der Vollständigkeit können Sie sich die ausgeblendeten Kommentare zu diesem Artikel aber gerne weiterhin ansehen.
Kommentar von Eckard Ahlers am 27.04.2010 um 00:39
Normalerweise sind bei DateiOperationen die Daten ja sofort da, also ruft das OS sofort zurück, und der thread ist belegt.
Evtl ist .BeginRead für NetStreams von Interesse.
Mir jedenfalls ist es bis jetzt nicht gelungen, eine Umgebung zu schreiben, die genanntes Verhalten demonstriert, und vorteile zB. gegenüber dem Verhalten des threadPools (delegate.BeginInvoke) herausarbeitet.
Kommentar von Dario am 26.04.2010 um 12:19
Das wesentliche an asynchronen Dateioperationen ist ja, dass sie keinen Thread blockieren. Man kann immer nur eine begrenzte Zahl von Threads erstellen und wirklich effizient ist das eh nur, wenn sie sich bspw. auf mehrere Kerne verteilen. Das geniale an Begin/End ist ja eben, dass sie keinen Arbeitsthread belegen, sondern das Betriebssystem einfach "zurückruft", wenn es Daten gibt.
Kommentar von Eckard Ahlers am 11.11.2007 um 13:33
Hmm.
Das wesentliche am Streaming, nämlich, daß der Stream häppchenweise in einen Buffer gelesen wird, ist nicht implementiert.
Dabei ist grade das bei asynchronen Dateizugriffen besonders kniffelig.
Um 1000 Zeichen zu lesen oder zu schreiben würde man doch nie Threading einsetzen.
Kommentar von willibrord oomen am 18.04.2005 um 19:49
Friend Sub UserDocument_AsyncReadProgress(AsyncProp As AsyncProperty)
If AsyncProp.StatusCode = vbAsyncStatusCodeConnecting then
[how to write to code to unload this userdocument ?]
End Sub
ps: ich lese deutsch aber kann es nich rechtschreiben ... entschüldigen sie mich bitte.