Real-Time Examples of Synchronization Proxy Design Pattern in C#

Real-Time Examples of Synchronization Proxy Design Pattern in C#

In this article, I will discuss the Real-Time Examples of Synchronization Proxy Design Patterns in C#. Please read our previous article, discussing Real-Time Examples of the Logging Proxy Design Pattern in C#. At the end of this article, you will understand the following pointers.

  1. What is the Synchronization Proxy Design Pattern in C#?
  2. Multiple Real-Time Examples of Synchronization Proxy Design Pattern in C#
  3. Advantages and Disadvantages of Synchronization Proxy Design Pattern in C#
  4. When to use Synchronization Proxy Design Pattern in C#?
Synchronization Proxy Design Pattern in C#

The Synchronization Proxy is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. This pattern often appears when an object’s methods must be called in a specific sequence or with concurrency constraints.

This pattern is particularly useful when working with multithreaded environments where multiple threads might attempt to call an object’s method simultaneously. The Synchronization Proxy ensures the methods are called safely by managing concurrent access. Let’s break it down with a simple example in C#. Imagine we have a class Data which needs to be accessed by multiple threads:

using System;
using System.Threading.Tasks;

namespace SynchronizationProxyDesignPattern
{
    //Imagine we have a class Data which needs to be accessed by multiple threads
    public class Data
    {
        public void SomeOperation()
        {
            Console.WriteLine("Data operation performed.");
        }
    }

    //Now, let's create a Synchronization Proxy for this class
    public class DataProxy
    {
        private readonly Data _data = new Data();
        private readonly object _lock = new object();

        public void SafeOperation()
        {
            lock (_lock)
            {
                _data.SomeOperation();
            }
        }
    }

    //In the code above, the DataProxy class acts as a proxy for the Data class. 
    //We've introduced a lock mechanism in the SafeOperation method to ensure that 
    //the method from the Data class is thread-safe.
    
    //Client Code
    //Testing Synchronization Proxy Design Pattern
    public class Program
    {
        public static void Main()
        {
            //Here's how you might use the proxy
            DataProxy proxy = new DataProxy();

            Task.Run(() => proxy.SafeOperation());
            Task.Run(() => proxy.SafeOperation());
            Console.ReadKey();
        }
    }
}

When the above program is run, even though two tasks are running concurrently and trying to call the SafeOperation method simultaneously, the lock in the DataProxy class ensures that only one thread can execute the method simultaneously, preventing potential issues due to concurrent access.

This is a basic example, and the pattern can be extended or modified to suit more complex use cases, such as acquiring and releasing multiple resources, handling timeouts, etc. The primary goal of the Synchronization Proxy is to control access and ensure safety, especially in a multithreaded environment.

Real-Time Example of Synchronization Proxy Design Pattern in C#

Consider a real-time example: a shared printer in an office environment. Multiple employees (represented by threads) want to print their documents on this shared printer. We can use the Synchronization Proxy pattern to prevent clashes and ensure that documents are printed without interruption.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace SynchronizationProxyDesignPattern
{
    //Printer(The actual object that does the printing) :
    public class Printer
    {
        public void PrintDocument(string document)
        {
            Console.WriteLine($"Printing: {document}");
            Thread.Sleep(1000); // Simulate the time taken to print.
            Console.WriteLine($"Finished Printing: {document}");
        }
    }

    //PrinterProxy(The Synchronization Proxy) :
    public class PrinterProxy
    {
        private readonly Printer _printer = new Printer();
        private readonly object _lock = new object();

        public void Print(string document)
        {
            lock (_lock)
            {
                _printer.PrintDocument(document);
            }
        }
    }

    //Client Code
    //Testing Synchronization Proxy Design Pattern
    public class Program
    {
        public static void Main()
        {
            PrinterProxy proxy = new PrinterProxy();

            // Employees trying to print their documents simultaneously
            Task.Run(() => proxy.Print("Document A"));
            Task.Run(() => proxy.Print("Document B"));
            Task.Run(() => proxy.Print("Document C"));
            Console.ReadKey();
        }
    }
}

When the above code is executed, even though three tasks (representing employees) are trying to print their documents simultaneously, the lock in the PrinterProxy ensures that only one document is printed at any given time. As a result, the print operations are serialized, preventing potential issues.

Real-Time Example of Synchronization Proxy Design Pattern in C#

This example is a simplistic representation. In a real-world scenario, the proxy might need to handle priorities (some documents might have higher printing priority), notify users when their document is printed, handle printer errors, manage a print queue, etc. The main idea is that the synchronization logic is encapsulated within the proxy, ensuring safe access to the shared printer resource.

Real-Time Example of Synchronization Proxy Design Pattern in C#

Let’s see another real-time example: managing access to an online bank account by multiple devices or applications. Imagine a scenario where you’re building a banking application with web and mobile interfaces. Multiple devices or instances of the application may try to access and modify the account balance simultaneously. In such situations, consistency is paramount; we must avoid scenarios where concurrent modifications lead to incorrect balance calculations.

using System;
using System.Threading.Tasks;

namespace SynchronizationProxyDesignPattern
{
    //BankAccount(The actual object that manages the balance) :
    public class BankAccount
    {
        private decimal _balance;

        public BankAccount(decimal initialBalance)
        {
            _balance = initialBalance;
        }

        public void Deposit(decimal amount)
        {
            Console.WriteLine($"Depositing: ${amount}");
            _balance += amount;
            Console.WriteLine($"New Balance: ${_balance}");
        }

        public void Withdraw(decimal amount)
        {
            if (_balance >= amount)
            {
                Console.WriteLine($"Withdrawing: ${amount}");
                _balance -= amount;
                Console.WriteLine($"New Balance: ${_balance}");
            }
            else
            {
                Console.WriteLine($"Insufficient funds. Current Balance: ${_balance}");
            }
        }
    }

    //BankAccountProxy(The Synchronization Proxy) :
    public class BankAccountProxy
    {
        private readonly BankAccount _bankAccount;
        private readonly object _lock = new object();

        public BankAccountProxy(decimal initialBalance)
        {
            _bankAccount = new BankAccount(initialBalance);
        }

        public void SafeDeposit(decimal amount)
        {
            lock (_lock)
            {
                _bankAccount.Deposit(amount);
            }
        }

        public void SafeWithdraw(decimal amount)
        {
            lock (_lock)
            {
                _bankAccount.Withdraw(amount);
            }
        }
    }

    //Client Code
    //Testing Synchronization Proxy Design Pattern
    public class Program
    {
        public static void Main()
        {
            BankAccountProxy accountProxy = new BankAccountProxy(1000);

            // Multiple devices/instances trying to modify the account balance concurrently
            Task.Run(() => accountProxy.SafeDeposit(200));
            Task.Run(() => accountProxy.SafeWithdraw(500));
            Task.Run(() => accountProxy.SafeDeposit(300));
            Task.Run(() => accountProxy.SafeWithdraw(150));
            Console.ReadKey();
        }
    }
}

With the BankAccountProxy in place, even if multiple devices or application instances try to access and modify the account balance concurrently, the lock ensures that only one operation is processed at any given time. This guarantees that our bank account modifications remain consistent and accurate, no matter how many concurrent access attempts occur.

Real-Time Example of Synchronization Proxy Design Pattern in C#

Advantages and Disadvantages of Synchronization Proxy Design Pattern in C#

The Synchronization Proxy pattern provides a mechanism to control access to an object, particularly in a multithreaded environment. Let’s explore its advantages and disadvantages:

Advantages of Synchronization Proxy Design Pattern in C#:
  • Separation of Concerns: The pattern ensures that each class has a single responsibility by separating synchronization logic from the actual business logic. This separation makes the code easier to understand and maintain.
  • Safety in Concurrent Environments: When multiple threads access a shared resource, there’s potential for issues like race conditions. A Synchronization Proxy helps mitigate these risks by providing controlled access.
  • Flexibility: If the synchronization mechanism needs to change, it can be done in the proxy without touching the actual object’s implementation.
  • Reusability: The synchronization logic encapsulated in the proxy can be reused for multiple objects, ensuring consistent behavior across different application parts.
  • Laziness: Alongside synchronization, the proxy can also instantiate the actual object lazily (i.e., when needed).
Disadvantages of Synchronization Proxy Design Pattern in C#:
  • Overhead: Introducing a proxy adds an extra layer of indirection. This can lead to a slight performance overhead, especially if the synchronization mechanism is heavy.
  • Complexity: Introducing a Synchronization Proxy might be overkill for simpler use cases or in single-threaded applications. It could add unnecessary complexity.
  • Possible Deadlocks: If not designed properly, introducing synchronization mechanisms can lead to deadlocks where two or more threads wait indefinitely for resources.
  • Limitations with Certain Synchronization Needs: Sometimes, the synchronization needs might be more complex than a simple proxy can handle. In such cases, more sophisticated synchronization constructs or patterns may be required.
  • Testing Challenges: Multithreading and synchronization introduce challenges in testing. It might be harder to reproduce certain issues or ensure the proxy synchronizes correctly in all scenarios.
  • Possible Misuse: If developers aren’t aware of the proxy or its purpose, they might bypass it and directly access the actual object, defeating the purpose of the proxy.

The Synchronization Proxy pattern can effectively ensure safe access to objects in a multithreaded environment. However, its application should be carefully considered in the context of the problem. It’s essential to weigh the benefits against the added complexity and potential disadvantages.

When to use Synchronization Proxy Design Pattern in C#?

The Synchronization Proxy design pattern is useful when providing controlled access to an object, especially in concurrency situations. Here are some scenarios where the Synchronization Proxy pattern can be particularly beneficial:

  • Shared Resources: When multiple threads or processes might be trying to access a shared resource simultaneously, a Synchronization Proxy can prevent potential race conditions and ensure that the resource is accessed safely.
  • Thread Safety: If you have an object that isn’t inherently thread-safe, and you need to ensure that its methods are called in a thread-safe manner, you can wrap it inside a Synchronization Proxy to introduce thread safety.
  • Managing Access Order: Sometimes, certain operations on an object must be performed in a specific sequence. A Synchronization Proxy can ensure that methods are called in the required order.
  • Lazy Initialization: If an object is expensive to create, you can delay its creation until it’s needed. The Synchronization Proxy can handle the logic of checking whether the object has been initialized and, if not, initialize it the first time it’s accessed.
  • Resource Pooling: If you have a limited number of resources, like a connection pool, a Synchronization Proxy can ensure that they are accessed and released properly, preventing situations where too many consumers try to acquire the resource at once.
  • Additional Logging or Monitoring: If you want to add logging, monitoring, or other side-effects when accessing an object, the Synchronization Proxy can be a suitable place to introduce this, especially when concurrency is involved.

However, it’s crucial to recognize when not to use it:

  • Single-threaded Context: If there’s no chance of concurrent access to the object, introducing a Synchronization Proxy can add unnecessary complexity and overhead.
  • Performance Critical Paths: Introducing locks or other synchronization mechanisms can have an overhead. If performance is critical, ensure the introduced synchronization doesn’t become a bottleneck.
  • Over-Engineering: If the main objective can be achieved using simpler synchronization constructs provided by the language or framework, then it might be overkill to introduce a Synchronization Proxy.

While the Synchronization Proxy pattern can be instrumental in ensuring controlled and safe access to objects, assessing the problem domain is essential to decide if it’s the right solution.

In the next article, I will discuss the Real-Time Examples of Firewall Proxy Design Patterns in C#. Here, in this article, I try to explain the Real-Time Examples of Synchronization Proxy Design Patterns in C#. I hope you enjoy this Synchronization Proxy Design Pattern in Real-time Examples using the C# article.

Leave a Reply

Your email address will not be published. Required fields are marked *