Classes to handle the multithreading aspects in applications.

Classes

  ClassDescription
Public classDelegateTask
Executes DelegateTask.CallBack in IDispatcher thread. Usually used in synchronization with GUI thread.
Public classGuiDispatcher
Executes tasks in GUI thread.
Public classSequencer
Guarantees execution of ITasks in a sequence.

Interfaces

  InterfaceDescription
Public interfaceIDispatcher
Executes ITask objects with or without blocking the calling thread
Public interfaceITask
Represents a task, that can be executed in the IDispatcher thread

Delegates

  DelegateDescription
Public delegateDelegateTask..::..CallBack
DelegateTask..::..CallBack delegate that is called in IDispatcher thread. Usually used in synchronization with GUI thread.

Remarks

All .Net controls are single-threaded, which means that their methods can be called only in thread where they are running, i.e. the GUI thread. This thread also processes all window messages. To make sure that application will run safely, you have to synchronize threads. There are two types of synchronization: synchronous and asynchronous. These types are available via Control.Invoke() and Control.BeginInvoke() methods accordingly. The main difference between these two methods is that synchronous calls block the calling thread until the end of Control.Invoke() function call, while asynchronous calls block the thread only for the time while a delegate is added to the queue. Delegate code and the calling thread are executed simultaneously.

All things we described above are the basics of multi-threaded application development, especially with regard to GUI interaction. However this seeming simplicity may cause dire consequences. The following example shows, how an application may deadlock.

 Copy imageCopy
//Some class
public class Product
{
    private readonly object synchro = new object();
    private double price;

    //Threadsafe property
    public double Price
    {
        get
        {
            lock (synchro)
            {
                return price;
            }
        }
        set
        {
            //When price is changed, this object sends a notification. 
            lock (synchro)
            {
                price = value;
                if (PriceChanged != null)
                {
                    PriceChanged(this, EventArgs.Empty);
                }
            }
        }
    }

    public event EventHandler PriceChanged;
}

//Some control
public class DeadLockExample : Control
{
    public void SubscribeToProduct(Product product)
    {
        product.PriceChanged += OnProduct_PriceChanged;
    }

    private void OnProduct_PriceChanged(object sender, EventArgs e)
    {
        if (InvokeRequired)
        {
            //Call OnProduct_PriceChanged in the GUI thread.
            Invoke(new EventHandler(OnProduct_PriceChanged), new object[] {sender, e});
        }
        else
        {
            //We are in the GUI thread. The calling thread is blocked by Invoke() method

            //Get a product
            Product updatedProduct = (Product) sender;

            //Get a price... Boom! The GUI thread is deadlocked with the calling thread!
            double price = updatedProduct.Price;
        }
    }
}

//If we change product price in non-GUI thread, the application deadlocks
public void UpdateProductPrice(Product product)
{
    ThreadPool.QueueUserWorkItem(delegate
    {
        product.Price = 10;
    });
}

Today .Net Grid is the only component that supports working with data that is added, removed and updated via non-GUI threads. The grid may work in several modes. We shall describe the most important modes and explain how thread safety is achieved in these modes.

  • Non-event mode. Data is added to the grid and updated as follows:
     Copy imageCopy
    grid.Rows.Add(new object[]{"value1", 123, true, DateTime.Now});
    
    //Data is updated by calling Cell.Value property.
    Row row = ...;
    Cell cell = row[0] ;
    cell.Value = "value2";
    All calls for adding, modifying and removing data are thread safe. In all cases synchronization is performed with synchronous method blocking the calling thread. We also want to add an important note about data updating. Data is usually repainted in two stages firstly, Control.Invalidate() method is called and then the required area is repainted via WM_PAINT message. WM_PAINT message is not generated within Control.Invalidate() function, but arrives when it is completed. From practical point of view this means that data is repainted is already unblocked and therefore there is no risk of deadlock.
  • Event-driven model implies working with objects of classes that implement INotifyPropertyChanged interface. When a grid received a notification from this interface, it repaints, sorts, groups and invalidates data of this sole object. In this case you can use Grid.Threadsafety property to choose whether to use synchronous or asynchronous method.
  • The third method of grid operation is called data source binding. Both Grid.DataSource and Grid.DataMember methods are thread-safe with synchronous data processing method. Dapfor .Net Grid supports data sources that implement IBindingList interface and can receive IBindingList.ListChanged notifications from any thread. Synchronization is performed with synchronous method. We can say that today it is the only existing thread-safe data binding implementation among the grids.

As we have said above, ensuring thread safety is not a trivial task for application developers. Complex modern applications may contain a lot of assemblies. Some of them may contain codes with graphical controls, others may contain business logic, various math libraries, code for TCP/IP interaction, etc. However, limitations related to GUI operation only in one thread and thread synchronization method require unneeded and dangerous dependencies of business logic from graphical components (Control.Invoke / Control.BeginInvoke). This may seriously violate the principle of business logic independence from its presentation. Dapfor .Net Grid doesn't just enable thread synchronization, but also makes it possible to completely avoid such dependencies using an event-driven model. It means that if the application is well architected, business logic assemblies will not (and should not!) depend on Dapfor assemblies and System.Windows.Forms libraries.