19.多线程和锁

19.多线程


19.1 知识点

了解线程前先了解进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。简单来说,打开一个应用程序就是在操作系统上开启了一个进程。进程之间可以相互独立运行,互不干扰,也可以相互访问、操作。

什么是线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程。我们目前写的程序都在主线程中。简单理解线程:就是代码从上到下运行的一条“管道”。

什么是多线程

我们可以通过代码开启新的线程,可以同时运行代码的多条“管道”就叫多线程。

多线程的语法相关

在类语句块内、命名空间语句块外,可以设置线程死循环的布尔标识:

// 线程死循环中的布尔标识
static bool isRuning = true;

在类语句块内、主函数外,编写新开线程的逻辑方法:

static void NewThreadLogic()
{
    Console.WriteLine("新开线程逻辑方法");

    int i = 0;

    while (isRuning)
    {
        // 新开线程执行的代码逻辑在该函数语句块中
        // isRuning是死循环中的布尔标识
        // 新线程死循环内
    }
}

主函数内

线程类Thread
需要引用命名空间 using System.Threading;

传入委托或函数创建Thread类对象申明一个新的线程

//Thread构造函数(暂时只关心这个构造函数好了)
//public Thread(ThreadStart start);

//ThreadStart实际上是一个无参无返回值的委托
//public delegate void ThreadStart();

//注意 线程执行的代码 需要封装到一个函数中
//新线程 将要执行的代码逻辑 被封装到了一个函数语句块中
//传入构造函数
Thread newThread = new Thread(NewThreadLogic);
//这样知识构造出新线程 但是没有启动

Start方法启动线程

//Thread中的Start方法
newThread.Start();

IsBackground属性设置为后台线程

//Thread中的IsBackground属性设置为true 
newThread.IsBackground = true;
//当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
//后台线程不会防止应用程序的进程被终止掉
//如果不设置为后台线程 可能导致进程无法正常关闭
//说人话:就比如主线程开了个新线程 新线程有个死循环在那里挂在 永远不会结束 进程无法正常关闭
//假如把新线程设置为后台线程 主线程结束 进程就结束 新线程也就结束了 死循环停止不起作用

修改线程运行标识或Abort方法关闭释放一个线程

//如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它 想情空把线程设置为null也行
//如果是死循环 想要中止这个线程 有两种方式

//4.1-通过死循环中bool标识 关闭释放一个线程
////等待玩家输入 让主线程一直卡着 没有结束 新线程死循环逻辑一直运行
//Console.ReadKey();
////修改新线程死循环中bool标识 这样就不会进新线程死循环逻辑了
//isRuning = false;
////等待玩家输入 让主线程一直卡着 没有结束 但是因为修改新线程死循环中bool标识 不会进线程死循环逻辑了
//Console.ReadKey();

//4.2-通过线程提供的方法(注意在.Net core版本中无法中止 会报错) 关闭释放一个线程
//Thread中的Abort方法
//在让线程=null

//防止报错 用try catch
//中止线程
//try
//{
//    newThread.Abort();
//    newThread = null;
//}
//catch
//{

//}

Sleep方法线程休眠

//Thread中的Sleep方法
//让线程休眠多少毫秒  1s = 1000毫秒
//在哪个线程里执行 就休眠哪个线程
Thread.Sleep(1000);//让主线程休眠1秒 再执行后面的逻辑

线程之间共享数据

在类语句块内、命名空间语句块外,可以声明一个用来加锁的引用类型对象:

// 随便声明的用来加锁的引用类型对象
static object lockObject = new object();

在新线程死循环内,可以使用lock关键字对共享数据进行加锁:

// 加锁的循环中,将光标位置设置为 (60, 0),输出一个黄色的正方形
lock (lockObject)
{
    Console.SetCursorPosition(60, 0);
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.Write("■");
}

在主函数内,可以通过加锁避免多线程访问共享数据时可能产生的问题:

//多个线程使用的内存是共享的,都属于该应用程序(进程)
//所以要注意 当多线程 同时操作同一片内存区域时可能会出问题
//可以通过加锁的形式避免问题

//例子:
//主线程死循环设置 光标位置 30 0 输出一个红色的圆
//新线程死循环设置 光标位置 60 0 输出一个黄色的正方形
//因为用到的都是Console相关 两个线程可能会同时访问同一块内存
//执行顺序不一定 会造成冲突 不能达到想要的效果
//比如可能会设置 光标位置 30 0  输出了一个黄色的正方形等

//解决办法:加锁
//关键字:lock
//使用语法:lock(引用类型对象)
//当我们在多个线程当中想要访问同样的东西 进行逻辑处理时
//为了避免不必要的逻辑顺序执行的差错 我们要进行加锁
//加了锁的话 系统会判断传进去的引用类型对象有没有被锁住
//假如传进去的引用类型对象被锁住了 意味着有别的地方也在访问同样的东西
//就不会执行里面的代码 等待锁释放后在执行里面的代码
//缺点:锁会影响线程执行效率

//加锁的循环中在 光标位置 30 0 输出一个红色的圆
while (true)
{
    lock (lockObject)
    {
        Console.SetCursorPosition(30, 0);
        Console.ForegroundColor = ConsoleColor.Red;
        Console.Write("●");
    }
}

多线程对于我们的意义

可以用多线程专门处理一些复杂耗时的逻辑,比如 寻路、网络通信等等

总结

多线程是多个可以同时执行代码逻辑的“管道”,可以通过代码开启多线程,用多线程处理一些复杂的可能影响主线程流畅度的逻辑。


19.2 知识点代码

using System;
using System.Threading;

namespace Lesson18_多线程
{
    class Program
    {
        //class语句块内 namespace语句块外

        #region 知识点四 多线程的语法相关

        //线程死循环中bool标识
        static bool isRuning = true;

        #endregion

        #region 知识点五 线程之间共享数据

        //随便声明的用来加锁的引用类型对象
        static object lockObject = new object();

        #endregion
        static void Main(string[] args)
        {
            Console.WriteLine("多线程");

            #region 知识点一 了解线程前先了解进程
            //进程(Process)是计算机中的程序关于某数据集合上的一次运行活动
            //是系统进行资源分配和调度的基本单位,是操作系统结构的基础
            //说人话:打开一个应用程序就是在操作系统上开启了一个进程
            //进程之间可以相互独立运行,互不干扰
            //进程之间也可以相互访问、操作
            #endregion

            #region 知识点二 什么是线程

            //操作系统能够进行运算调度的最小单位。
            //它被包含在进程之中,是进程中的实际运作单位
            //一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
            //我们目前写的程序 都在主线程中
            //简单理解线程:就是代码从上到下运行的一条“管道”

            #endregion

            #region 知识点三 什么是多线程
            //我们可以通过代码 开启新的线程
            //可以同时运行代码的多条“管道” 就叫多线程
            #endregion

            //主函数内

            #region 知识点四 多线程的语法相关

            //线程类 Thread
            //需要引用命名空间 using System.Threading;

            //1.申明一个新的线程 

            //Thread构造函数(暂时只关心这个构造函数好了)
            //public Thread(ThreadStart start);

            //ThreadStart实际上是一个无参无返回值的委托
            //public delegate void ThreadStart();

            //注意 线程执行的代码 需要封装到一个函数中
            //新线程 将要执行的代码逻辑 被封装到了一个函数语句块中
            //传入构造函数
            Thread newThread = new Thread(NewThreadLogic);
            //这样知识构造出新线程 但是没有启动

            //2.启动线程
            //Thread中的Start方法
            newThread.Start();

            //3.设置为后台线程
            //Thread中的IsBackground属性设置为true 
            newThread.IsBackground = true;
            //当前台线程都结束了的时候,整个程序也就结束了,即使还有后台线程正在运行
            //后台线程不会防止应用程序的进程被终止掉
            //如果不设置为后台线程 可能导致进程无法正常关闭
            //说人话:就比如主线程开了个新线程 新线程有个死循环在那里挂在 永远不会结束 进程无法正常关闭
            //假如把新线程设置为后台线程 主线程结束 进程就结束 新线程也就结束了 死循环停止不起作用

            //4.关闭释放一个线程
            //如果开启的线程中不是死循环 是能够结束的逻辑 那么 不用刻意的去关闭它 想情空把线程设置为null也行
            //如果是死循环 想要中止这个线程 有两种方式

            //4.1-通过死循环中bool标识 关闭释放一个线程
            ////等待玩家输入 让主线程一直卡着 没有结束 新线程死循环逻辑一直运行
            //Console.ReadKey();
            ////修改新线程死循环中bool标识 这样就不会进新线程死循环逻辑了
            //isRuning = false;
            ////等待玩家输入 让主线程一直卡着 没有结束 但是因为修改新线程死循环中bool标识 不会进线程死循环逻辑了
            //Console.ReadKey();

            //4.2-通过线程提供的方法(注意在.Net core版本中无法中止 会报错) 关闭释放一个线程
            //Thread中的Abort方法
            //在让线程=null

            //防止报错 用try catch
            //中止线程
            //try
            //{
            //    newThread.Abort();
            //    newThread = null;
            //}
            //catch
            //{

            //}

            //5.线程休眠
            //Thread中的Sleep方法
            //让线程休眠多少毫秒  1s = 1000毫秒
            //在哪个线程里执行 就休眠哪个线程
            Thread.Sleep(1000);//让主线程休眠1秒 再执行后面的逻辑

            #endregion

            #region 知识点五 线程之间共享数据
            //多个线程使用的内存是共享的,都属于该应用程序(进程)
            //所以要注意 当多线程 同时操作同一片内存区域时可能会出问题
            //可以通过加锁的形式避免问题

            //例子:
            //主线程死循环设置 光标位置 30 0 输出一个红色的圆
            //新线程死循环设置 光标位置 60 0 输出一个黄色的正方形
            //因为用到的都是Console相关 两个线程可能会同时访问同一块内存
            //执行顺序不一定 会造成冲突 不能达到想要的效果
            //比如可能会设置 光标位置 30 0  输出了一个黄色的正方形等

            //解决办法:加锁
            //关键字:lock
            //使用语法:lock(引用类型对象)
            //当我们在多个线程当中想要访问同样的东西 进行逻辑处理时
            //为了避免不必要的逻辑顺序执行的差错 我们要进行加锁
            //加了锁的话 系统会判断传进去的引用类型对象有没有被锁住
            //假如传进去的引用类型对象被锁住了 意味着有别的地方也在访问同样的东西
            //就不会执行里面的代码 等待锁释放后在执行里面的代码
            //缺点:锁会影响线程执行效率

            //加锁的循环中在 光标位置 30 0 输出一个红色的圆
            while (true)
            {
                lock (lockObject)
                {
                    Console.SetCursorPosition(30, 0);
                    Console.ForegroundColor = ConsoleColor.Red;
                    Console.Write("●");
                }
            }
            #endregion

            #region 知识点六 多线程对于我们的意义
            //可以用多线程专门处理一些复杂耗时的逻辑
            //比如 寻路、网络通信等等
            #endregion
        }

        //class语句块内 主函数外

        #region 知识点四 多线程的语法相关

        //新开线程逻辑方法
        static void NewThreadLogic()
        {
            Console.WriteLine("新开线程逻辑方法");

            int i = 0;

            //新开线程 执行的代码逻辑 在该函数语句块中
            //isRuning是死循环中bool标识
            while (isRuning)
            {
                //Thread.Sleep(1000);//让新线程休眠1秒 再执行后面的逻辑
                //Console.WriteLine("新开线程代码逻辑{0}", i);
                //i++;

                //新线程死循环内

                #region 知识点五 线程之间共享数据

                //加锁的循环中在 光标位置 60 0 输出一个黄色的正方形
                lock (lockObject)
                {
                    Console.SetCursorPosition(60, 0);
                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.Write("■");
                }

                #endregion
            }
        }

        #endregion
    }

    //总结
    //多线程是多个可以同时执行代码逻辑的“管道”
    //可以通过代码开启多线程,用多线程处理一些复杂的可能影响主线程流畅度的逻辑
    //关键字 Thread
}

19.3 练习题

控制台中有一个■,它会如贪食蛇一样自动移动,请开启一个多线程来检测输入,控制它的转向

class语句块内 namespace语句块外

// 方向枚举
enum E_MoveDir
{
    Up,
    Down,
    Right,
    Left,
}

// 图标类
class Icon
{
    // 当前移动的方向
    public E_MoveDir dir;

    // 当前位置
    public int x;
    public int y;

    // 构造函数 初始化位置和方向
    public Icon(int x, int y, E_MoveDir dir)
    {
        this.x = x;
        this.y = y;
        this.dir = dir;
    }

    // 移动
    public void Move()
    {
        switch (dir)
        {
            case E_MoveDir.Up:
                y -= 1;
                break;
            case E_MoveDir.Down:
                y += 1;
                break;
            case E_MoveDir.Right:
                x += 2;
                break;
            case E_MoveDir.Left:
                x -= 2;
                break;
        }
    }

    // 绘制
    public void Draw()
    {
        Console.SetCursorPosition(x, y);
        Console.Write("■");
    }

    // 擦除
    public void Clear()
    {
        Console.SetCursorPosition(x, y);
        Console.Write("  ");
    }

    // 转向
    public void ChangeDir(E_MoveDir dir)
    {
        this.dir = dir;
    }
}

class语句块内 主函数外

// 图标的静态成员变量
static Icon icon;

// 新线程逻辑
static void NewThreadLogic()
{
    // 检测输入 修改方向
    while (true)
    {
        switch(Console.ReadKey(true).Key)
        {
            case ConsoleKey.W:
                icon.ChangeDir(E_MoveDir.Up);
                break;
            case ConsoleKey.A:
                icon.ChangeDir(E_MoveDir.Left);
                break;
            case ConsoleKey.S:
                icon.ChangeDir(E_MoveDir.Down);
                break;
            case ConsoleKey.D:
                icon.ChangeDir(E_MoveDir.Right);
                break;
        }
    }
}

主函数内

// 光标不显示
Console.CursorVisible = false;

// 构造一个图标 
icon = new Icon(10, 5, E_MoveDir.Right);
icon.Draw();

// 开启多线程 这个线程主要用来检测输入
Thread newThread = new Thread(NewThreadLogic);
newThread.IsBackground = true;
newThread.Start();
   
// 死循环不停的擦除移动绘制图标
while (true)
{
    Thread.Sleep(500); // 每次休眠个0.5秒 再擦除移动绘制图标
    icon.Clear();
    icon.Move();
    icon.Draw();
}

19.4 练习题代码

using System;
using System.Threading;

namespace Lesson18_练习题
{
    //class语句块内 namespace语句块外

    #region 练习题一

    //方向枚举
    enum E_MoveDir
    {
        Up,
        Down,
        Right,
        Left,
    }

    //图标类
    class Icon
    {
        //当前移动的方向
        public E_MoveDir dir;

        //当前位置
        public int x;
        public int y;

        //构造函数 初始化位置和方向
        public Icon(int x, int y, E_MoveDir dir)
        {
            this.x = x;
            this.y = y;
            this.dir = dir;
        }

        //移动
        public void Move()
        {
            switch (dir)
            {
                case E_MoveDir.Up:
                    y -= 1;
                    break;
                case E_MoveDir.Down:
                    y += 1;
                    break;
                case E_MoveDir.Right:
                    x += 2;
                    break;
                case E_MoveDir.Left:
                    x -= 2;
                    break;
            }
        }

        //绘制
        public void Draw()
        {
            Console.SetCursorPosition(x, y);
            Console.Write("■");
        }

        //擦除
        public void Clear()
        {
            Console.SetCursorPosition(x, y);
            Console.Write("  ");
        }

        //转向
        public void ChangeDir(E_MoveDir dir)
        {
            this.dir = dir;
        }
    }

    #endregion


    class Program
    {

        static void Main(string[] args)
        {
            Console.WriteLine("多线程练习题");

            //主函数内

            #region 练习题一
            //控制台中有一个■
            //它会如贪食蛇一样自动移动
            //请开启一个多线程来检测输入,控制它的转向

            //光标不显示
            Console.CursorVisible = false;

            //构造一个图标 
            icon = new Icon(10, 5, E_MoveDir.Right);
            icon.Draw();

            //开启多线程 这个线程主要用来检测输入
            Thread newThread = new Thread(NewThreadLogic);
            newThread.IsBackground = true;
            newThread.Start();

            //死循环不停的擦除移动绘制图标
            while (true)
            {
                Thread.Sleep(500);//每次休眠个0.5秒 再擦除移动绘制图标
                icon.Clear();
                icon.Move();
                icon.Draw();
            }


            #endregion
        }

        //class语句块内 主函数外

        #region 练习题一

        //图标的静态成员变量
        static Icon icon;

        //新线程逻辑
        static void NewThreadLogic()
        {
            //检测输入 修改方向
            while (true)
            {
                switch (Console.ReadKey(true).Key)
                {
                    case ConsoleKey.W:
                        icon.ChangeDir(E_MoveDir.Up);
                        break;
                    case ConsoleKey.A:
                        icon.ChangeDir(E_MoveDir.Left);
                        break;
                    case ConsoleKey.S:
                        icon.ChangeDir(E_MoveDir.Down);
                        break;
                    case ConsoleKey.D:
                        icon.ChangeDir(E_MoveDir.Right);
                        break;
                }
            }
        }

        #endregion
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏