异步多线程之Thread

x33g5p2x  于2021-11-13 转载在 其他  
字(9.8k)|赞(0)|评价(0)|浏览(396)

上一篇:异步多线程之入门介绍
下一篇:异步多线程之入ThreadPool,待更新

介绍

Thread 是 1.0 时代的产物,当时 C# 就考虑到了多线程,于是就设计了 Thread 。其实现在不推荐使用,除非在维护老的项目已经用了的。Thread 也是比较鸡肋的,有很多缺陷,但也并不是一无是处。

Thread API

这里对 Thread 的一些常用 API 进行介绍,使用一些案例进行说明。由于 Thread 的不可控与效率问题,Thread 现在已经不常用了,这里介绍一些 API ,想更深入的同学可以继续研究研究。

Instance

首先看 Thread 的构造函数,有 ThreadStart 、ParameterizedThreadStart 、maxStackSize 类型的参数,这三个常用的也就 ThreadStart ,其他两个可以作为了解。

分别 F12 查看 ThreadStart、ParameterizedThreadStart ,可以看到 ThreadStart 是无参数类型的委托、ParameterizedThreadStart 是有参数类型的委托。maxStackSize 是指定线程占用的最大内存数。


接着我们创建一个简单的案例,启动一个线程,模拟做一些任务行。如下代码

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任务

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);
thread.Start();

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

启动程序,可以看到线程 1(主线程),没有等待线程 3(子线程)执行完成匿名方法内的任务,再执行 Main 结束这段代码。如果使用的是 winform 是不会卡界面的。

这就是异步多线程,异步在于线程 1 并没有等待线程 3 执行完成任务,再执行线程 1 内的下一行,而是让线程 3 在不影响线程 1 执行任务的情况下执行,这就是异步。多线程在于我们启动了一个线程 3(子线程),在 Main 方法由线程1(子线程)与线程 3(主线程)一起完成 Main 方法内的代码,这就是多线程。

说到委托可会有小伙伴发出疑问,为啥不用 Action ?

因为在这个版本还没有 Action、Func,这是在 .Net 3.0 时代的产物,Action、Func 的出现就是为了统一,也是为了解决此类问题。

在 dotnet 框架,也建议最好使用 Action、Func,所以,在这使用 Action 是不可以的。如下

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Action action = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任务

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};

ThreadStart threadStart = action;

Thread thread = new Thread(threadStart);
thread.Start();

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

Suspend、Resume

Suspend 挂起、Resume 唤醒,这两个是一对相互对应的 API,使用时这两个容易产生死锁,其实在实际中也是不应该使用的,.NET 框架已经抛弃了,说的很清楚了。

为什么会死锁呢?比如你开启了一个子线程 01,对 A 文件进行读写操作,此时你对子线程 01 进行了挂起。当你另外一个线程对 02 A 文件进行操作时,此时提示会 A 文件被占用,就行形成死锁。

Abort、ResetAbort

Abort 销毁,很多人在使用,这种是抛异常方式,使子线程销毁结束。这个功能也比较鸡肋,Abort 时子线程并不能立即停止,往往会有一些延迟,那这个销毁有时也不能达到我们可控的效果。

比如,在一个方法内开了一个子线程进行数据计算,但执行的时间太长了,我们等待了 5000 ms,此时 Abort 子线程,是不能立马让子线程停止计算,而是可能要等一会才能结束子线程。

比如,发出的动作,可能收不回来。查询数据库来说,当一个查库命令发送到数据库,我们在C# 执行了 Abort,但查库这个命令是收不回来的,因为他是在数据库层面,当数据库查询完成只是没有接收响应的线程罢了。

Abort 不建议使用,如果使用,一定要 try catch 一下。

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId}");
    // 做一些任务

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId}");
};
Thread thread = new Thread(threadStart);

thread.Start();

try
{
    thread.Abort(); // 销毁,方式是抛异常,不一定及时
}
catch (Exception ex)
{
    //Thread.ResetAbort(); // 取消异常
}

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId}");

Console.ReadLine();

Suspend、Resume、Abort 这几个方法不建议使用,操作线程暂停、销毁或者其他操作都是不可控的,应为线程本身是操作系统的, CPU 分时分片会按照自己的规则进行运行,此时已经不是程序可以进行控的了。 既然设计了 Thread 不可能一无是处,接下来我们说些有用的

Join

线程等待 ,Join 可以一直等,也可以设置超时,超时就是等待一定时间,就不等了。等待的过程中主线程处于闲置状态等着子线程完成任务。如果是 winform 是会卡界面的,主线程等待也是一种工作。

例如:threadStart 我们模拟任务耗时 5 秒,在 thread.Start() 任务开始后,使用 thread.Join() 等着子线程完成工作

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任务
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join();

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

启动程序,可以看到是我们想要的结果(与同步执行一样),主线程 1 一直等着 子线程 3 完成执行的任务。如果是 winform 是会卡界面的,虽然 thread.Join() 主线程 1 会等着子线程 3 完成工作,但主线程 1 等着也是一种工作。

接着我们看下超时等待,Join 的重载方法

例如:threadStart 我们模拟任务耗时 5 秒,在 thread.Start() 任务开始后,使用 thread.Join(3*1000) ,让主线程最多等子线程 3 秒,如果 3 秒子线程还未完成任务,就不等待了

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任务
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join(3 * 1000);

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

启动程序,主线程 1 开始任务,子线程 3 也开始任务,当子线程执行 3 s 后(期间主线程 1 在等待),主线程 3 开始执行任务了。

注意:thread.Join(n * 1000) 并不是一定会等待那么长时间,而是最多等待,期间子线程任务执行完成后,就不等待了。

例如:threadStart 任务方法模拟 5 s,thread.Join(7 * 1000) 主线程等待 7 s

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任务
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

thread.Join(7 * 1000);

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

ThreadState

线程状态,ThreadState 也可以做线程等待,等待的过程中主线程处于闲置状态等着子线程完成任务。如果是 winform 是会卡界面的,主线程等待也是一种工作。

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任务
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Start();

while (thread.ThreadState != ThreadState.Stopped)
{
    Thread.Sleep(200); // 当前线程休息 200 毫秒
}

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

Sleep

线程暂停,Sleep 当前线程暂停。如果是 winform 是会卡界面的,当 Sleep 时,CPU 分片就交出去了,主线程并不在工作状态。

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.ReadLine();

IsBackground

是否是后台线程,当实例 Thread 时,默认是前台线程(IsBackground == false )。前台线程一定要任务完成,才会让进程退出。后台线程(IsBackground == true)会随着进程的结束而结束,无论子线程任务是否完成。

前台线程,意思也就是,当我们启动一个程序,当关闭程序时,如果还有子线程执行任务,当前进程是不会退出的,会等待着子进程将任务执行完成,也就是会阻止进程结束,反之亦然。

例如:前台线程,启动控制台后,主线程执行完任务后,会等待子线程任务完成(5s)后,窗口才会被关闭

static void Main(string[] args)
{
    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };

    Thread thread = new Thread(threadStart);
    thread.Start();

    while (thread.ThreadState != ThreadState.Stopped)
    {
        Thread.Sleep(200); // 当前线程休息 200 毫秒
    }

    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

例如:后台线程,启动控制台后,主线程任务执行完毕后,窗口会立马被关闭

static void Main(string[] args)
{
    Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

    ThreadStart threadStart = () =>
    {
        Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
        // 做一些任务
        Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

        Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    };

    Thread thread = new Thread(threadStart);
    thread.IsBackground = true;
    thread.Start();

    Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");

    Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
}

Priority

线程可以设置优先级,当线程从高到低分配了优先级,在向 CPU 申请线程时会优先分配。但是这个功能也比较鸡肋,对于 CPU 而言,当他们同时过来,只是会为优先级高的先分进行分片,但优先级低的并不是不会分配,也不代表优先级高的就会先执行完成,这也取决执行的任务量。其实优先级也没什么用,多线程本来就是无序的。

Console.WriteLine($"Main 方法开始,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

ThreadStart threadStart = () =>
{
    Console.WriteLine($"Task Start ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
    // 做一些任务
    Thread.Sleep(5 * 1000); // 模拟任务耗时 5 秒

    Console.WriteLine($"Task End ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");
};

Thread thread = new Thread(threadStart);
thread.Priority = ThreadPriority.Highest;// CPU 会先执行,不代表 Highest 就最优先
thread.Start();

Console.WriteLine($"thread IsBackground:{thread.IsBackground},DateTime:{DateTime.Now.ToLongTimeString()}");

Console.WriteLine($"Main 方法结束,ThreadId:{Thread.CurrentThread.ManagedThreadId},DateTime:{DateTime.Now.ToLongTimeString()}");

总结

其实现在来说 ,1.0 时代的 Thread 已经没有什么优势,现在 Thread 唯一有意义的就是 IsBackground = false,这个前线程(前台线程会阻碍进程的退出),后续的多线程设计都是后台线程,没有前台线程这个功能设计。

相关文章