07.并发编程Threads

参考文档

1. 基础概念

1.1 进程/线程/多线程

进程(Process

计算机概念,程序在服务器运行时占据全部计算机资源综合指标,虚拟的数据;普通的解释就是,进程是程序的一次执行

线程(Thread

计算机概念,进程在响应操作时最小单元,也包含CPU/内存/网络/硬盘IO,是虚拟的概念;线程可以理解为进程中的执行的一段程序片段

句柄

操作系统用来标识应用程序的某一个最小单元,比如一个窗体标识,一个按钮标识等,一个long数字,操作系统通过句柄识别应用程序,也可以通过句柄操作控件

多线程

计算机概念,一个进程有多个线程同时运行

2. C#中的线程(Thread

在C#中,Thread类是操作线程的,是C#对线程的一个封装

说明:这种方式不是特别重要,因为此操作线程方法是.NET Framework 1.0低版本的,只做学习了解即可

2.1 构造函数

线程函数通过委托传递,可以不带参数,也可以带参数(只能有一个参数),可以用一个类或结构体封装参数

方式一,实例一个无参数,无返回值的线程对象

ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);
thread.Start();

方式二:实例一个有参数,无返回值的线程对象

ParameterizedThreadStart threadStart = (v) => Console.WriteLine("有参数无返回值委托:" + v);
Thread thread = new Thread(threadStart);
thread.Start("libai");

2.2 属性,方法

静态属性(常用)

属性名 属性值类型 描述
Thread.CurrentThread Thread 获取当前正在运行的线程
Thread.CurrentContext Context 获取当前线程的上下文

实例属性(常用)

属性名 属性值类型 描述
thread.ThreadState ThreadState 获取当前线程状态
thread.Priority ThreadPriority 获取或设置线程优先级
thread.Name string 获取或设置线程名称
thread.ManagedThreadId int 获取线程唯一ID
thread.IsBackground bool 获取或设置线程是否为后台线程

实例方法(常用)

方法名 返回值类型 描述
thread.Start(...) void 启动线程
thread.Abort(...) void 终止线程
thread.Join(...) bool 等待线程

2.3 线程等待方式

方式一:持续判断线程状态,直到线程停止

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
                Thread thread = new Thread(threadStart);
                
                thread.Start();

                while (thread.ThreadState != ThreadState.Stopped)
                {
                    Thread.Sleep(200);
                }
            }
        }
    }
}

方式二:Join方法

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
                Thread thread = new Thread(threadStart);
                
                thread.Start();
                thread.Join();  // 一直等待,直到结束
                thread.Join(3000);  // 最多等待 3000 毫秒
            }
        }
    }
}

2.4 线程状态值

// 指定的执行状态
public enum ThreadState
{
    // 启动线程
    Running = 0,
    // 正在请求线程停止,仅供内部使用
    StopRequested = 1,
    // 正在请求线程挂起
    SuspendRequested = 2,
    // 该线程将作为后台线程
    Background = 4,

    Unstarted = 8,
    // 该线程已停止
    Stopped = 16,
    // 线程将受阻
    WaitSleepJoin = 32,
    // 该线程已挂起
    Suspended = 64,

    AbortRequested = 128,

    Aborted = 256
}

2.5 线程的优先级别

.NET为线程设置Priority属性来定义线程执行的优先级别,里面包含5个选项,其中Normal是默认值,除非系统有特殊要求,否则不应该随便设置线程的优先级别;因为线程是计算机概念,执行由操作系统执行,所以这个属性只是增加优先级概率,并不一定按照设置的优先级执行

ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
Thread thread = new Thread(threadStart);

thread.Priority = ThreadPriority.Highest;

thread.Start();

线程优先级选项:

// 指定的调度优先级的
public enum ThreadPriority
{
    // 可以安排在具有任何其他优先级的线程之
    Lowest = 0,
    // 可以将 Thread 安排在具有 Normal 优先级的线程之后,在具有 Lowest 优先级的线程之前
    BelowNormal = 1,
    // 可以将 Thread 安排在具有 AboveNormal 优先级的线程之后,在具有 BelowNormal 优先级的线程之前
    Normal = 2,
    // 可以将 Thread 安排在具有 Highest 优先级的线程之后,在具有 Normal 优先级的线程之前
    AboveNormal = 3,
    // 可以将 Thread 安排在具有任何其他优先级的线程之前
    Highest = 4
}

2.6 线程池(ThreadPool

概念说明

由来:在 Thread 中对线程的管理需要手动操作,在不断的开启和销毁中,存在很大的开销,为了让线程反复使用,出现了池化思想;线程池可以节省资源,控制线程总数量,防止滥用

线程池维护一个请求队列,线程池的代码从队列提取任务,然后委派给线程池的一个线程执行,线程执行完不会被立即销毁,这样既可以在后台执行任务,又可以减少线程创建和销毁所带来的开销

线程池并不是在 CLR 初始化的时候立刻创建线程的,而是在应用程序要创建线程来执行任务的时候,线程池才会初始化一个线程,初始化的线程和其他线程一样,但是在线程完成任务之后不会自行销毁,而是以挂起的状态回到线程池。当应用程序再次向现成池发出请求的时候,线程池里挂起的线程会再度激活执行任务。这样做可以减少线程创建和销毁所带来的开销

在C#中,提供了操作线程池的静态帮助类ThreadPool,并提供一些静态方法操作线程池

静态方法(常用)

方法 返回值类型 描述
ThreadPool.QueueUserWorkItem(...) bool 可以理解为开启一个线程,成功true
ThreadPool.GetMaxThreads(...) void 获取本机最大辅助,异步I/O线程数
ThreadPool.GetMinThreads(...) void 获取本机最小辅助,异步I/O线程数
ThreadPool.SetMaxThreads(...) bool 设置本机最大辅助,异步I/O线程数
ThreadPool.SetMinThreads(...) bool 设置本机最小辅助,异步I/O线程数

代码示例

示例一:开启一个线程

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(o=>
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                Tesk();
            });

            Thread.Sleep(2000);

        }

       static void Tesk()
        {
            Console.WriteLine("开启了一个线程");
        }
    }
}

示例二:开启一个线程并传入参数

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(o =>
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                Tesk(o);
            }, "libai");

            Thread.Sleep(2000);

        }

        static void Tesk(object value)
        {
            Console.WriteLine("开启了一个线程,参数:" + value);
        }
    }
}

示例三:获取线程池中辅助线程,异步 I/O 线程的最大,最小数目

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
            ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);

            Console.WriteLine($"辅助线程数,最大【{maxWorkerThreads}】,最小【{minWorkerThreads}】");
            Console.WriteLine($"异步I/O线程数,最大【{maxCompletionPortThreads}】,最小【{minCompletionPortThreads}】");
        }
}

示例四:设置线程池最大最小线程数(这个设置是全局的)

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            ThreadPool.SetMaxThreads(100, 100); // 设置最大数量时不能低于本机线程数,否则设置无效
            ThreadPool.SetMinThreads(1, 2);

            ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
            ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);

            Console.WriteLine($"辅助线程数,最大【{maxWorkerThreads}】,最小【{minWorkerThreads}】");
            Console.WriteLine($"异步I/O线程数,最大【{maxCompletionPortThreads}】,最小【{minCompletionPortThreads}】");
        }
    }
}

3. Task任务

3.1 简述说明

Task对象

Task 对象是一种的中心思想基于任务的异步模式首次引入.NET Framework 3 中,表示一个任务

System.Threading.Tasks.Task 类是Task Programming Library(TPL)中最核心的一个类

注意的是,Task里面的线程是来自线程池的

任务与线程

任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行,但是任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制

如下图所示,任务最后是托管到线程上执行的

07.并发编程Threads

3.2 创建方式

创建Task有三种方式,使用构造函数创建,使用Task.Run创建,使用 Task.Factory.StartNew 进行创建,这三种方式都是一样的,不过Task.Factory 是对Task进行管理,调度管理这一类的

方式一:使用构造函数创建

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = new Task(() =>
            {
                Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
            });

            task1.Start();
            Thread.Sleep(2000);
        }
    }
}

方式二:使用Task.Run创建

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            Task task1 = Task.Run(() =>
            {
                Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
            });

            Thread.Sleep(2000);
        }
    }
}

方式三:使用 Task.Factory.StartNew 进行创建并运行

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            // 方式一:使用静态工厂属性
            {
                Task task1 = Task.Factory.StartNew(() =>
                {
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
            }
            // 方式二:使用实例工厂
            {
                TaskFactory factory = new TaskFactory();
                Task task1 = factory.StartNew(() =>
                {
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
            }

            Thread.Sleep(2000);
        }
    }
}

重载参数补充说明

在创建Task的时候,Task有很多的构造函数的重载,一个主要的重载就是传入TaskCreateOptions的枚举

  • TaskCreateOptions.None:用默认的方式创建一个Task
  • TaskCreateOptions.PreferFairness:请求scheduler尽量公平的执行Task(优先级)
  • TaskCreateOptions.LongRunning:声明Task将会长时间的运行
  • TaskCreateOptions.AttachToParent:因为Task是可以嵌套的,所以这个枚举就是把一个子task附加到一个父task

3.3 属性,方法

实例属性(常用)

属性名 属性值类型 描述
task1.Id int 获取此Task 实例的 ID
task1.IsCompleted bool 获取此Task 是否已完成
task1.Status TaskStatus 获取此任务的任务状态
runTask.Result T 获取任务返回值

实例方法(常用)

方法名 返回值类型 描述
task1.Start(...) void 启动,开启一个线程
task1.ContinueWith(...) Task 创建一个在目标Task 完成时异步执行的延续任务
task1.Wait(...); bool 等待任务完成

静态方法(常用)

方法名 返回值类型 描述
Task.WaitAny(...) int 等待提供的任一 Task 对象完成执行过程
Task.WaitAll(...) void 等待提供的所有Task 对象完成执行过程

代码示例

示例一:WaitAnyWaitAll方法示例(结果会受计算机性能影响,建议执行多次)

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                Task task2 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                task1.Start();
                task2.Start();

                Task.WaitAny(task1, task2);

                Console.WriteLine("执行完成WaitAny");
                Thread.Sleep(2000);
            }
            Console.WriteLine("============================================");
            {
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                Task task2 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                task1.Start();
                task2.Start();

                Task.WaitAll(task1, task2);

                Console.WriteLine("执行完成WaitAll");
                Thread.Sleep(2000);
            }
        }
    }
}
using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(3000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                Task task2 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                task1.Start();
                task2.Start();

                Task.WaitAny(task1, task2);
                Console.WriteLine("有一个执行结束");
                Task.WaitAll(task1, task2);
                Console.WriteLine("全部执行结束");
            }
        }
    }
}

3.4 常用示例

1. 示例:连续任务

所谓的延续的Task就是在第一个Task完成后自动启动下一个Task,参数为上次Task的引用

示例一:单个连续任务

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                Task task1 = new Task(() =>
                {
                    Thread.Sleep(2000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });
                Task task2 = new Task(() =>
                {
                    Thread.Sleep(1000);
                    Console.Write(Thread.CurrentThread.ManagedThreadId.ToString("00"));
                });

                task1.ContinueWith(t=>
                {
                    task2.Start();
                });

                task1.Start();

                Thread.Sleep(5000);
            }
        }
    }
}

示例二:多个连续任务

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            var SendTask = Task.Factory.StartNew(() => Console.WriteLine("无参数"))
                            .ContinueWith<bool>(s => { return true; })
                            .ContinueWith<string>(r =>
                            {
                                Console.WriteLine(r.Result);
                                return "libai";
                            });
            Console.WriteLine(SendTask.Result);
        }
    }
}

2. 示例:任务结果

示例一:单个任务结果

using System;
using System.Threading.Tasks;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            // 方式一
            {
                Task<int> task = new Task<int>(()=> { return 1; });
            	task.Start();
            	Console.WriteLine(task.Result);
            }
            // 方式二
            {
                Task<int> task = Task.Run<int>(()=> { return 1; });
            	Console.WriteLine(task.Result);
            }
        }
    }
}

3.5 线程同步

所谓同步:是指在某一时刻只有一个线程可以访问变量

c#为同步访问变量提供了一个非常简单的方式,即使用c#语言的关键字Lock,它可以把一段代码定义为互斥段,互斥段在一个时刻内只允许一个线程进入执行,而其他线程必须等待

关键字Lock定义

  • expression代表你希望跟踪的对象:
    • 如果你想保护一个类的实例,一般地,你可以使用this
    • 如果你想保护一个静态变量(如互斥代码段在一个静态方法内部),一般使用类名就可
    • 如果需要锁变量,推荐使用 object类型
  • statement_block就算互斥段的代码,这段代码在一个时刻内只可能被一个线程执行
Lock(expression)
{
   statement_block
}

示例代码

namespace T1
{
    class Program
    {
        static void Main(string[] args)
        {
            BookShop book = new BookShop();
            //创建两个线程同时访问Sale方法
            Thread t1 = new Thread(new ThreadStart(book.Sale));
            Thread t2 = new Thread(new ThreadStart(book.Sale));
            //启动线程
            t1.Start();
            t2.Start();
            Console.ReadKey();
        }
    }
    class BookShop
    {
        //剩余图书数量
        public int num = 1;
        public void Sale()
        {
            //使用lock关键字解决线程同步问题
            lock (this)
            {
                int tmp = num;
                if (tmp > 0)//判断是否有书,如果有就可以卖
                {
                    Thread.Sleep(1000);
                    num -= 1;
                    Console.WriteLine("售出一本图书,还剩余{0}本", num);
                }
                else
                {
                    Console.WriteLine("没有了");
                }
            }
        }
    }
}

4. async/await

.NET 4.5版本时,C#提供AsyncAwait关键字(语法糖)来实现异步编程,两者是一组关键子,通常一起使用

Async是用来修饰方法,Await在方法内部,只能方法Async修饰的方法内或者Task前面

作用:用同步方法的思维写异步方法,提高了内存占用率

官方介绍

  • await 运算符暂停对封闭 async方法的求值,直到其操作数表示的异步操作完成

  • 异步操作完成后,await 运算符将返回操作的结果(如果有)

  • await 运算符应用到表示已完成操作的操作数时,它将立即返回操作的结果,而不会暂停封闭的方法

  • await 运算符不会阻止计算异步方法的线程

  • 若某个函数F的函数体中需要使用await关键字的函数必须以async标记

4.1 代码示例

示例一:执行顺序

using System;
using System.Threading;
using System.Threading.Tasks;

namespace th4
{
    class Program
    {
        static void Main(string[] args)
        {
            Show();
            Console.ReadKey();
        }

        static void Show()
        {
            Console.WriteLine("1\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 1.首先执行主线程
            Async();    // 2.2.调用后返回主线程
            Console.WriteLine("2\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 3.主线程执行
        }
        static async void Async()
        {
            Console.WriteLine("5\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 2.1.按照顺序执行,也是主线程
            await Task.Run(() =>    // 4.开启子线程执行,使用await等待完成
            {
                Thread.Sleep(200);
                Console.WriteLine("3\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 5.子线程执行
            });
            Console.WriteLine("4\t" + Thread.CurrentThread.ManagedThreadId.ToString("00")); // 6.等待子线程完成后执行
        }
    }
}

5. 扩展补充

5.1 为什么可以多线程

  1. 多个CPU的核可以并行工作,几核几线程,这里的线程指的是模拟核并行;多核之间叫并行

  2. CPU分片,1秒的处理能力分成1000份,操作系统调度着去响应不同的任务,从宏观角度来说,感觉就是多个任务在并行执行,从微观角度来说,一个物理CPU同一时刻只能为一个任务服务,并发:CPU分片的并发

5.2 同步异步特点

可等待

同步方法:主线程(UI线程)忙于计算,无暇其它(卡界面)

异步多线程方法:主线程闲置,计算任务交给子线程完成(不卡界面)

执行速度

同步方法:慢,因为只有一个线程计算

异步多线程方法:快,因为多个线程并发计算,CPU利用率高(密集型计算),以资源换性能

执行顺序

同步方法:有序执行

异步多线程方法:启动无序,线程资源是向操作系统申请的,由操纵系统的调度策略决定,所以启动顺序随机;同一个任务同一个线程,执行时间也不确定,CPU分片结束也无序

5.3 如何实现线程有序执行?

控制线程执行顺序有四种方法可实现

方法一:使用委托回调,回调方法是在子线程中执行

using System;

namespace th2
{
    class Program
    {
        static void Main(string[] args)
        {
            Action<string> action = Tesk1;
            action.Invoke("libai");
            Console.WriteLine("end");

            AsyncCallback callback = re =>
            {
                Console.WriteLine("end");
            };
            action.BeginInvoke("zhaoyun",callback,null);

        }
        static void Tesk1(string key)
        {
            Console.WriteLine("start:" + key);
        }
    }
}

5.4 线程/进程的区别

  • 进程间是独立的,这表现在内存空间,上下文环境;

    线程运行在进程空间内,进程包含线程(包含关系)

  • 进程是无法突破进程存取其他进程内的存储空间;

    而线程由于处于进程空间内,所以同一进程所产生的线程共享同一内存空间。

  • 同一进程中的两段代码不能够同时执行,除非引入线程

  • 线程是属于进程的,当进程退出时该进程所产生的线程都会被强制退出并清除

5.5 前台线程,后台线程

通过设置线程的IsBackground属性,设置线程为前台或后台线程,通常用在WinFrom程序

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                ThreadStart threadStart = () => Console.WriteLine("无参数无返回值委托");
                Thread thread = new Thread(threadStart);

                thread.IsBackground = true;     // 后台线程,进程结束,线程结束
                thread.IsBackground = false;    // 前台线程,进程结束,等待线程执行结束

                thread.Start();
            }
        }
    }
}

6. 应用示例

6.1 示例一:封装线程回调

需求:封装一个方法,当子线程执行结束后,调用回调方法

思路:首先肯定是需要一个子线程来执行的,其次回调方法必须是线程要执行的内容结束后调用;所以可以在其外部加一层,因为子线程是不卡主线程的,可以在子线程内部依次等待调用

using System;
using System.Threading;

namespace th3
{
    class Program
    {
        static void Main(string[] args)
        {
            {
                ThreadStart threadStart = () =>
                {
                    Console.WriteLine("开始执行");
                    Thread.Sleep(2000);
                };
                Action action = () =>
                {
                    Console.WriteLine("执行结束");
                };

                ThreadWithCallBack(threadStart, action);
            }
        }

        static void ThreadWithCallBack(ThreadStart threadStart, Action callback)
        {
            ThreadStart start = () =>
            {
                threadStart.Invoke();
                callback.Invoke();
            };
            Thread thread = new Thread(start);
            thread.Start();
        }
    }
}
上一篇:解决开发者短缺困境,也许它能帮到你


下一篇:forest-森林图:分组回归系数可视化