C#多线程入门概念及技巧
- 一、什么是线程
- 1.1线程的概念
- 1.2为什么要多线程
- 1.3线程池
- 1.4线程安全
- 1.4.1同步机制
- 1.4.2原子操作
- 1.5线程安全示例
- 1.5.1示例一
- 1.5.2示例二
- 1.6C#一些自带的方法实现并行
- 1.6.1 Parallel——For、ForEach、Invoke
- 1.6.1 PLINQ——AsParallel、AsSequential、AsOrdered
- 1.7Semaphore
一、什么是线程
1.1线程的概念
- 线程是操作系统中能够独立运行的最小单位,也是程序中能够并发执行的一段指令序列
- 线程是进程的一部分,一个进程可以包括多个线程,这个线程可以共享进程的资源
- 进程有入口线程,也可用创建更多的线程
1.2为什么要多线程
- 批量重复任务希望同时进行
- 多个不同任务希望同时进行,并且互不干扰
1.3线程池
- 一组预先创建的线程,可以被重复使用来执行多个任务
- 避免频繁地创建和销毁线程,从而减少了现成创建和销毁的开销,提高了系统的性能和效率
- 异步编程默认使用线程池
1.4线程安全
多个线程访问共享资源时,对共享资源的访问不会导致数据不一致或不可预期的结果
1.4.1同步机制
- 用于协调和控制多个线程之间的执行顺序和互斥访问共享资源
- 确保线程按照特定的顺序执行,避免竞态条件和数据不一致的问题
1.4.2原子操作
- 在执行过程中不会被中断的操作,不可分割,要么完全执行,要么完全不执行,没有中间状态
- 在多线程环境下,原子操作能够保证数据的一致性和可靠性,避免出现竞太条件和数据竞争的问题
1.5线程安全示例
1.5.1示例一
两个线程对一个变量进行操作,每个线程都让count增加10000,代码如下:
namespace ThreadStudy
{
class Thread_Lock
{
const int total = 100_000;
public static int count = 0;
static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(ThreadMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadMethod));
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Count:{count}");
}
public static void ThreadMethod()
{
for (int i = 0; i < total; i++)
count++;
}
}
}
输出结果确不为两万,并且每次都不一样:
这是因为线程一在访问并修改这个变量值的时候,另一个线程也在访问并修改这个值,这就会导致一个线程修改后的值被另一个线程修改后的值给覆盖,这个时候我们就需要加锁,修改后的代码如下:
class Thread_Lock
{
const int total = 100_000;
public static int count = 0;
public static object lockobjcet = new object();
static void Main(string[] args)
{
Thread thread1 = new Thread(new ThreadStart(ThreadMethod));
Thread thread2 = new Thread(new ThreadStart(ThreadMethod));
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"Count:{count}");
}
public static void ThreadMethod()
{
for (int i = 0; i < total; i++)
{
lock (lockobjcet)
count++;
//这么写也可用 原子操作:
//count++在底层可能经过了很多步才加一 这个过程中数据可能被其它线程更改
//原子操作能一步完成,防止其它线程对变量进行更改
//Interlocked.Increment(ref count);
}
}
}
输出结果:
1.5.2示例二
正常结果是要输出0-19,不加锁的情况下就会输出一些无序数
public static Queue<int> queue = new Queue<int>();
public static object lockObject = new object();
static void Main(string[] args)
{
Thread producer = new Thread(new ThreadStart(AddNumber));
Thread consumer1 = new Thread(new ThreadStart(WriteNumber));
Thread consumer2 = new Thread(new ThreadStart(WriteNumber));
producer.Start();
consumer1.Start();
consumer2.Start();
producer.Join();
consumer1.Interrupt();
consumer2.Interrupt();
consumer1.Join();
consumer2.Join();
}
public static void AddNumber()
{
for (int i = 0; i < 20; i++)
{
Thread.Sleep(20);
queue.Enqueue(i);
}
}
public static void WriteNumber()
{
try
{
while (true)
{
lock(lockObject)
if (queue.TryDequeue(out var res))
{
Console.WriteLine(res);
Thread.Sleep(1);
}
}
}
catch (Exception)
{
Console.WriteLine("Thread interrupted");
}
}
输出结果:
1.6C#一些自带的方法实现并行
1.6.1 Parallel——For、ForEach、Invoke
正常For循环需要4s
class Program
{
static void Main(string[] args)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < 20; i++)
{
Thread.Sleep(200);
Console.WriteLine($"I:{i}");
}
Console.WriteLine($"Elapsed time: {sw.ElapsedMilliseconds}ms");
}
}
使用Parallel进行For循环:
效果提升近10倍,美滋滋
class Program
{
static void Main(string[] args)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < 20; i++)
{
Thread.Sleep(200);
Console.WriteLine($"I:{i}");
}
Console.WriteLine($"Elapsed time: {sw.ElapsedMilliseconds}ms");
}
}
1.6.1 PLINQ——AsParallel、AsSequential、AsOrdered
//ToDo 后续补充
1.7Semaphore
Semaphore可以控制线程开启的多少,比如Parallel.For开启了5个线程,而Semaphore定义只能开启三个,当有三个线程正在做时,那么其它的线程就不能够再做,Semaphore等待后要释放掉,最后面还需要Dispose,之前用Parallel在不控制线程的情况下需要400ms,现在控制线程数量,需要1400ms
static void Main(string[] args)
{
var sw = Stopwatch.StartNew();
//第一个参数 最开始有几个线程可以用 第二个参数 最多可以同时用几个线程
var seamphore = new Semaphore(3, 3);
Parallel.For(0, 20, i =>
{
seamphore.WaitOne();
Thread.Sleep(200);
Console.WriteLine($"I:{i}");
seamphore.Release();
});
seamphore.Dispose();
Console.WriteLine($"Elapsed time: {sw.ElapsedMilliseconds}ms");
}