C# example for designing a Type that exposes an Event which is raised in a thread-safe manner.

// ===========================================================================
// Example from CLR via C# 4th Edition. By Jeffrey Richter
// Chapter 11: Events - Designing a Type That Exposes an Event, Raising an Event in a Thread-Safe Way
// https://www.microsoftpressstore.com/store/clr-via-c-sharp-9780735667457
// ===========================================================================

/// <summary>
/// Step #1: Define a type that will hold any additional information that
/// should be sent to receivers of the event notification.
/// </summary>
internal class NewMailEventArgs : EventArgs
{
    private readonly String m_from, m_to, m_subject;

    public NewMailEventArgs(String from, String to, String subject)
    {
        m_from = from; m_to = to; m_subject = subject;
    }

    public String From    { get { return m_from;    } }
    public String To      { get { return m_to;      } }
    public String Subject { get { return m_subject; } }
}

public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e);

internal class MailManager
{
    /// <summary>
    /// Step #2: Define the event member
    ///
    /// NewMail is the name of this event. The type of the event member is
    /// EventHandler&lt;New­ MailEventArgs&gt;, which means that all receivers of the
    /// event notification must supply a callback method whose prototype matches
    /// that of the EventHandler&lt;NewMailEventArgs&gt; delegate type. Because the
    /// generic System.EventHandler delegate is defined as follows:
    ///
    /// public delegate void EventHandler&lt;TEventArgs&gt;(Object sender, TEventArgs e);
    ///
    /// the method prototypes must look like the following:
    ///
    /// void MethodName(Object sender, NewMailEventArgs e);
    /// </summary>
    public event EventHandler<NewMailEventArgs> NewMail;

    /// <summary>
    /// Step 3: Raising an Event in a Thread-Safe Way
    ///
    /// Define a method responsible for raising the event to notify registered
    /// objects that the event has occurred If this class is sealed, make
    /// this method private and non-virtual.
    ///
    /// The call to Volatile.Read forces NewMail to be read at the point of
    /// the call and the reference really has to be copied to the temp
    /// variable now. Then, temp will be invoked only if it is not null.
    /// </summary>
    protected virtual void OnNewMail(NewMailEventArgs e)
    {
        EventHandler<NewMailEventArgs> temp = Volatile.Read(ref NewMail);
        if (temp != null)
        {
            temp(this, e);
        }
    }

    /// <summary>
    /// Step #4: Define a method that translates the input into the desired event.
    ///
    /// Your class must have some method that takes some input and translates
    /// it into the raising of the event. In my MailManager example,
    /// the SimulateNewMail method is called to indicate that a new email
    /// message has arrived into MailManager.
    /// </summary>
    public void SimulateNewMail(String from, String to, String subject)
    {
        // Construct an object to hold the information we want
        // to pass to the receivers of our notification
        NewMailEventArgs e = new NewMailEventArgs(from, to, subject);

        // Call our virtual method notifying our object that the event
        // occurred. If no type overrides this method, our object will
        // notify all the objects that registered interest in the event
        OnNewMail(e);
    }
}

/// <summary>
/// As a convenience, you could define an extension method (as discussed in
/// Chapter 8, "Methods") that encapsulates this thread-safety logic.
/// Define the extension method as follows.
/// </summary>
public static class EventArgExtensions
{
    public static void Raise<TEventArgs>(this TEventArgs e, Object sender, ref EventHandler<TEventArgs> eventDelegate)
    {
        // Copy a reference to the delegate field now into a temporary field for thread safety
        EventHandler<TEventArgs> temp = Volatile.Read(ref eventDelegate);

        // If any methods registered interest with our event, notify them
        if (temp != null)
        {
            temp(sender, e);
        }
    }
}

// And now, we can rewrite the OnNewMail method as follows.
protected virtual void OnNewMail(NewMailEventArgs e)
{
    e.Raise(this, ref m_NewMail);
}