C# 线程同步
要避免同步问题,最好不要在线程之间共享数据。当然,这并不总是可行的。如果需要共享数据,就必须使用同步技术。如果不注意这些问题,就很难在应用程序中找到问题的原因,因为线程问题是不定期发生的。
1. lock
C#为多个线程的同步提供了自己的关键字:lock语句。lock 语句获取给定对象的互斥 lock,执行语句块,然后释放 lock。 持有 lock 时,持有 lock 的线程可以再次获取并释放 lock。 阻止任何其他线程获取 lock 并等待释放 lock。
namespace ConsoleApp1
{
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
object lockObj = new object();
long num = 0;
Parallel.For(0, 100000, a =>
{
lock (lockObj)
{
num += a;
}
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
PS:锁只在线程之间有效,同一线程中并不会产生锁,例如下例,递归并不会锁住
namespace ConsoleApp1
{
using System;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
object lockObj = new object();
test(20);
Console.ReadKey();
void test(int a)
{
lock (lockObj)
{
if (a > 10)
{
test(--a);
}
}
Console.WriteLine(a);
}
}
}
}
lock创建单例:
public class People
{
static People _instance;
static readonly object _lockObj = new object();
public static People Instance
{
get
{
if (_instance == null) //防止每次获取都要进行lock判断
{
lock (_lockObj)
{
if (_instance == null) //防止多线程访问导致初始化多次
{
_instance = new People();
}
}
}
return _instance;
}
}
}
2. Monitor
提供同步访问对象的机制。lock 相当于一个简易的 Monitor。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
object lockObj = new object();
long num = 0;
Parallel.For(0, 100000, a =>
{
Monitor.Enter(lockObj);
num += a;
Monitor.Exit(lockObj);
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
相对于 lock,Monitor类还提供了:
TryEnter(object obj, TimeSpan timeout) | 在指定的时间内尝试获取指定对象上的排他锁。 |
IsEntered(object obj) | 确定当前线程是否保留指定对象上的锁。 |
Wait(object obj) | 释放对象上的锁并阻止当前线程,直到它重新获取该锁。 |
Pulse(object obj) | 通知等待队列中的线程锁定对象状态的更改,允许一个等待中的继续 |
3. Interlocked
为多个线程共享的变量提供原子操作。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
Parallel.For(0, 100000, a =>
{
Interlocked.Add(ref num, a);
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
Decrement(ref int location) | 以原子操作的形式递减指定变量的值并存储结果。 |
Increment(ref int location) | 以原子操作的形式递增指定变量的值并存储结果。 |
Exchange(ref int location1, int value) | 以原子操作的形式,将 32 位有符号整数设置为指定的值并返回原始值。 |
4. Mutex
Mutex 主要用于进程间同步的同步基元。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
Mutex mut = new Mutex();
Parallel.For(0, 100000, a =>
{
mut.WaitOne();
num += a;
mut.ReleaseMutex();
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
static class Program
{
[STAThread]
static void Main()
{
bool createdNew;
Mutex mutex = new Mutex(false, "SingletonWinAppMutex", out createdNew);
if (createdNew)
{
MessageBox.Show("You can only start one instance of the application");
Application.Exit();
return;
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
5. AutoResetEvent
用一个指示是否将初始状态设置为终止的布尔值初始化 AutoResetEvent 类的新实例。若为 false,表示起始就是非终止状态,即是锁住状态。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
AutoResetEvent autoResetEvent = new AutoResetEvent(true);
Parallel.For(0, 100000, a =>
{
autoResetEvent.WaitOne();
num += a;
autoResetEvent.Set();
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
WaitOne() | 阻止当前线程,直到当前 System.Threading.WaitHandle 收到信号。 |
Set() | 将事件状态设置为有信号,从而允许一个WaitOne等待线程继续执行。 |
Reset() | 将事件状态设置为非终止,从而导致WaitOne线程受阻。但不会导致本线程受阻。 |
6. ManualResetEvent
用法类似于 AutoResetEvent。
区别:
- 在 Set 方法上,ManualResetEvent 每次 Set 后会允许所有正在 WaitOne 的线程继续执行。
- 需要手动调用Reset将状态设置为受阻状态,AutoResetEvent.WaiteOne 相当于 ManualResetEvent.WaiteOne;ManualResetEvent.Reset;
同时还有 ManualResetEvent 的轻量版:ManualResetEventSlim
7. SpinLock
提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。 注意:SpinLock 是结构体
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
SpinLock spinLock = new SpinLock();
Parallel.For(0, 100000, a =>
{
bool lockTaken = false;
spinLock.Enter(ref lockTaken);
num += a;
if (lockTaken)
{
spinLock.Exit();
}
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
除了体系结构上的区别之外, SpinLock结构的用法非常类似于 Monitor类。获得锁定使用 Enter()或TryEnter()方法,释放锁定使用Exit()方法。 如果基于对象的锁定对象( Monitor)的系统开销由于垃圾回收而过高,就可以使用 SpinLock结构。如果有大量的锁定(例如,列表中的每个节点都有一个锁定),且锁定的时间总是非常短, SpinLock结构就很有用。应避免使用多个SpinLock结构,也不要调用任何可能阻塞的内容。
8. Semaphore
可以进行进程间的互斥。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
Semaphore semaphore = new Semaphore(1, 1);
Parallel.For(0, 100000, a =>
{
semaphore.WaitOne();
num += a;
semaphore.Release();
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
信号量分为两种类型:本地信号量和命名系统信号量。 本地信号灯对应用程序而言是本地的,系统信号量在整个操作系统中均可见,适用于进程间同步。 SemaphoreSlim是 Semaphore 不使用 Windows 内核信号量的类的轻型替代项。 与 Semaphore 类不同, SemaphoreSlim 类不支持已命名的系统信号量。 只能将其用作本地信号量。 SemaphoreSlim类是用于在单个应用内进行同步的建议信号量。
9. SemaphoreSlim
对可同时访问资源或资源池的线程数加以限制的 Semaphore 的轻量替代。轻型信号灯控制对应用程序的本地资源池的访问。 实例化信号量时,可以指定可同时进入信号量的最大线程数。 还可以指定可同时进入信号量的初始线程数。 这会定义信号量的计数。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
long num = 0;
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(1, 1);
Parallel.For(0, 100000, a =>
{
semaphoreSlim.Wait();
num += a;
semaphoreSlim.Release();
});
Console.WriteLine(num);
Console.ReadKey();
}
}
}
10. ThreadStatic
ThreadStaticAttribute 只能应用与静态字段。被标记的字段不会在线程之间共享。 每个执行线程都有单独的字段实例,并分别设置和获取该字段的值。 如果在不同的线程上访问该字段,则该字段将包含不同的值。
namespace ConsoleApp1
{
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
[ThreadStatic]
static long num = 0;
static void Main(string[] args)
{
Parallel.For(0, 100000, a =>
{
num = a; //可以保证num在该线程中的值不会因为其他线程而改变
if (num != a)
{
Console.WriteLine($"num:{num} a:{a}");
}
});
Console.ReadKey();
}
}
}
11. MethodImpl(Synchronized)
实现方法的同步:该方法一次性只能在一个线程上执行。 静态方法在类型上锁定,而实例方法在实例上锁定。 只有一个线程可在任意实例函数中执行,且只有一个线程可在任意类的静态函数中执行。
namespace ConsoleApp1
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
List<DateTime> listDate = new List<DateTime>();
Parallel.For(0, 100000, a =>
{
Add(listDate);
});
Console.WriteLine(listDate.Count);
Console.ReadKey();
}
[MethodImpl(MethodImplOptions.Synchronized)]
public static void Add(List<DateTime> listDate)
{
listDate.Add(DateTime.Now);
}
}
}
12. Concurrent
列举几个线程安全的集合类,不用考虑在插入或者删除时导致枚举失败。
表示对象的线程安全的无序集合。类似于List,但是不能通过索引获取值 | |
相当于 Dictionary,可以通过将 TKey设置为索引相当于List来使用 | |
相当于Queue,表示线程安全的先进先出 (FIFO) 。 | |
相当于Stack,表示线程安全的后进先出 (LIFO) 。 |