C# 多线程

本文详细介绍了C#中的多线程概念,包括线程的建立与启动、线程同步、线程池的使用、Task任务以及死锁的避免等。重点讨论了线程的后台运行、线程池的工作原理、Monitor、Lock、Interlocked、Semaphore和ReaderWriterLockSlim等同步机制,以及异步委托和屏障在多线程编程中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

多线程

线程的建立与启动
void fun() {
    // .. 执行耗时操作
}
var start = new ThreadStart(fun); // 创建线程入口
var thread = new Thread(start); // 创建线程
thread.Start();
线程的常用方法
方法作用
Start启动线程,分配给线程除了CPU之外的所有系统资源,并执行各种安全检查。调用后线程处于Unstarted状态
Join当前线程阻塞直到调用这个方法的线程执行完毕
Sleep休眠指定毫秒数,醒来后变为就绪态
Suspend阻塞线程
Resume恢复挂起的线程,进入就绪状态
Abort永久删除线程的数据
Thread类的常用属性
属性作用
CurrentThread获取当前正在运行的线程
Name线程名称
Priority线程调度优先级 (Highest, AboveNormal, Normal, BelowNormal, Lowest
IsBackground设置后台线程
IsAlive线程如果启动且尚未正常终止,为true
ThreadState指示当前线程的状态
给线程传递数据
public class Data {
    public string Message;
}
public void fun(object o) {
    // 做耗时的任务
}
var d = new Data { Message = "数据" }
new Thread(fun).Start(d);
public class MyThread {
    private string data;
    public MyThread(string data) {
        this.data = data;
    }
    public fun() {
        // ... 耗时
    }
}
var myThread = new MyThread("info");
new Thread(myThread.fun).Start();
后台线程
  • 默认情况下,Thread类创建的是前台线程,线程池的线程是后台线程
  • 只有一个前台线程在运行,进程就在运行,后台线程不能独立于前台线程运行
  • 后台线程适合完成后台任务,例如word的拼写检查器
  • 通过IsBackground属性可以设置线程为后台线程或者前台线程,也可以获取线程是后台线程还是前台线程
线程池

线程池主要特点:

  1. 如果线程池线程始终保持繁忙,队列中包含挂起的工作,线程池会在一段时间后创建另一个辅助线程
  2. 每个进程有且仅有一个线程池,第一次将回调方法排入队列时才会创建线程池
  3. 最大线程数是可以配置的,工作数量大于最大线程数,新入队的工作就要排队
  4. 线程池通常用于服务器应用程序,可以来一个请求分配一个线程执行,不会占用主线程。
public void fun(object state) {
    // ... 耗时
}

ThreadPool.GetMaxThreads(); // 获得最大线程数量
// 传入线程池中的回调方法是一个委托 public delegate void WaitCallBack(object state);
ThreadPool.QueueUserWorkItem(fun);

线程池限制

  1. 不能把线程设置为前台线程
  2. 不能给线程设置优先级和名称
  3. 只能用于比较短的任务,如果要一直运行,用Thread类创建线程
  4. 不能阻塞线程池中的线程
线程同步

同步就是某一个时刻只有一个线程可以访问某个区域,这个区域也被称为临界区。

  1. System.Threading.Monitor
方法作用
Enter(object obj)在obj上获取排它锁
Exit(object obj)释放obj上的排它锁
Wait(object obj)释放obj上的锁并阻塞这个线程
Pluse(object obj)通知等待该锁的其中一个线程
PluseAll(object obj)通知所有等待该锁的线程
public void fun() {
    Monitor.Enter (this);
    // 临界区
    Monitor.Exit (this);
}

当时上述代码如果在获取或者释放之间发生异常,可能导致锁永远得不到释放,导致死锁

public void fun() {
    Monitor.Enter(this);
    try {
        
    }
    catch {
        ...
    }
    finally {
        Monitor.Exit(this);
    }
}

有时候可能还会调用这个类的其他方法, 例如 TryEnter 可以尝试获得锁,如果500ms后没有获取的话,就不再等待,继续下面的操作

if (Monitor.TryEnter(obj, 500)) {
    try {
        ...
    }
    finally {
        Monitor.Exit();
    }
} else {
    ...
}

上述方法也可以改写为

Bool lockToken = false;
Monitor.TryEnter(obj, 500, ref lockToken);
if (lockToken) {
    try {
        ...
    }
    finally {
        Monitor.Exit();
    }
} else {
    ...
}
  1. Lock

上述代码其实每次都要获得锁和释放锁,需要两行代码,有没有更加简便的方式呢?

object obj = new Object();
public void fun() {
    lock(obj) {
        // do something
    }
}

上述代码就可以保证在lock块内,最多只有一个进程

  1. Interlocked

为了实现进程的互斥

public int State {
    get {
        lock(this) {
            return ++state;
        }
    }
}

在访问一个变量的时候我们可以加锁,上述语句是简单的语句

Interlocked`可以使变量的简单语句原子化,提供了以线程安全的方式递增,递减以及交换值的方法。

与其他同步技术相比,使用这个类会快的很多,但是只能解决简单的同步问题

上述代码可以替换为

public int State {
    get {
        // 这个方法可以使变量递增1
        return Interlocaked.Increment(ref state);
    }
}
  1. mutex
// 第一个参数表示是否为这个锁由调用线程所拥有
var mutex = new Mutex(false);
public void fun() {
    if (mutex.WaitOne()) {
        try {
            // ... 临界区
        } finally {
            mutex.ReleaseMutex();
        }
    } else {
        
    }
}

其实就是在临界区的前后调用Mutex 的两个方法

WaitOneReleaseMutex

public static void Main(string[] args) {
    bool createNew;
    var mutex = new Mutex(false, "AppMutex", out createNew);
    if (!createNew) {
        Message.Show("不可以同时启动两个一样的APP");
        Application.Exit();
        return;
    }
    ...
}
信号量 Semaphore
// 第一个参数是指初始可以用的资源数为2
// 第二个参数是指最大可以用的资源数为5
// 第三个参数是信号量的名称
var sp = new Semaphore(2, 5, "sp");
public void fun() {
    sp.WaitOne(); // 获得信号量
    // do something
    sp.Release(); // 释放信号量, 释放后资源数 + 1,但是如果超过了最大可用的资源数,将会抛出异常
}

// Release(int n = 1); 不指定的话,默认释放一个资源
句柄
int base, num1, num2;
AutoResetEvent[] eventNum = {
    new AutoResetEvent(false),
    new AutoResetEvent(false)
}
ManualResetEvent eventBase = new ManualResetEvent();

public void fun1(object state) {
    base = 1 * 1;
    eventBase.Set();
}

public void fun2(object state) {
   	// 等待 eventBase 被 Set
    eventBase.WaitOne();
    num1 = base * 2;
    eventNum[0].Set();
}

public void fun3(object state) {
    // 等待 eventBase 被 Set
    eventBase.WaitOne();
    num2 = base * 4;
    eventNum[0].Set();
}

// 向进程池入队三个计算任务
ThreadPool.QueueUserWorkItem(computeBase);
ThreadPool.QueueUserWorkItem(computeNum1);
ThreadPool.QueueUserWorkItem(computeNum2);

// 等待数组里面的元素都被Set
waitHandle.WaitAll(eventNum);
Console.WriteLine(num1);
Console.WriteLine(num2);
Task

任务内部其实悄悄使用了线程池,任务有几个优点

  1. 可以定义连续的任务
  2. 可以在层次结构中安排任务,例如父任务可以创建新的子任务,可以创建依赖关系
启动任务
public void fun() {};

// 1.
new TaskFactory().StartNew(fun);
// 2.
Task.Factory.StartNew(fun);
// 3.
new Task(fun).RunSynchronously(); // 可以直接阻塞当前运行的线程,获得资源
new Task(fun).Start();
// 4.
new Task(fun, TaskCreationOptions.PreferFairness).Start();
TaskCreationOptions效果
LongRunning表示任务可能需要很长时间运行,这样调度器可能会使用新线程运行这个任务
AttachToParent如果父任务取消了,这个任务也会取消
PreferFairness以公平的方式运行
None默认
对任务传参
public void fun(object o) {
   COnsole.WriteLine(o);  // 输出 "xxx"
}
new Task(fun, "xxx").Start();
连续任务
public void first() {}
public void second() {}
public void third() {}

var t1 = new Task(first);
var t2 = t1.ContinueWith(second);
var t3 = t1.ContinueWith(third);
t1.Start();

上面的任务只可以保证 secondthirdfirst 之后运行,并不能保证secondthird 的运行关系,如果需要的话可以改成

var t1 = new Task(first);
var t2 = t1.ContinueWith(second);
var t3 = t2.ContinueWith(third);
t1.Start();

无论前面一个任务是完成了还是失败了,都会在前一个任务结束的时候启动下一个任务

可以通过枚举类型 TaskContinuationOptions 指定启动条件

Task t2 = t1.ContinueWith(DoOnError, TaskContinuationOptions.OnlyOnFaulted);
任务的结果
public string fun() {
    return "xxx";
}
var t = new Task(fun).Start();
COnsole.WriteLine(t.Result);
父子任务
static void ParentAndChild() {
    Task parent = new Task(ParentTask);
    parent.Start();
}

static void ParentTask() {
    Task child = new Task(ChildTask);
    child.Start();
}

static void ChildTask() {
	Thread.Sleep(3000);
    COnsole.WriteLine("xxx");
}
ReaderWriterLockSlim

ReaderWriterLockSlimReaderWriterLock 的轻量版

ReaderWriterLockSlim
var readerWriterLockSlim = new ReaderWriterLockSlim();
public void Read() {
    // TryEnterWriteLock(50) 也可以调用这个方法,返回值是bool,表示尝试等待50ms,要是没有资源的话,就返回 false
    readerWriterLockSlim.EnterReadLock();
    // read
    readerWriterLockSlim.ExitReadLock();
}
public void Write() {
    readerWriterLockSlim.EnterWriterLock();
    // writer
    readerWriterLockSlim.ExitWriterLock();
}
死锁的产生和避免

死锁的产生需要的条件是请求和保持,互斥,不可剥夺资源以及环路等待条件。

要避免死锁,可以让程序以相同的顺序申请对象

例如

a => 1
b => 2
c => 3

给资源编号,每次申请的时候都从小到大申请需要的资源

例如线程1需要ab资源,线程2需要ba资源。

都让他们先申请a再申请b,按顺序申请就不会出现死锁。

异步委托
  1. 轮询
public delegate int beginDelegate();
class Worker {
    private event beginDelegate bd;
    public void work() {
        if (bd != null) {
            foreach (beginDelegate e in bd.GetInvocationList()) {
                IAsyncResult r = e.BeginInvoke(null, null);
                while (!r.IsCompleted) {
                    Thread.Sleep(1); // 如果没有完成的话,就等待一段时间
                }
                Console.WriteLine(e.EndInvoke(r)); // 获得返回值
            }
        }
    }
}
  1. 等待句柄
public delegate int beginDelegate();
class Worker {
    private event beginDelegate bd;
    public void work() {
        if (bd != null) {
            foreach (beginDelegate e in bd.GetInvocationList()) {
                IAsyncResult r = e.BeginInvoke(null, null);
                while (!r.AsyncWaitHandle.waitOne(50)) {
                    Thread.Sleep(1);
                }
                Console.WriteLine(e.EndInvoke(r)); // 获得返回值
            }
        }
    }
}
  1. 异步回调
public delegate int beginDelegate();
class Worker{
    private event beginDelegate bd;
    public void Work() {
        if (bd != null) {
            foreach(beginDelegate e in bd.GetInvocationList()) {
                IAsyncResult r = e.BeginInvoke(GetGrade, e);
            }
        }
    }
    public void GetGrade(IAsyncResult r) {
        beginDelegate e = r.AsyncState as beginDelegate;
        Console.WriteLine(e.EndInvoke(r));
    }
}
屏障
static void Main(string[] args)
{
    Barrier barrier = new Barrier(4, it => {
        Console.WriteLine("再次集结,友谊万岁,再次开跑");
    });
    string[] names = { "张三", "李四", "王五", "赵六" };
    Random random = new Random();
    foreach(string name in names)
    {
        Task.Run(() => {
            Console.WriteLine($"{name}开始跑");
            int t = random.Next(1, 10);
            Thread.Sleep(t * 1000);
            Console.WriteLine($"{name}用时{t}秒,跑到友谊集结点");
            barrier.SignalAndWait();
            Console.WriteLine($"友谊万岁,{name}重新开始跑");
        });
    }
    Console.ReadKey();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值