出售域名 11365.com.cn
有需要请联系 16826375@qq.com
在手机上浏览
在手机上浏览

多线程之Thread

发布日期:2022-09-09

延申阅读:

1) 多线程之Thread

2) 多线程之线程池

3) 多线程之Task

 

一、什么是线程
线程是程序能够独立运行的最小单位,线程具有进程所具有的特征,所以线程又叫轻型进程。
1)一个进程可以有若干个线程,默认只有一个线程。
2)进程入口的第一个线程就是主线程,也就是默认的线程,主线程一般就是UI线程。
3)进程本身不执行代码, 执行代码的是线程。
4)线程可以认为是受系统自主运行,不被人为控制的一个执行过程。
5)线程一定会从头到尾执行完,所以它有许多状态。
6)线程创建的默认都是前台线程,前台线程全部执行完成,程序才会退出。
7)后台线程,当所有前台线程执行完成,立即退出。因此非关键操作都可以用后台线程。
8)线程的并发,除CPU多核是真实并发外,都是CPU的运算切换,只是切换时间非常短,让人感觉是并发。

 

二、创建线程
最基本的创建方式
public delegate void ThreadStart(); //ThreadStart 是一个委托
public Thread(ThreadStart start); //public Thread(委托);

//简化写法
由于ThreadStart是一个委托,那么我们就可以用Lambda表达式
Thread thread=new Thread(()=>method());

1、静态方法、实例方法
和平时调用的方法一样,实例需要new 类名().实例方法(),静态方法可以直接调用类名.静态方法();

2、无参方法
调用无参委托 ThreadStart
private static void MethodNoParam()
{
    Console.WriteLine("无参方法");
}
var thread1 = new Thread(new ThreadStart(MethodNoParam));

//简写
var thread1 = new Thread(()=>{ Console.WriteLine("无参方法"); });

3、有参方法
调用有参委托 ParameterizedThreadStart
private static void MethodWithParam(object obj)
{
    Console.WriteLine($"有参方法,参数是:{obj}");
}
var thread2 = new Thread(new ParameterizedThreadStart(MethodWithParam));

//简写
var thread2 = new Thread((param)=> { Console.WriteLine($"有参方法,参数是:{param}"); });

4、线程属性
ManagedThreadId     获取当前托管线程的唯一标识符。
Name    获取或设置线程的名称。
ThreadState     获取一个值,该值包含当前线程的状态。
Priority    获取或设置指示线程的调度优先级的值。
IsAlive     获取指示当前线程的执行状态的值。
IsBackground    获取或设置一个值,该值指示某个线程是否为后台线程。
IsThreadPoolThread    获取指示线程是否属于托管线程池的值。

ThreadState 线程状态
Unstarted //未启动
Running //正在运行 任务正在执行中
WaitSleepJoin //阻塞等待中
Aborted //已终止
Stopped //已停止 任务已完成

Priority 优化级。优化级越高拥有越多的迭代机会
Highest //最高
Normal  //正常
Lowest  //最低

 

三、前台线程和后台线程
主功能用前台线程,副功能用后台线程,设置后台线程thread.IsBackground = true;
1)前台线程,前台线程必须全部跑完,进程才完成并退出
2)后台线程,是指在程序运行的时候在后台提供一种通用服务的线程,并且这种线程并不属于程序中不可或缺的部分。
前台线程跑完后,后台线程同时也退出(即使未完成也退出)

private void Form1_Load(object sender, EventArgs e)
{
    CreateThread();
}        

private void CreateThread()
{
    var thread = new Thread(x=> {
        while(true) //while(true)保持任务永不退出,因此线程常驻
        {
            System.IO.File.AppendAllText(@"D:\Study\ForegroundThreadTest.txt", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") "\r\n");
            Thread.Sleep(1000); //每次遍历都挂起1秒,即每1秒阻塞一次线程
        }
    });

    //thread.IsBackground = true; //设置为后台线程,才会在前台线程退出后,同时退出
    thread.Start();
}

可以看出,当程序退出后,线程没有退出,这是因为线程的任务并没有完成。
要让前台线程也同时退出,在编写Winform/Wpf程序时,可以加上这段代码以退出程序。

System.Environment.Exit(0);

 

四、线程方法
Sleep 线程挂起,阻塞当前线程,使当前线程处在延时状态
Thread.Sleep(100); //毫秒

Join 两个线程间同步。因为线程是并发的,所以会同时执行,而Join方法会阻塞后一个线程,直到当前线程完成。
t1.Start();
t1.Join();
t2.Start(); //线程t1完成后才执行

Abort 终止线程。抛出线程异常达到终止的效果,可能引发不可预知后果,不建议使用


五、线程同步
线程同步即资源同时仅允许一个线程进入,防止多个线程同时访问资源,造成数据混乱。
实现线程同步有3个方法。
1、lock
静态方法lock(静态变量),实例方法lock(this)

private static readonly object obj = new object();
private static void GetMoney(string threadName)
{
    lock (obj)
    {
        if (TotalMoney - 100 < 0)
            Console.WriteLine("余额不足了!");
        else
        {
            Console.WriteLine($"{threadName}取钱{100}");
            TotalMoney -= 100;
            Console.WriteLine($"余额:{TotalMoney}");
        }
    }
}

经典的银行取钱操作,当不加锁时会造成数据错误

只有线程同步才能得到正确的结果

2、特性 [MethodImpl(MethodImplOptions.Synchronized)]

[MethodImpl(MethodImplOptions.Synchronized)]
private static void GetMoney(string threadName)
{
    if (TotalMoney - 100 < 0)
        Console.WriteLine("余额不足了!");
    else
    {
        Console.WriteLine($"{threadName}取钱{100}");
        TotalMoney -= 100;
        Console.WriteLine($"余额:{TotalMoney}");
    }
}

3、Monitor.TryEnter
区别于lock在于不会因为资源被占用,而一直阻塞。
详情参考:https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.monitor.tryenter?redirectedfrom=MSDN&view=net-6.0

private static readonly object obj = new object();
private static void GetMoney(string threadName)
{
    if (Monitor.TryEnter(obj, 500)) //如果资源被占用,那么等待500毫秒
    {
        try
        {
            if (TotalMoney - 100 < 0)
                Console.WriteLine("余额不足了!");
            else
            {
                Console.WriteLine($"{threadName}取钱{100}");
                TotalMoney -= 100;
                Console.WriteLine($"余额:{TotalMoney}");
            }
        }
        finally
        {
            Monitor.Exit(obj);//释放对象的锁
        }
    }
}

4、互拆锁 Mutex
Mutex是全局的,比较消耗资源

const string MutexName = "CSharpThreadingCookbook";
using (var m = new Mutex(false, MutexName))
{
    if (!m.WaitOne(TimeSpan.FromSeconds(5), false))
    {
        WriteLine("Second instance is running!");
    }
    else
    {
        WriteLine("Running!");
        ReadLine();
        m.ReleaseMutex();
    }
}

 

六、死锁 deadlock
死锁是由于两个或以上的线程互相持有对方需要的资源,导致这些线程处于等待状态,无法执行。线程同步会造成死锁,要尽量避免!
互斥性:线程对资源的占有是排他性的,一个资源只能被一个线程占有,直到释放。
请求和保持条件:一个线程对请求被占有资源发生阻塞时,对已经获得的资源不释放。
不剥夺:一个线程在释放资源之前,其他的线程无法剥夺占用。
循环等待:发生死锁时,线程进入死循环,永久阻塞。说白了死锁就是两个或以上线程交替访问,形成死循环

static void LockTooMuch(object lock1, object lock2)
{
    lock (lock1)
    {
        Console.WriteLine("LockTooMuch-lock1");
        Console.WriteLine($"LockTooMuch-lock1的状态:{Thread.CurrentThread.ThreadState}");
        lock (lock2) 
        {  
            Console.WriteLine("LockTooMuch-lock2");
            Console.WriteLine($"LockTooMuch-lock2的状态:{Thread.CurrentThread.ThreadState}");
        }
    }
}

static void Main(string[] args)
{
    object lock1 = new object();
    object lock2 = new object();

    var t1 = new Thread(() => LockTooMuch(lock1, lock2)); //t1 线程
    t1.Start();
    //t1.Join(); //阻塞主线程,先让t1线程完成,这样不会死锁,但没有达到并发效果

    /*
    主线程和子线程是并发的,因此lock2还没有被t1线程锁定,可以访问,打印t1状态、会死锁等信息
    此时t1线程要访问lock2的资源,但lock2被锁定,t1处在阻塞等待状态
    然后主线程要访问lock1的资源,但lock1被t1线程锁定,主线程片在阻塞等待状态
    双方互相在等待对方释放资源,处在死锁状态
    */
    lock(lock2) //主线程
    {
        Console.WriteLine($"主线程的状态:{Thread.CurrentThread.ThreadState}");
        Console.WriteLine("会死锁");
        lock(lock1)
        {
            Console.WriteLine("读取资源");
        }

        /*可以防止死锁,当超过3秒未能访问lock1的资源时,释放资源。t1线程得以继续下去
        if(Monitor.TryEnter(lock1,3000))
        {
            Console.WriteLine("访问lock1成功");
        }
        else
        {
            Console.WriteLine("访问lock1超时,主线程退出");
        }
        */
    }
}

 

七、跨线程访问
.net禁止跨线程访问,因为这是不安全的。一般UI控件都是UI主线程中,但可以委托调用
textBox1.Invoke(new EventHandler((x,y)=> {
    textBox1.Text = i.ToString();
}));

简写
Invoke((EventHandler) delegate { textBox1.Text = DateTime.Now.ToString(); });

或者
Action act = () =>
{
    textBox1.Text = i.ToString();
};

textBox1.Invoke(act);

 

八、线程通讯
1)AutoResetEvent 自动信号
var autoReset=new AutoResetEvent(false); //false表示初始化无信息 true表示初始化有信息
WaitOne 表示设置接收信息,当没有信号时,阻塞线程
Set 表示发送信息,当前WaitOne标记的地方,释放阻塞,线程得以继续运行
此类的Reset不起作用,和ManualResetEvent最大的区别就是,发送信息号,立即复位到无信息状态

private static AutoResetEvent autoReset = new AutoResetEvent(false); //初始化无信息号状态,按键位1通知线程往下执行
static void Main(string[] args)
{           
    var t1 = new Thread(() =>
    {
        while(true)
        {
            autoReset.WaitOne();
            Console.WriteLine("这是输出消息");                    
        }
    });

    t1.Start();

    while (true)
    {
        switch (Console.ReadLine())
        {
            case "1":
                autoReset.Set(); //AutoResetEvent自动复位到无信号状态
                Console.WriteLine("开始运行");
                break;
            case "2":
                autoReset.Reset(); //ManualResetEvent有效
                Console.WriteLine("暂停运行");
                break;
            default:
                break;
        }
    }
}

2)ManualResetEvent 手动信号
WaitOne 表示设置接收信息,当没有信号时,阻塞线程
Set 表示发送信息,当前WaitOne标记的地方,释放阻塞,线程得以继续运行
Reset 表示重置信息,即无信息状态

和AutoResetEvent最大的区别是,不会自动复位,需要手动设置。

3)CountdownEvent 计数器
Signal //计一次线程运行
Wait //阻塞当前线程,只到计数完成