Thread

先来看几个基本概念(纯属个人见解,可能不准确):

进程:程序运行时,占用的全部运行资源的总和。

线程:线程由线程调度程序在内部管理,CLR通常将这一功能委托给操作系统。线程也可以有自己的计算资源,是程序执行流的最小单位。任何的操作都是由线程来完成的。

每个线程都在操作系统的进程内执行,而操作系统进程提供了程序运行的独立环境。

多线程:多核cpu协同工作,多个执行流同时运行,是用资源换时间。(单核cpu,不存在所谓的多线程)。

单线程应用:在进程的独立环境中只跑一个线程,所以该线程拥有独立权。

多线程应用:单个进程中会跑多个线程,它们会共享当前的执行环境(尤其是内存)。

在单核计算机上,操作系统必须为每个线程分配“时间片”来模拟并发。而在多核或多处理器计算机上,多个线程可以真正的并行执行。(可能会受到计算机上其他活动进程的竞争)。

win10上的时间片(使用微软官方小工具测得):

Thread

  Thread的对象是非线程池中的线程,有自己的生命周期(有创建和销毁的过程),所以不可以被重复利用(一个操作中,不会出现二个相同Id的线程)。

Thread的常见属性:

  • 线程一旦开始执行,IsAlive就是true,线程结束就变成false。
  • 线程结束的条件是:线程构造函数传入的委托结束了执行。
  • 线程一旦结束,就无法再重启。
  • 每个线程都有一个Name属性,通常用于调试,线程的Name属性只能设置一次,以后更改会抛出异常。
  • 静态的Thread.CurrentThread属性,会返回当前线程。

Thread的常见用法:

join

调用join方法可以等待另一个线程结束。

private void button5_Click(object sender, EventArgs e) {     
Console.WriteLine($"===============Method start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}==================="); //开启一个线程,构造方法可重载两种委托,一个是无参无返回值,一个是带参无返回值 Thread thread = new Thread(a => DoSomeThing("Thread")); //当前线程状态 Console.WriteLine($"thread's state is {thread.ThreadState},thread's priority is {thread.Priority} ,thread is alived :{thread.IsAlive},thread is background:{thread.IsBackground},thread is pool threads: {thread.IsThreadPoolThread}"); //告知操作系统,当前线程可以被执行了。 thread.Start(); //阻塞当前执行线程,等待此thread线程实例执行完成。无返回值 thread.Join(); //最大等待的时间是5秒(不管是否执行完成,不再等待),返回一个bool值,如果是true,表示执行完成并终止。如果是false,表示已到指定事件但未执行完成。 thread.Join(5000); Console.WriteLine($"===============Method end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},,Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}==================="); }
private void DoSomeThing(string name) { Console.WriteLine($"do some thing start time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}"); long result = 0; for (long i = 0; i < 10000 * 10000; i++) { result += i; } Console.WriteLine($"do some thing end time is {DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")},Thread ID is {Thread.CurrentThread.ManagedThreadId},is back ground: {Thread.CurrentThread.IsBackground}"); }

Sleep

Thread.Sleep()会暂停当前线程,,并等待一段时间。其实,Thread.Sleep只是放弃时间片的剩余时间,让系统重新调度并选择一个合适的线程。

在没有其他活动线程的情况下,使用Thread.Sleep(0)还是会选上自身,即连任,系统不会对其做上下文切换。

static void Main(string[] args)
{
    Stopwatch stopwatch=new Stopwatch();
    stopwatch.Start();
    Thread.Sleep(0);
    stopwatch.Stop();
    System.Console.WriteLine(stopwatch.ElapsedMilliseconds); //返回0
}

而Thread.Sleep(大于0)却让当前线程沉睡了,即使只有1ms也是沉睡了,也就是说当前线程放弃下次的竞选,所以不能连任,系统上下文必然发生切换。

阻塞

如果线程的执行由于某种原因导致暂停,那么就认为该线程被阻塞了。例如在Sleep或者Join等待其他线程结束。被阻塞的线程会立即将其处理器的时间片让给其他线程,从此就不再消耗CPU时间片时间。可以通过ThreadState这个属性来判断线程是否处于被阻塞状态。

下面是ThreadState的所有值(官方截图):

ThreadState是一个flags枚举,通过按位的形式可以合并数据的选项。

bool isBlocked = (thread.ThreadState & ThreadState.WaitSleepJoin) != 0;

 本地独立(Local)

CLR为每个线程分配自己的内存栈(Statck),以便本地变量保持独立。

static void Main(string[] args)
{
    //在新线程上调用Test()
    new Thread(Test).Start();
    //在主线程上调用Test()
    Test();
}
static void Test()
{
    //i 是本地的局部变量,在每个线程的内存栈上,都会创建i独立的副本
    for (int i = 0; i < 5; i++)
    {
        System.Console.Write("o");
    }
}
//结果会输出十个o

 共享(Shared)

如果多个线程都引用了同一个对象的实例,那么它们就共享了数据。

class Program
{
    bool flag;
    static void Main(string[] args)
    {
        //创建一个实例
        Program program = new Program();
        //分别在新线程上和主线程上调用同一实例的Test()方法
        new Thread(program.Test).Start();
        program.Test();
        //输出一个FLAG
    }
    void Test()
    {
        if (!flag)
        {
            flag = true;
            System.Console.WriteLine("FLAG");
        }
    }
}

被Lambda表达式或匿名委托所捕获的本地变量,会被编译器转换为字段(field),所以也会被共享。

class Program
{
    static void Main(string[] args)
    {
        //此局部变量会被编译器转换为所在类的字段,进行处理,所以会被共享。
        bool flag = false;
        ThreadStart threadStart = () =>
        {
            if (!flag)
            {
                flag = true;
                System.Console.WriteLine("FLAG");
            }
        };
        new Thread(threadStart).Start();
        threadStart();
    }

 

 静态字段(field)也会在线程间共享数据。

class Program
{
    //静态字段在同一应用域下的所有线程中被共享
    static  bool flag = false;
    static void Main(string[] args)
    {
        new Thread(Test).Start();
        Test();
    }
    static void Test()
    {
        if(!flag){
            flag=true;
            System.Console.WriteLine("FLAG");
        }
    }
}

线程安全 

由于线程中数据可以共享,可能会引发线程安全(Thread Safety)问题(上面三个例子都有线程安全问题,应尽可能的避免使用共享状态)。

在读取和写入共享数据的时候,通过使用一个互斥锁(exclusive lock),就可以解决上面三个例子的线程安全问题。锁可以基于任何引用类型对象。使用lock语句来加锁,当两个线程同时竞争一个锁的时候,一个线程会等待或阻塞,直到锁变成可用状态。

class Program
{
    //静态字段在同一应用域下的所有线程中被共享
    static bool flag = false;
    static readonly object locker = new object();
    static void Main(string[] args)
    {
        new Thread(Test).Start();
        Test();
    }
    static void Test()
    {
        lock (locker)
        {
            if (!flag)
            {
                System.Console.WriteLine("FLAG");
                Thread.Sleep(1000);
                flag = true;
            }
        }
    }
}

 

注意:防止lock使用不当引起死锁,最好不要lock public的东西,例如:

1lockthis2lock(“string”)
3locktypeofint))

向线程传递数据

往线程的启动方法里传递数据:

1、使用Thread的重载构造方法和Thread.Start()的重载方法来传递数据。(只能接收object类型的参数,是C# 3.0之前的用法)

class Program
{
    static void Main(string[] args)
    {
        new Thread(Print).Start("asdf");
    }
    static void Print(object? message)
    {
        System.Console.WriteLine(message);
    }
}

2、使用Lambda表达式,在里面使用参数调用方法。

class Program
{
    static void Main(string[] args)
    {
        new Thread(()=>Print("Hello World!")).Start();
    }
    static void Print(string message)
    {
        System.Console.WriteLine(message);
    }
}

使用Lambda表达式可以很简单的给Thread传递参数。但是线程开始后,可能会不小心修改了被捕获的变量,这要多加注意。

static void Main(string[] args)
{
    //i是一个瞬时的临时变量
    for (int i = 0; i < 10; i++)
    {
        //每个线程对Console.Write(i)的调用,传递的是当时的i值。
        new Thread(()=>Console.Write(i)).Start();
    }
}

解决办法:

static void Main(string[] args)
{
    for (int i = 0; i < 10; i++)
    {
        //每次循环都有一个临时变量记录i的值。
        int temp=i;
        new Thread(()=>Console.Write(temp)).Start();
    }
    //但是输出顺序依然无法保证
}

 前台和后台线程

默认情况下,手动创建的线程就是前台线程。只要有前台线程在运行,那么应用程序就会一直处于活动状态。

 thread 默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。可以把thread 指定为后台线程,随着进程的退出而终止。

//false,默认是前台线程,启动后一定要完成任务的,即使程序关掉(进程退出)也要执行完。
Console.WriteLine(thread.IsBackground); 
thread.IsBackground = true;//指定为后台线程。(随着进程的退出而退出)

 

static void Main(string[] args)
{
    Thread thread = new Thread(() =>
      {
          Console.ReadLine();
      });
    if (args.Length > 0)
        thread.IsBackground = true;
    thread.Start();
    /*
        如果运行时不传递参数,thread为前台线程,程序会等待输入而不会退出。
    如果在运行时传递参数(dotnet run XXXX),thread变成后台线程,会随着进程的结束而终止线程。
     */
}

 

注意:线程的前台、后台状态与它的优先级无关。

Thread的回调用法:

Thread没有像Framework中的delegate的回调用法,如果需要回调得自动动手改造:

private void CallBack(Action action, Action calback)
{
    Thread thread = new Thread(() => { action(); calback(); });
    thread.Start();
}
//无参无返回值
CallBack(() => Console.WriteLine("好吗?"), () => Console.WriteLine("好的!"));
private Func<T> CallBackReturn<T>(Func<T> func)
{
    T t = default(T);
    Thread thread = new Thread(() =>
    {
        t = func();
    });
    thread.Start();
    return () =>
    {
        thread.Join();
        return t;
    };
}
//带返回值得用法
Func<int> func = CallBackReturn<int>(() => DateTime.Now.Second);
Console.WriteLine("线程未阻塞");
int result = func.Invoke();
Console.WriteLine("result:" + result);

ThreadPool 线程池

posted @ 2021-02-20 18:18  厉致彤  阅读(399)  评论(0编辑  收藏  举报