C# Multithreading (11): Thread Wait

2020年4月26日 22点热度 0人点赞 0条评论
内容目录

In this article, we will focus on the topics related to thread waiting, following our exploration of various thread management types and synchronization methods.

Before delving into multithreading, the author only knew how to use new Thread; locking? Lock; thread waiting? Thread.Sleep().

We have already explored various methods of creating threads and using different types of locks, as well as employing multiple waiting methods such as Thread.Sleep(), Thread.SpinWait();, and {some lock}.WaitOne().

These waiting mechanisms can significantly impact the algorithm logic and performance of the code, and may also lead to deadlocks. In this article, we will gradually explore waiting in threads.

volatile keyword

The volatile keyword indicates that a field can be accessed by multiple concurrently executing threads.

Let's continue with the example from “C# Multithreading (3): Atomic Operations”:

        static void Main(string[] args)
        {
            for (int i = 0; i 

After running this, you'll find that the result is not 500,0000, but by using Interlocked.Increment(ref sum);, you can obtain an accurate and reliable result.

Try running the example below again:

        static void Main(string[] args)
        {
            for (int i = 0; i 

You think it's normal now? Haha, not at all.

The role of volatile lies in reading, ensuring that the order of observations is consistent with the order of writes; every read retrieves the latest value without interfering with write operations.

For more details, click here: https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile

For an explanation of its principles, refer to: https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/

file

Three common waits

The three common waits are:

Thread.Sleep();
Thread.SpinWait();
Task.Delay();

Thread.Sleep(); blocks the thread, yielding the time slice and putting it into a sleeping state until it is awakened; it is suitable for long waits.

Thread.SpinWait(); uses spin waiting, performing some computations during the wait, and the thread does not sleep; it is used for very short waits; prolonged waiting could affect performance.

Task.Delay(); is used for waiting within asynchronous operations, which we will cover in a later article, so let's not consider it here for now.

We also need to examine the two types, SpinWait and SpinLock, before summarizing and comparing them.

Spin vs Block

Earlier, we studied the differences between spin and block. Let’s clarify this again.

Thread waiting has kernel mode and user mode.

Since only the operating system can control the lifecycle of threads, blocking threads using Thread.Sleep() and similar methods involves context switching; this type of waiting is known as kernel mode.

User mode allows threads to wait without needing to switch contexts; instead, it makes the thread perform some meaningless calculations to achieve the wait, also known as spinning.

SpinWait Structure

According to Microsoft documentation, this provides support for spin-based waiting.

Thread blocking incurs the cost of context switching, which can be expensive for very short thread waits. In our earlier examples, the extensive use of Thread.Sleep() and various waiting methods was not ideal.

SpinWait provides a better option.

Properties and Methods

As usual, let's first look at the common properties and methods of SpinWait.

Properties:

| Property | Description |
|----------|-------------|
| Count | Gets the number of times SpinOnce() has been called on this instance. |
| NextSpinWillYield | Gets whether the next call to SpinOnce() will yield to the processor and force a context switch. |

Methods:

| Method | Description |
|--------|-------------|
| Reset() | Resets the spin counter. |
| SpinOnce() | Performs a single spin. |
| SpinOnce(Int32) | Performs a single spin and calls Sleep(Int32) after reaching the minimum spin count. |
| SpinUntil(Func) | Spins until a specified condition is met. |
| SpinUntil(Func, Int32) | Spins until a specified condition is met or a specified timeout expires. |
| SpinUntil(Func, TimeSpan) | Spins until a specified condition is met or a specified timeout expires. |

Spin Example

Here, we'll implement a function where the current thread waits for other threads to complete their tasks.

The function will initiate a thread to increment sum by +1, and the main thread will only continue running once the new thread completes its operation.

    class Program
    {
        static void Main(string[] args)
        {
            new Thread(DoWork).Start();

            // Wait for the above thread to complete its work
            MySleep();

            Console.WriteLine(sum =  + sum);
            Console.ReadKey();
        }

        private static int sum = 0;
        private static void DoWork()
        {
            for (int i = 0; i 

New Implementation

We will improve the above example by modifying the MySleep method to:

        private static bool isCompleted = false;        
        private static void MySleep()
        {
            SpinWait wait = new SpinWait();
            while (!isCompleted)
            {
                wait.SpinOnce();
            }
        }

Or alternatively, it can be changed to:

        private static bool isCompleted = false;        
        private static void MySleep()
        {
            SpinWait.SpinUntil(() => isCompleted);
        }

SpinLock Structure

According to Microsoft documentation, it provides a mutual exclusion lock primitive, where threads trying to acquire the lock will wait in a loop of repeated checks until the lock becomes available.

SpinLock is a type of spin lock, suitable for scenarios with frequent contention and short wait times. Its main feature is that it avoids blocking and does not incur expensive context switching.

The author's expertise is limited, regarding SpinLock, you can refer to https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/.

Furthermore, do you remember Monitor? SpinLock is quite similar to Monitor~ https://www.cnblogs.com/whuanle/p/12722853.html#2monitor.

In “C# Multithreading (10: Reader-Writer Locks)”, we introduced ReaderWriterLock and ReaderWriterLockSlim, where ReaderWriterLockSlim internally relies on SpinLock and is three times faster than ReaderWriterLock.

Properties and Methods

The commonly used properties and methods of SpinLock are as follows:

Properties:

| Property | Description |
|----------|-------------|
| IsHeld | Gets whether the lock is currently held by any thread. |
| IsHeldByCurrentThread | Gets whether the lock is held by the current thread. |
| IsThreadOwnerTrackingEnabled | Gets whether thread ownership tracking is enabled for this instance. |

Methods:

| Method | Description |
|--------|-------------|
| Enter(Boolean) | Acquires the lock in a reliable manner, so that even if an exception occurs during the method call, it can reliably check lockTaken to determine if the lock was acquired. |
| Exit() | Releases the lock. |
| Exit(Boolean) | Releases the lock. |
| TryEnter(Boolean) | Tries to acquire the lock in a reliable manner, even if an exception occurs during method calls, allowing for reliable checking of lockTaken. |
| TryEnter(Int32, Boolean) | Tries to acquire the lock in a reliable manner, allowing for reliable checking of lockTaken during exceptions in method calls. |
| TryEnter(TimeSpan, Boolean) | Tries to acquire the lock reliably, enabling reliable checking of lockTaken even during exceptions in calls. |

Example

Here’s a template for using SpinLock:

        private static void DoWork()
        {
            SpinLock spinLock = new SpinLock();
            bool isGetLock = false;     // If the lock has been acquired
            try
            {
                spinLock.Enter(ref isGetLock);
                // Computation
            }
            finally
            {
                if (isGetLock)
                    spinLock.Exit();
            }
        }

No specific scenario examples will be provided here.

It's important to note that a SpinLock instance should not be shared or reused.

Wait Performance Comparison

For expert insights, refer to the performance test data for various locks in .NET: http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/.

Here, we will perform a simple test comparing the wait performance between blocking and spinning.

It's often said that Thread.Sleep() incurs context switching, leading to significant performance loss. Just how much loss are we talking about? Let's test it out. (All operations are tested in Debug mode.)

Testing Thread.Sleep(1):

        private static void DoWork()
        {
            Stopwatch watch = new Stopwatch();
            watch.Start();
            for (int i = 0; i 

In tests on the author's machine, the result is approximately 20018. Subtracting the waiting time of 10000 milliseconds from Thread.Sleep(1), it suggests that making 10000 context switches costs approximately 10000 milliseconds—around 1 millisecond per switch.

When the above example is modified to:

            for (int i = 0; i 

The outcome shows a result of 30013, indicating context switching takes approximately 1 millisecond as well.

When changed to Thread.SpinWait(1000):

            for (int i = 0; i 

The result is 28876, meaning spinning 1000 times takes about 0.03 milliseconds.

痴者工良

高级程序员劝退师

文章评论