DOI: 10.7256/2454-0714.2022.4.39029
EDN: IAXOMA
Received:
25-10-2022
Published:
30-12-2022
Abstract:
When the first version of the .NET Framework was released, there was a pattern in enriched client applications that focused on message processing loops, where an embedded queue was used to pass execution units from worker threads. A generalized ISynchronizeInvoke solution was then developed in which the source thread could queue a delegate to the destination thread and, as an optional option, wait for that delegate to complete. After asynchronous page support was introduced into the ASP.NET architecture, the ISynchronizeInvoke pattern did not work because asynchronous ASP.NET pages are not mapped to a single thread. This was the reason for creating an even more generalized solution – SynchronizationContext, which is the subject of the research. The article uses practical examples to show how to update UI elements from worker threads without breaking thread-safety of the user application. Solutions proposed in this aspect are: using Beginlnvoke or Invoke methods to put this delegate into the UI thread message queue; capturing the UI thread synchronization context via the Current property of the SynchronizationContext class; using the deprecated BackgroundWorker class, which provides an implicit capture of the UI thread synchronization context. The peculiarity of implementation of the SynchronizationContext abstract class in ASP.NET platform is not left unnoticed. Practical recommendations on the use of marshalling mechanism on the example of development of multiclient chat with a centralized server are formulated.
Keywords:
programming, multithreading, Windows Forms, Windows Presentation Foundation, Universal Windows Platform, synchronization context, delegates, NET Framework, parallel programming, design patterns
This article is automatically translated.
Introduction Multithreading can improve performance in Windows Presentation Foundation (WPF), Universal Windows Platform (UWP), and Windows Forms applications, but access to controls is not thread-safe. Multithreading can present code for serious and complex errors. Two or more threads controlling the control can lead to an unstable state and cause competition conditions. This article is devoted to the disclosure of the topic of calling controls in a thread-safe manner using the example of developing a multiclient chat with a centralized server. Multithreading in rich client applications In WPF, UWP and Windows Forms applications, performing time-consuming operations in the main thread reduces the responsiveness of the application, because the main thread also processes the message loop, which is responsible for visualizing and supporting keyboard and mouse events. Therefore, in enriched client applications, where various functionality is implemented, we have to deal with multithreading every now and then. A popular approach involves setting up “worker” threads to perform time-consuming operations. The code in the workflow starts a long operation and updates the user interface when it is completed. However, all enriched client applications support a streaming model in which user interface controls can only be accessed from the thread that created them (usually the main UI thread). Violation of this rule leads either to unpredictable behavior or to the generation of an exception. The latter can be disabled by setting the Control property.CheckForIllegalCrossThreadCalls set to false. Therefore, when it is necessary to update the user interface from the workflow, the request must be redirected to the user interface thread (formally this is called marshalization). Here's what it looks like: - in the WPF application, call the Beginlnvoke or Invoke method on the Dispatcher object of the element;
- in the UWP application, call the RunAsync or Invoke method on the Dispatcher object;
- in the Windows Forms application, call the Beginlnvoke or Invoke method on the control.
All the mentioned methods accept a delegate referring to the method that you want to run. The Beginlnvoke/RunAsync methods work by putting this delegate in the message queue of the UI thread (the same queue that handles events coming from the keyboard, mouse, and timer). The Invoke method does the same thing, but is then blocked until the message is read and processed by the UI thread. For this reason, the Invoke method allows you to get the return value from the method. If the return value is not required, then the Beginlnvoke/RunAsync methods are preferable due to the fact that they do not block the calling component and do not introduce the possibility of deadlock [1,2]. You can imagine that when calling the Application method.Run executes the following pseudocode: while (application is not completed) { Wait for something to appear in the message queue. Something has been received: what kind of messages does it belong to? Keyboard/Mouse message -> start event handler. User message Beginlnvoke -> execute delegate. User message Invoke -> execute the delegate and send the result. } A loop of this kind allows a worker thread to marshal a delegate for execution in a user interface thread. For demonstration purposes, let's assume that there is a WPF window with a text field named txtMessage, the contents of which must be updated by the worker thread after a long task (emulated by calling the Thread method.Sleep). Below is the required code: void Main() { new MyWindow().ShowDialog(); } partial class MyWindow : Window { TextBox txtMessage; public MyWindow() { InitializeComponent(); new Thread (Work).Start(); }
void Work() { Thread.Sleep (5000); // Simulate time-consuming task UpdateMessage ("The answer"); } void UpdateMessage (string message) { Action action = () => txtMessage.Text = message; Dispatcher.BeginInvoke (action); } void InitializeComponent() { SizeToContent = SizeToContent.WidthAndHeight; WindowStartupLocation = WindowStartupLocation.CenterScreen; Content = txtMessage = new TextBox { Width=250, Margin=new Thickness (10), Text="Ready" }; } } After running the code shown, a window appears immediately. After five seconds, the text field is updated. For the case of Windows Forms, the code will be similar, but only the Beginlnvoke method of the Form object is called in it: void UpdateMessage (string message) { Action action = () => txtMessage.Text = message; this.BeginInvoke (action); } It is allowed to have multiple user interface threads if each of them owns its own window. The main scenario can be an application with several high–level windows, which is often called an application with a Single Document Interface (SDI), for example, Microsoft Word. Each SDI window usually displays itself as a separate “application” in the taskbar and for the most part it is functionally isolated from other SDI windows. By providing each such window with its own user interface flow, windows become more responsive. Synchronization contexts In the System namespace.ComponentModel defines an abstract SynchronizationContext class that makes generalization of thread marshalization possible. The need for such a generalization is described in detail in an article by Stephen Cleary [3]. In the APIs for mobile and desktop applications (UWP, WPF, and Windows Forms), instances of SynchronizationContext subclasses are defined and created, which can be accessed via the SynchronizationContext.Current static property (when executed in the UI thread). Capturing this property allows you to later “send” messages to user interface controls from the workflow: partial class MyWindow : Window { TextBox txtMessage; SynchronizationContext _uiSyncContext; public MyWindow() { InitializeComponent(); // Capture the synchronization context for the current UI thread: _uiSyncContext = SynchronizationContext.Current; new Thread (Work).Start(); } void Work() { Thread.Sleep (5000); // Simulate time-consuming task UpdateMessage ("The answer"); } void UpdateMessage (string message) { // Marshal the delegate to the UI thread: _uiSyncContext.Post (_ => txtMessage.Text = message, null); } void InitializeComponent() { SizeToContent = SizeToContent.WidthAndHeight; WindowStartupLocation = WindowStartupLocation.CenterScreen;
Content = txtMessage = new TextBox { Width=250, Margin=new Thickness (10), Text="Ready" }; } } The convenience is that the same approach works with all the enriched APIs. However, not all implementations of SynchronizationContext guarantee the order of execution of delegates or their synchronization (see the table). UI-based implementations of SynchronizationContext satisfy these conditions, whereas ASP.NET SynchronizationContext only provides synchronization. Table. Summary description of SynchronizationContext implementations | Executing delegates in a specific thread | Delegates are executed one at a time | Delegates are executed in the order of the queue | Send can directly call a delegate | Post can directly call a delegate | Windows Forms | Yes | Yes | Yes | If called from a UI thread | Never | WPF/Silverlight | Yes | Yes | Yes | If called from the UI thread | Never |
By default | No | No | No | Always | Never | ASP.NET | No | Yes | No | Always | Always | SynchronizationContext by default does not guarantee either the order of execution or synchronization, where the basic implementation of the Send and Post methods looks like this: public virtual void Send (SendOrPostCallback d, object state) { d (state); } public virtual void Post (SendOrPostCallback d, object state) { ThreadPool.QueueUserWorkItem (d.Invoke, state); } As you can see, Send simply executes a delegate in the calling thread, Post does the same, but using a thread pool for asynchrony. But in APIs, these methods are redefined and implement the message queue concept: calling the Post method is equivalent to calling Beginlnvoke on the Dispatcher object (for WPF) or Control (for Windows Forms), and the Send method is equivalent to Invoke. BackgroundWorker class The BackgroundWorker class allows enriched client applications to start a workflow and report the percentage of work completed without the need for explicit capture of the synchronization context [1]. For example: var worker = new BackgroundWorker { WorkerSupportsCancellation = true }; worker.DoWork += (sender, args) => { // Running in the workflow if (args.Cancel) return; Thread.Sleep(1000); args.Result = 123; }; worker.RunWorkerCompleted += (sender, args) => { // Executed in the user interface thread // Here you can safely update the controls // user interface if (args.Cancelled)
Console.WriteLine("Cancelled"); else if (args.Error != null) Console.WriteLine("Error: " + args.Error.Message); else Console.WriteLine("Result is: " + args.Result); }; worker.RunWorkerAsync(); // Captures the synchronization context // and starts the operation The RunWorkerAsync method starts the operation by triggering the DoWork event in the worker thread from the pool. It also captures the synchronization context, and when the operation completes (or fails), the RunWorkerCompleted event is generated through this context (similar to the continuation sign). The BackgroundWorker class generates large-module parallelism, in which the DoWork event is initiated entirely in the workflow. If you need to update user interface controls in this event handler (in addition to sending a message about the percentage of work completed), then you will have to use Beginlnvoke or a similar method. Approbation of the ISynchronizeInvoke template on the example of application development Let's apply the marshalization mechanism described earlier on the example of developing a multiclient chat with a centralized server (based on a software solution proposed by web developer Andrew Pociu, the author of the already closed geekpedia project). Having discarded some details, we will focus on the capabilities of the application and on the code sections where the principles described earlier are relevant. The application provides real-time messaging over a computer network. The application is based on two modules: the client and the server (Figures 1 and 2 show the user interfaces of these modules). Clients can specify the IP address of the server and the port through which they will exchange messages after connecting to the server, as well as set their name to place it in the title part of the sent messages. All clients connected to the server can simultaneously send messages to the server and see each other's messages through a broadcast that the server simulates. The server module contains information about all connected clients, waits for messages from each and sends incoming messages to all, simulating a broadcast. Figure 1. User interface of the client module Figure 2. User interface of the server module Since the client module uses networking, streaming and multithreaded objects, we have the following declaration: using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; Most of our fields and methods are made private, since accessing them from third-party objects, as well as from child classes, is not required. The following types are used in the Form1 class: - StreamReader and StreamWriter to send and receive messages;
- TcpClient to connect to the server;
- Thread, so that messages from the server are received in parallel.
StreamWriter swSender; StreamReader srReceiver; TcpClient tcpServer; Thread thrMessaging The BtnConnectClick method is responsible for clicking the "Connect" button. void BtnConnectClick(object sender, EventArgs e) { if (connected == false) { InitializeConnection(); } else {
CloseConnection("Disconnected at user's request."); } } Connection initialization is provided by calling the InitializeConnection method. void InitializeConnection() { try { tcpServer = new TcpClient(); tcpServer.Connect(IPAddress.Parse(txtIp.Text), ushort.Parse(txtPort.Text)); connected = true; txtIp.Enabled = false; txtPort.Enabled = false; txtUser.Enabled = false; txtMessage.Enabled = true; btnSend.Enabled = true; btnConnect.Text = "Disconnect"; swSender = new StreamWriter(tcpServer.GetStream()) { AutoFlush = true }; swSender.WriteLine(txtUser.Text); thrMessaging = new Thread(ReceiveMessages); thrMessaging.Start(); } catch (Exception exc) { UpdateLog("Error: " + exc.Message); } } Setting the AutoFlush property to true ensures that the StreamWriter buffer is written to the internal stream every time after calling the WriteLine method on the stream adapter. When an exception occurs, the UpdateLog method is called to display an error message in the client log window. Note that the method call is performed in the user interface thread, and therefore there is no need to marshal the delagate referring to this method. The ReceiveMessages method, which is responsible for receiving messages from the server, is executed in a separate thrMessaging thread. void ReceiveMessages() { try { srReceiver = new StreamReader(tcpServer.GetStream()); string ConResponse = srReceiver.ReadLine(); if (ConResponse[0] == '1') { Invoke(new Action(UpdateLog), "Connected successfully!"); } else { string Reason = "Not connected: "; Reason += ConResponse.Substring(2, ConResponse.Length - 2); Invoke(new Action(CloseConnection), Reason); return; } while (connected) { string s = srReceiver.ReadLine(); if (s == "Administrator: Server is stopped.") Invoke(new Action(CloseConnection), s); else Invoke(new Action(UpdateLog), s); } } catch { Invoke(new Action(CloseConnection),
"The connection to the server is complete."); } } This method implements the following protocol. The first character of the message received from the server signals a successful or unsuccessful client connection. The response from the server can be of two types: - "1", if the connection is successful;
- "0|{Error message}", if the connection failed.
If the connection is unsuccessful, the CloseConnection method is called (connection termination), otherwise – UpdateLog (log update). Both methods update the user interface control. And since Windows Forms applications support a streaming model in which user interface controls are accessible only from the thread that created them, the Invoke method ensures that methods are executed in the context of the user interface thread. If an exception occurs when executing the ReceiveMessages method, the CloseConnection method is called with an error message in the client log window. Reading lines from the NetworkStream stream is provided by the StreamReader stream adapter. The UpdateLog method updates the log in the client window. void UpdateLog(string message) { txtLog.AppendText(message + "rn"); } Sending a message to the server is provided by pressing the "Send" button or the "Enter" key. void BtnSendClick(object sender, EventArgs e) { SendMessage(); } void MessageKeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar == (char)13) SendMessage(); } The SendMessage method ensures that a message is sent to the server if there is at least one character in the text field txtMessage. void SendMessage() { try { if (txtMessage.Text.Length > 0) { swSender.WriteLine(txtMessage.Text); txtMessage.Text = ""; } } catch (Exception exc) { CloseConnection($"Error: {exc.Message}"); } } The CloseConnection method changes the status of form work items and closes thread adapters. At the same time, the closure of adapters leads to the automatic closure of the underlying threads. void CloseConnection(string reason) { txtIp.Enabled = true; txtPort.Enabled = true; txtUser.Enabled = true; txtMessage.Enabled = false; btnSend.Enabled = false; btnConnect.Text = "Connect"; connected = false; swSender.Close(); srReceiver.Close(); txtLog.AppendText(reason + "rn"); } The following are the Form1 constructor and the OnApplicationExit method, which is called when the ApplicationExit event occurs (closing the application). public Form1() {
Application.ApplicationExit += new EventHandler(OnApplicationExit); InitializeComponent(); } void OnApplicationExit(object sender, EventArgs e) { if (connected == true) { connected = false; swSender.Close(); srReceiver.Close(); } } Since the server module uses networking, streaming and multithreaded objects, a competitive collection and objects representing Unicode character encoding, we have the following declaration: using System; using System.Net; using System.Net.Sockets; using System.IO; using System.Threading; using System.Collections.Concurrent; using System.Collections.Generic; using System.Text; The BtnListenClick method sets a new handler for the StatusChanged event. This handler indirectly updates the server module log via the UpdateStatus method by changing the contents of the text field txtLog. readonly ChatServer mainServer; public Form1() { mainServer = new ChatServer(); Application.ApplicationExit += new EventHandler(OnApplicationExit); InitializeComponent(); } void BtnListenClick(object sender, EventArgs e) { if (txtIP.Enabled) { mainServer.StatusChanged += new StatusChangedEventHandler(MainServerStatusChanged); mainServer.SetIPEndPoint(txtIP.Text, txtPort.Text); mainServer.StartListening(); txtIP.Enabled = false; txtPort.Enabled = false; btnListen.Text = "Stop Listening"; txtLog.AppendText("Monitoring is started.rn"); } else mainServer.StopListener(); } void MainServerStatusChanged(object sender, StatusChangedEventArgs e) { Invoke(new Action(UpdateStatus), e.eventMessage); } void UpdateStatus(string message) { txtLog.AppendText(message + "rn"); if (message == "Administrator: Listening is stopped.") { txtIP.Enabled = true; txtPort.Enabled = true; btnListen.Text = "Start Listening"; txtLog.AppendText("Monitoring is stopped.rn"); mainServer.StatusChanged -= new StatusChangedEventHandler(MainServerStatusChanged); } }
Note the presence of the Invoke call in the MainServerStatusChanged method. Understanding the need for such a call lies in the description of the ChatServer class, the full content of which is beyond the scope of this article. Note only the main thing that the call of the StatusChanged event is performed from worker threads, therefore, to ensure thread safety when working with the text field txtLog, the Action delegate(UpdateStatus) must be queued in the message queue of the user interface thread. To view the full description of the project, as well as to test the developed application, you can download the archive [4] from the web resource of the author of the current article. It should be noted that the original project, originally proposed by Andrew Potsu, has been optimized and substantially modified (some objects have been removed or replaced in order to eliminate redundancy of the program code and ensure reliable operation of the client and server modules), for example: - The htUsers and htConnections hash tables have been replaced by the concurrent collection ConcurrentDictionary<string, TcpClient> htUsers, which provides thread safety while accessing the collection from worker threads at the same time (note that this collection contains elements representing the names of users (clients) and associated objects that provide client connections for TCP network services).
- The client and server interfaces are endowed with the ability to specify a port for establishing a TCP connection.
- In order to avoid an emergency termination of the client or server module, the code sections are equipped with try-catch constructs to handle various kinds of exceptions and display the reason for the sudden termination of the application in the log window.
- The code of the onStatusChanged method has been replaced with
StatusChanged?.Invoke(null, e); In multithreaded scenarios, before checking and calling, the delegate must be assigned to a temporary variable in order to avoid an error related to thread safety: var temp = StatusChanged; if (temp != null) temp(this, e); Starting with C#6, the same functionality can be obtained without the temp variable using a null conditional operation: StatusChanged?.Invoke(this, e); Being thread-safe and concise, this is now the best generally accepted way to invoke events. Conclusion A considerable number of concepts need to be studied by those who are faced with multithreaded programming for the first time. The SynchronizationContext class, which is available in Microsoft.NET Framework, implements the concept of synchronization context regardless of the platform (whether ASP.NET , Windows Forms, WPF, Silverlight or something else) and is a help for developers who want to create thread-safe applications. Understanding this concept is useful to any programmer. The article gives a brief and meaningful understanding of the SynchronizationContext class. Practical recommendations on the use of the marshalization mechanism are formed on the example of the development of a multiclient chat with a centralized server. The original project taken as a basis has been substantially refined and is an exemplary example of application development, where it is required to ensure streaming security not only when updating user interface elements, but also with respect to the use of a collection of objects. The latter is achieved by using a competitive collection, which is implemented using lightweight synchronization tools with the exception of locks where they are not needed. The subject of further research on the basis of the author's reserve [5-8] is to endow the developed multiclient chat with a protocol for secure client interaction with a centralized server while maintaining streaming security and reliability of the modules.
References
1. Joseph A., Ben A. C# 7.0 in a Nutshell: The Definitive Reference. – 2017.
2. D. Hutchins, A. Ballman and D. Sutherland, "C/C++ Thread Safety Analysis," 2014 IEEE 14th International Working Conference on Source Code Analysis and Manipulation, 2014, pp. 41-46, doi: 10.1109/SCAM.2014.34.
3. Stephen C. Parallel Computing-It's All About the SynchronizationContext [Electronic resource]. Electronic Journal. February 2011. Vol. 26, No. 2. URL: https://learn.microsoft.com/en-us/archive/msdn-magazine/2011/february/msdn-magazine-parallel-computing-it-s-all-about-the-synchronizationcontext (Date of reference: 23.10.2022).
4. Multiclient chat with a centralized server [Electronic resource]. URL: https://csharpcooking.github.io/practice/Multi-Client-Chat-Server-Correct-Version.zip (Äàòà îáðàùåíèÿ: 31.10.2022).
5. Raikhlin, V.A., Vershinin, I.S., Gibadullin, R.F. et al. Reliable recognition of masked binary matrices. Connection to information security in map systems. Lobachevskii J Math 34, 319–325 (2013). https://doi.org/10.1134/S1995080213040112.
6. Ãèáàäóëëèí Ð.Ô. Organization of secure data transfer in the sensor network based on AVR microcontrollers // Cybernetics and Programming. – 2018. – ¹6. – Ñ.80-86. DOI: 10.25136/2306-4196.2018.6.24048 URL: https://e-notabene.ru/kp/article_24048.html.
7. Vershinin, I.S., Gibadullin, R.F., Pystogov, S.V. et al. Associative Steganography. Durability of Associative Protection of Information. Lobachevskii J Math 41, 440–450 (2020). https://doi.org/10.1134/S1995080220030191.
8. Raikhlin, V.A., Gibadullin, R.F. & Vershinin, I.S. Is It Possible to Reduce the Sizes of Stegomessages in Associative Steganography?. Lobachevskii J Math 43, 455–462 (2022). https://doi.org/10.1134/S1995080222050201.
Peer Review
Peer reviewers' evaluations remain confidential and are not disclosed to the public. Only external reviews, authorized for publication by the article's author(s), are made public. Typically, these final reviews are conducted after the manuscript's revision. Adhering to our double-blind review policy, the reviewer's identity is kept confidential.
The list of publisher reviewers can be found here.
The article submitted for review examines the issues of thread safety when calling controls in Windows Presentation Foundation, Universal Windows Platform and Windows Forms applications, and reveals the mechanism that ensures thread safety. The research methodology is based on the study of literary sources on the topic of the work, consideration of fragments of program codes. The authors of the article attribute the relevance of the work to the fact that multithreading can present code for serious and complex errors, and several threads controlling the control element can lead to an unstable state and cause conditions of competition. The scientific novelty of the reviewed study, according to the reviewer, lies in the generalization, brief and meaningful presentation of the essence of thread-safe calls to controls in various applications. The following sections are structurally highlighted in the article: Introduction, Multithreading in enriched client applications, Synchronization Contexts, BackgroundWorker, Conclusion, Bibliography. The author examines the marshalization process, provides relevant program codes, the publication discusses the Beginlnvoke/RunAsync and Invoke, Send and Post methods, shows their similarities and differences, reflects a summary description of SynchronizationContext implementations in the form of a separate table, which considers such parameters as executing delegates in a certain thread, executing them one at a time, in in the order of the queue, the ability in the Send and Post methods to directly call the delegate. The bibliographic list includes 2 sources – publications by foreign authors on the topic of the article. As a comment, it can be noted, firstly, the authors' use of abbreviations in the title of the article and the headings of its individual sections in a foreign language is not the best solution, since a significant part of readers who are not familiar with the English-language abbreviations used do not receive adequate information about the content of the article and its components. Secondly, the elements of the increment of scientific knowledge are not disclosed in sufficient detail in the text of the article, and the very formulation of the key goal reflected in the Conclusion: "to give a brief and meaningful understanding to the SynchronizationContext class" seems more suitable not for a scientific article, but for a popularization publication. Thirdly, it is unlikely that the list of literature from only two sources can be considered sufficient to reflect different approaches to solving the article in question, it also raises doubts about the appropriateness of using a reference to the first source in relation to the naming of structural sections of the publication, and this is done twice, and the link to the second source is not given at all – therefore, the presence of an appeal to opponents in the the presented material is missing. The reviewed material corresponds to the direction of the journal "Software Systems and Computational Methods", has been prepared on an urgent topic, contains generalizations on the topic under consideration, may be of interest to readers, however, according to the reviewer, the article needs to be finalized in accordance with the comments made. Comments of the editor-in-chief dated 06.11.2022: "The author has finalized the manuscript in accordance with the requirements of the reviewers."
|