一、Thread
Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}start");
Thread thread1 = new Thread(() =>
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}启动");
Thread.Sleep(5000);
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}休眠结束");
/*
* 让出当前线程时间片的剩余部分,用于其他的线程,如果操作系统转而执行了另一个线程,则返回true,否则false
* 该方法不是百分百成功的,和线程优先级,操作系统的线程调度,其他线程的情况有关
*/
Console.WriteLine($"让出线程资源是否成功:{Thread.Yield()}");
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}Yield()结束");
});
Thread thread2 = new Thread(() =>
{
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}启动");
Thread.Sleep(5000);
Console.WriteLine($"线程{Thread.CurrentThread.ManagedThreadId}结束");
})
{
/*
* 当被设置为false时,线程不会随着程序的运行结束而结束,而是等到自己处理的任务处理完成才结束,默认是true
* 当被设置为true时,程序运行结束后,不管任务是否完成,该线程都会被结束
*/
IsBackground = true
};
Console.WriteLine($"线程thread2是否是线程池线程:{thread2.IsThreadPoolThread}");
thread1.Start();
/*
* 线程不允许被重复启动,否则会抛异常
*/
//thread1.Start();
thread2.Start();
/*
* 在调用此方法的线程上引发 ThreadAbortException,以开始终止此线程的过程。
* 调用此方法通常会终止线程。
* 注意:.net core不支持
*/
//thread2.Abort();
//阻塞当前线程,直到线程thread1执行结束,该方法要求thread1必须被启动,否则抛异常
thread1.Join();
Console.WriteLine($"主线程{Thread.CurrentThread.ManagedThreadId}end");
以上代码时创建一个线程常用的方法和要注意的地方,线程相关的方法还有很多。具体可以参考官方文档:Thread 类 (System.Threading) | Microsoft Docs
二、线程池
直接new线程的方式,不是一个很好地利用线程的方式,对于一个较大的项目,如果多人合作,每个人都在自己的子模块里面随意new新线程,就很容易导致线程泛滥,为了统一地管理线程池,合理地使用线程资源,引入了线程池的概念。
在c#里面线程池类是ThreadPool,下面是一个示例:
class Program
{
/// <summary>
/// 每个进程都有一个线程池。 从 .NET Framework 4 开始,进程的线程池的默认大小取决于若干因素,例如虚拟地址空间的大小。
/// </summary>
public static void Main()
{
/*
* 获取电脑的cpu线程数量,我的电脑是6核12线程的
*/
int count = Environment.ProcessorCount;
Console.WriteLine($"当前系统的处理器线程数:{count}");
ThreadPool.GetMinThreads(out int a, out int b);
Console.WriteLine($"线程池默认最小工作线程数:{a}");
Console.WriteLine($"线程池默认最小i/o线程数:{b}");
ThreadPool.GetMaxThreads(out a, out b);
Console.WriteLine($"线程池默认最大工作线程数:{a}");
Console.WriteLine($"线程池默认最大i/o线程数:{b}");
/*
* 设置线程池的最大线程数,如果设置的值小于cpu的线程数量,则返回false,设置无效
* 这里有两个参数,第一个是workerThreads,第二个是completionPortThreads
* workerThreads表示工作线程数,就是我们这个demo现在用的线程
* completionPortThreads表示一步i/o线程数
* 线程池是全局静态的,所以更改线程池中线程的最大数量时,可能会对你使用的代码库产生影响,所以不要随意修改
* 将线程池大小设置得太大可能会导致性能问题。 如果同时执行的线程太多,任务切换开销会成为一个重要因素。
*/
bool result = ThreadPool.SetMaxThreads(count, count);
Console.WriteLine($"设置线程池最大线程数是否成功:{result}");
/*
* 进入线程池排队,如果有空闲线程,则按顺序执行
* 进入排队后无法取消
*/
for (int i = 0; i < 20; i++)
{
ThreadPool.QueueUserWorkItem(ThreadProc,i);
}
Console.WriteLine("主线程开始等待");
/*
* 线程池里的线程都是后台线程,主线程退出后,会终止执行并退出
* 该循环是等待线程池所有任务执行结束的一种方法
* 由于线程池是全局的,所以线程池没有等待所有线程结束的方法,
* 因为我们是无法确定在一个大的系统里面是否有其他的模块在使用线程池
*/
while (true)
{
ThreadPool.GetAvailableThreads(out int avail1, out int avail2);
if (avail1 == count) break;
}
Console.WriteLine("主线程退出");
}
static void ThreadProc(Object stateInfo)
{
Console.WriteLine($"子线程执行,线程号:{stateInfo},线程id:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(1000);
}
}
注意:当线程池重用某个线程时,它不会清除线程本地存储区中的数据或用 ThreadStaticAttribute 特性标记的字段中的数据。因此,当某个方法检查线程本地存储区或用 ThreadStaticAttribute 特性标记的字段时,它所找到的值可能会从先前使用线程池线程的过程中遗留。
参考下面的代码:
class Program
{
[ThreadStatic] static int flag = 0;
public static void Main()
{
int count = Environment.ProcessorCount;
bool result = ThreadPool.SetMaxThreads(count, count);
for (int i = 0; i < 20; i++)
{
ThreadPool.QueueUserWorkItem(ThreadProc,i);
}
Console.WriteLine("主线程开始等待");
while (true)
{
ThreadPool.GetAvailableThreads(out int avail1, out int avail2);
if (avail1 == count) break;
}
Console.WriteLine("主线程退出");
}
static void ThreadProc(Object stateInfo)
{
Console.WriteLine($"子线程执行,线程号:{stateInfo},flag:{++flag}");
Thread.Sleep(1000);
}
}
这段代码的输出结果为:
主线程开始等待
子线程执行,线程号:10,flag:1
子线程执行,线程号:9,flag:1
子线程执行,线程号:8,flag:1
子线程执行,线程号:7,flag:1
子线程执行,线程号:3,flag:1
子线程执行,线程号:1,flag:1
子线程执行,线程号:0,flag:1
子线程执行,线程号:11,flag:1
子线程执行,线程号:6,flag:1
子线程执行,线程号:5,flag:1
子线程执行,线程号:4,flag:1
子线程执行,线程号:2,flag:1
子线程执行,线程号:18,flag:2
子线程执行,线程号:15,flag:2
子线程执行,线程号:14,flag:2
子线程执行,线程号:19,flag:2
子线程执行,线程号:16,flag:2
子线程执行,线程号:13,flag:2
子线程执行,线程号:12,flag:2
子线程执行,线程号:17,flag:2
主线程退出
ThreadStatic标记的变量不会被线程共享,而是在每个线程里面生成一个单独的拷贝,所以第一批线程池启动后,所有的输出结果都是1,当第二批线程开始启动后,由于重复使用的线程池不清楚此变量数据,导致第二批的值都是2。
三、Task
static void Main()
{
static void action(object obj)
{
Console.WriteLine("Task={0}, obj={1}, Thread={2}",
Task.CurrentId, obj,
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
Console.WriteLine($"{obj}结束");
}
Console.WriteLine($"主线程id:{Thread.CurrentThread.ManagedThreadId}");
/*
* 创建一个task,但不启动
* 一般很少用构造方法创建task实例,都是使用 Task.Run 和 TaskFactory.StartNew
* 它的唯一好处是启动和创建分离
*/
Task t1 = new Task(action, "t1");
/*
* 创建一个task,同时异步启动
*/
Task t2 = Task.Factory.StartNew(action, "t2");
/*
* 阻塞当前线程,直到t2结束
*/
t2.Wait();
t1.Start();
t1.Wait();
/*
* 异步启动一个task
*/
Task t3 = Task.Run(() => {
Console.WriteLine("Task={0}, obj={1}, Thread={2}",
Task.CurrentId, "t3",
Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);
Console.WriteLine("t3运行结束");
});
Task t4 = new Task(action, "t4");
// 同步启动一个task
t4.RunSynchronously();
/*
* 这里仍然是可以等待的,虽然t4运行结束后代码才会执行到这里
*/
t4.Wait();
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Task t5 = Task.Factory.StartNew(()=> {
Console.WriteLine("t5开始");
Thread.Sleep(2500);
/*
* 线程是不可以被取消的,只是采取一种方式去中断,
* 调用cancellationTokenSource.Cancel()并不会被取消,
* 只是把标记改了
*/
if (cancellationTokenSource.IsCancellationRequested) cancellationTokenSource.Token.ThrowIfCancellationRequested();
Console.WriteLine($"t5被执行,线程id:{Thread.CurrentThread.ManagedThreadId}");
}, cancellationTokenSource.Token);
new Thread(()=> {
Thread.Sleep(100);
Console.WriteLine($"t5是否可以被取消:{cancellationTokenSource.Token.CanBeCanceled}");
cancellationTokenSource.Cancel();
}).Start();
/*
* 等待500ms内任一任务运行结束或者取消标记发出取消等待的操作
*/
try
{
Task.WaitAny(new List<Task> { t5 }.ToArray(), 2000, cancellationTokenSource.Token);
}
catch {
}
/*
* 等待所有的任务运行结束
*/
Task.WaitAll(new List<Task> { t1, t2, t3, t4}.ToArray());
Task t6 = Task.Factory.StartNew(async ()=> {
try
{
/*
* 下面的代码不重要,所以让出时间碎片,
* 让下面的代码重新在线程池排队,等待线程池执行
* 使用await强制异步完成方法
*/
await Task.Yield();
Thread.Sleep(2000);
//throw new Exception();
}
catch { throw; }
});
/*
* 创建一个等待其他任务全部完成的任务
*/
Task t = Task.WhenAll(new List<Task> { t6 }.ToArray());
try
{
t.Wait();
}
catch {
}
if (t.Status == TaskStatus.RanToCompletion)
Console.WriteLine("所有任务完成");
//如果某个任务抛异常了,那么下面的条件成立
else if (t.Status == TaskStatus.Faulted)
Console.WriteLine("有任务执行失败了");
Console.WriteLine("主线程结束");
}
}
task的ContinueWith:
//等待任务结束后创建一个新的任务,因此这两个任务不在一个线程
Task<bool> task1 = Task.Factory.StartNew(()=> {
Thread.Sleep(1000);
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return true;
});
task1.ContinueWith(task=> {
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.WriteLine(task.Result);
});
task的本质:threadpool。看下面的代码:
class Program
{
static void Main()
{
List<Task> tasks = new List<Task>();
Console.WriteLine("ThreadPool最小设置为100");
//ThreadPool.SetMinThreads(100, 100);
for (int i = 0; i < 50; i++)
{
tasks.Add(Task.Factory.StartNew(() =>
{
Console.WriteLine($"线程id:{Thread.CurrentThread.ManagedThreadId}");
Thread.Sleep(5000);
}));
}
Task.WaitAll(tasks.ToArray());
}
}
如果设置了线程池的最小线程数为100,那么上面的50个task会立即全部被执行,如果不设置,则逐步执行。这说明task的本质就是利用默认线程池的,同时对线程的调度是逐步增加的,连续多次运行并发线程,会提高占用的线程数,而等若干秒不运行,线程数又会降低。
四、await/async
/// <summary>
/// await/async 不创建新的线程,它更像是回调的语法糖
/// 让代码读起来更流畅,如果要开启一个线程,仍然需要使用thread或者task等方法
/// 因此这俩关键字通常和task结合使用,因为task会开启一个线程
/// </summary>
static async Task Main()
{
Console.WriteLine("start");
Console.WriteLine("程序开始时的线程号:" + Thread.CurrentThread.ManagedThreadId);
var b= TestAsync();
Console.WriteLine("TestAsync()调用结束");
/*
* 程序一旦遇到await关键字,后续的代码将在一个新线程里面进行
* 这个新线程是由这个TestAsync内部创建的
* 这样调用Main的地方就不需要等待了,可以在调用main之后继续调用其他的方法,
* 除非调用者在main方法调用前加了await
*/
Console.WriteLine(await b);
Console.WriteLine("程序退出时的线程号:"+Thread.CurrentThread.ManagedThreadId);
/*
* 使用Result和await是一样的,都是等待task执行结束返回结果
*/
//Console.WriteLine(b.Result);
}
private async static Task<bool> TestAsync()
{
Console.WriteLine("TestAsync方法内部开始时的线程号:" + Thread.CurrentThread.ManagedThreadId);
//这里才会真正开启线程
await Task.Delay(5000);
Console.WriteLine("TestAsync方法内部await之后的的线程号:" + Thread.CurrentThread.ManagedThreadId);
return true;
}
这段代码的执行结果:
start
程序开始时的线程号:1
TestAsync方法内部开始时的线程号:1
TestAsync()调用结束
TestAsync方法内部await之后的的线程号:5
True
程序退出时的线程号:5
五、ConfigureAwait
ConfigureAwait (false)会指示await之后的代码不在原先的SynchronizationContext上执行,可以认为在另外另外一个线程执行。在asp.net core里面不再有用,因为asp.net core的应用程序一般都是控制台应用程序,不存在SynchronizationContext,但在.net framework里面有用,设置为true后可能会引起死锁。比如下面的代码在.net framework里就会导致死锁:
public void Start()
{
Ceshi().Wait();
}
private async Task Ceshi()
{
await Task.Delay(1000);
}
虽然在.net core里面ConfigureAwait (false)有用,但在封装通用库的时候,仍建议加上,以防止你的库被其他存在SynchronizationContext的项目引用。
六、Parallel
static void Main()
{
//设置系统最小线程池大小
ThreadPool.SetMinThreads(100,100);
/*
* 这将导致系统同时开20个线程去调用test方法
* 不包含i=20的情况
* 该方法是基于线程池实现的,所以要设置线程池最小线程数,
* 否则不会一次全部开始,而是逐步开始,像task对线程池的利用一样
* 该方法会阻塞当前线程,直到所有的方法执行完成
* 该方法用于大数据的并行计算
*/
var v= Parallel.For(0,20,test);
Console.WriteLine("end");
Console.ReadLine();
}
private static void test(int i)
{
Thread.Sleep(5000);
Console.WriteLine($"根据i计算的值:{i*i},线程号:{Thread.CurrentThread.ManagedThreadId}");
}
前面的内容是线程的一些基本知识,涉及到多线程的时候有两个比较重要的问题,一个是线程安全,一个是线程同步/等待
1)线程安全
class Program
{
private volatile static int volatileInt = 0;
private static int commonInt1 = 0;
private static int commontInt2 = 0;
static int commontInt3 = 0;
[ThreadStaticAttribute] static int threadStaticAttributeInt = 0;
private static readonly object lockObj = new object();
static void Main()
{
ThreadPool.SetMinThreads(100,100);
List<Task> tasks = new List<Task>();
for (int i = 0; i < 20; i++)
{
//ThreadPool.QueueUserWorkItem(test,null);
tasks.Add(Task.Factory.StartNew(test));
}
//Task.WaitAll(tasks.ToArray());
Thread.Sleep(5000);
Console.WriteLine($"volatileInt:{volatileInt}\r\n" +
$"commonInt1:{commonInt1}\r\n" +
$"commontInt2:{commontInt2}\r\n" +
$"commontInt3:{commontInt3}\r\n" +
$"threadStaticAttributeInt:{threadStaticAttributeInt}");
}
private static void test()
{
Thread.Sleep(10);
//线程内存可见,但不具备原子性,因为++操作是非原子性操作,所以结果可能不是20
volatileInt++;
//线程不安全,最终结果不确定
commonInt1++;
//线程安全,最终结果是20
lock (lockObj)
{
commontInt2++;
}
/*
* 该类为多个线程共享的变量提供原子操作
* 内存可见,是线程安全的,最终结果是20
* 该类提供大量的原子性操作内存的方法,比如可用于单例模式的Interlocked.Exchange
* https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netframework-4.8
*/
Interlocked.Increment(ref commontInt3);
//每个内存拥有自己的拷贝,所有的线程它的最终结果都是1
threadStaticAttributeInt++;
Thread.Sleep(2000);
}
}
另外还有下面几个方法,都是值得关注的:
Thread.VolatileWrite();
Volatile.Write();
上面的方法的实现原理是使用了Interlocked.MemoryBarrier:
new Thread(() =>{
while (flag)
{
//如果不加这句,该循环将会一直执行下去
/*
* Synchronizes memory access as follows:
* The processor that executes the current thread cannot reorder instructions
* in such a way that
* memory accesses before the call to MemoryBarrier()
* execute after
* memory accesses that follow the call to MemoryBarrier().
* 同步内存访问按照下面的方式进行:
* 正在执行当前线程的处理器不可以用这样的方式重排指令:
* 调用Interlocked.MemoryBarrier()方法之前的内存存取操作
* 放在
* 调用Interlocked.MemoryBarrier()之后的内存存取操作
* 后执行
*/
Interlocked.MemoryBarrier();
}
Console.WriteLine("子线程结束");
}).Start();
Thread.Sleep(1000);
new Thread(() => {
flag = false;
}).Start();
}
Interlocked.MemoryBarrier的作用阻止处理器指令重排过程中,让更新某个变量的操作,因为重排的原因被放到了使用它之后,有点类似于内存可见的效果。
所谓内存可见是指当子线程更新了某个公共的变量,会立即更新主线程的内存,同时通知其他的子线程更新自己线程的临时缓存。
所谓原子性就是对于一个公共变量的操作是线程安全的,比如a++,分成两步:
a)读取主内存a的值到缓存
b)对a的值+1
c)更新缓存。
d)如果加了volatile,则更新主内存,同时通知其他子线程,更新缓存。
以上步骤可能会因为并发量很大导致写入的数据和预期不一致,因为a步骤到b步骤有时间间隔。
2)线程等待
如果你使用task方法,则不用担心线程等待的问题,因为task提供了await关键字用于线程等待。但是如果你自定义线程,那么可能就无法使用await方法进行线程等待了。看下面的代码:
/*
* 用于线程等待,调用WaitOne的代码会阻塞,直到有线程调用了set方法
* 如果传入的是true,表示已经发过信号,那么就不会阻塞了
* 手动和自动的区别在于前者需要手动调用Reset方法来重置信号状态
*/
static AutoResetEvent autoResetEvent = new AutoResetEvent(false);
static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
/*
* 第一个参数表示初始可用的信号数量
* 第二个参数表示最大可用信号数量
* 如果第一个参数设置为0,那么这个信号量容器一开始就没有可用的信号可用,
* 必须释放了,其他的线程才能拿到信号
* 使用这个参数来控制其他线程启动的时机
*/
static Semaphore semaphore = new Semaphore(5, 5);
static void Main()
{
Thread thread1 = new Thread(()=> {
Console.WriteLine("thread1 开始");
Thread.Sleep(5000);
Console.WriteLine("thread1 结束");
manualResetEvent.Set();
});
Thread thread2 = new Thread(()=> {
manualResetEvent.WaitOne();
Console.WriteLine("thread2 开始");
Console.WriteLine("thread2 结束");
});
thread2.Start();
thread1.Start();
//阻塞当前线程,直到thread2执行结束
thread2.Join();
//重置信号状态,否则线程4将不会等待
var b= manualResetEvent.Reset();
Thread thread3 = new Thread(() => {
Console.WriteLine("thread3 开始");
Thread.Sleep(5000);
Console.WriteLine("thread3 结束");
manualResetEvent.Set();
});
Thread thread4 = new Thread(() => {
manualResetEvent.WaitOne();
Console.WriteLine("thread4 开始");
Console.WriteLine("thread4 结束");
});
thread4.Start();
thread3.Start();
//thread4.Join();
for (int i = 0; i < 5; i++)
{
ThreadPool.QueueUserWorkItem(test,i);
}
Thread thread5 = new Thread(()=> {
//如果想等五个线程都结束,那么调用五次这个方法
semaphore.WaitOne();
semaphore.WaitOne();
semaphore.WaitOne();
semaphore.WaitOne();
semaphore.WaitOne();
Console.WriteLine("thread5启动");
Console.WriteLine("thread5结束");
semaphore.Release(5);
});
Thread.Sleep(100);
thread5.Start();
thread5.Join();
}
private static void test(object state)
{
semaphore.WaitOne();
Console.WriteLine($"线程池线程{state}启动");
Thread.Sleep(5000);
Console.WriteLine($"线程池线程{state}结束");
semaphore.Release();
}
}