异步并发编程

老是忘记,这里做一下笔记

线程


 创建一个线程来执行一段代码

class Program
    {
        static void Main(string[] args)
        {
            Thread thread = new Thread(Run1);
            thread.Start();  
        }
        static void Run1()
        {           
            Console.WriteLine("子线程Child"+DateTime.Now);
        }
    }

创建一个新的线程执行一段代码,这个线程和主线程线程是并行执行的,并不会等待主线程执行完再执行。

class Program
    {
        static void Main(string[] args)
        {
            // 两个线程是并行执行的
            Thread thread = new Thread(Run1);
            thread.Start();
            while (true)
            {
                Console.WriteLine("主线程中Main"+DateTime.Now);
            }
        }
        static void Run1()
        {
            while (true)
            {
                Console.WriteLine("子线程Child"+DateTime.Now);
            }
        }
    }

 

 参数化线程


 创建一个线程执行一个方法,给这个方法传值输出,m的值在启用线程之后再次赋值

  static void Main(string[] args)
        {
             int m = 5;
             Thread thread = new Thread(() =>
             {
                 Console.WriteLine("m="+m);
             });
             thread.Start();
             m = 6;
            Console.ReadKey();
        }

看输入结果是6,这是因为Main函数是由主线程执行的,新创建的thread函数虽然start了,但是合适执行是由操作系统决定,这时候m的值已经赋值成6了之后才执行thread线程,所以输出6

 再看一个例子,循环创建10个线程执行输出当前i

 static void Main(string[] args)
        { 
             for (int i = 0; i < 10; i++)
             {
                 Thread thread1 = new Thread(()=> {
                     Console.WriteLine(i);
                 });

                 thread1.Start();
             }          
            Console.ReadKey();
        }

可以看到执行结果并不是按照顺序输出,这是因为创建的线程并不是按照每次循环都会执行,或许循环到5的时候线程执行了两次

 所以不建议用以上方法向线程中传参,在线程中传递参数使用ParameterizedThreadStart,避免混乱。

上面的写法改成如下

static void Main(string[] args)
        {
            
            int m = 5;
            Thread thread1 = new Thread((obj) => {
                Console.WriteLine(obj);
            });
            thread1.Start(m);
            m = 6;
            Console.ReadKey();


            for (int i = 0; i < 10; i++)
            {
                Thread thread2 = new Thread((item)=> 
                {
                    Console.WriteLine("i="+item);
                });

                thread2.Start(i);
            }
            Console.ReadKey();                 
        }

 

后台线程


 创建一个Winform程序,点击按钮执行一段代码

 private void button1_Click(object sender, EventArgs e)
        {
            // 默认开启的线程都是前台线程,线程没有执行完毕,关闭程序是不会退出的,因为主线程没有关闭
            for (int i = 0; i < 100; i++)
            {
                Thread thread = new Thread((item) =>
                {
                    //textBox1.Text = item.ToString();
                    Thread.Sleep(3000);
                    MessageBox.Show(item.ToString());
                });
                thread.Start(i);
            }
        }

启动程序,然后关系Winform程序,会发现程序并不会立即退出,而是等待执行完成才会退出,这是因为,我们创建的线程默认都是前台线程,没有执行完代码程序不退出,

因此;我们只需要把创建的线程设置成后台线程即可,这样关系程序,即使没有执行完,也会立即关闭。

  private void button1_Click(object sender, EventArgs e)
        {           
            // 设置后台线程执行   秒关
            for (int i = 0; i < 100; i++)
            {
                Thread thread = new Thread((item) =>
                {
                    //textBox1.Text = item.ToString();
                    Thread.Sleep(3000);
                    MessageBox.Show(item.ToString());
                });

                thread.IsBackground = true;
                thread.Start(i);
            }
        }

 

Thread.Sleep()


 创建一个线程,先执行一句输出,睡3秒钟再执行,睡的是当前线程所在的代码

static void Main(string[] args)
        {
            Thread thread = new Thread(()=> 
            {
                Console.WriteLine("线程1我要睡了");
                Thread.Sleep(3000);
                Console.WriteLine("线程1我睡醒了");
            });

            thread.Start();
            Console.ReadKey();
        }

 

线程的优先级


 创建两个线程,可以设置他们执行的优先级,

  t1.Priority = ThreadPriority.Highest; // 优先执行
  t2.Priority = ThreadPriority.Lowest;  // 最后执行
 // 设置优先级后再查看结果
            int i = 0, j = 0;
            Thread t1 = new Thread(() => {
                while (true)
                {
                    i++;
                }
            });
            t1.Priority = ThreadPriority.Highest; // 优先执行
            t1.Start();

            Thread t2 = new Thread(() =>
            {
                while (true)
                {
                    j++;
                }
            });
            t2.Priority = ThreadPriority.Lowest;
            t2.Start();
            Thread.Sleep(3000);
            Console.WriteLine("i=" + i + ",j=" + j);
            Console.ReadKey();

 

线程同步


创建2个线程t1和t2,同时对一个静态全局变量counter进行加1000 的操作,最后输出counter的值;

while (t1.IsAlive) ;操作会大量消耗 cpu 空转, 可以改成 t1.Join()就是让当前线程等待 t1 线程的结束

class Program
    {
        private static int counter = 0;
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    counter++;
                    Thread.Sleep(1);
                }
            });

            t1.Start();
            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    counter++;
                    Thread.Sleep(1);
                }
            });

            t2.Start();
            //while (t1.IsAlive) { } //如果线程存活就一直循环
            //while (t2.IsAlive) { }

            t1.Join(); // 让当前线程等待t1线程执行结束
            t2.Join();
            Console.WriteLine(counter);
            Console.ReadKey();

        }
    }

可以看到结果并不是2000,这是因为线程同步问题引起的,当t1线程执行在500 的时候,它要加1,正好t1也执行到500,对counter加了1,这时候counter的值相当于重置成500再加1

 使用lock锁来解决

 

 Lock锁---线程同步问题的解决


上面例子问题造成的原因就是因为在同一时刻也有两个线程来访问同一资源,解决以上问题,对要修改的资源加上lock锁锁住即可。注意 lock 要锁定同一个对象,而且必须是引用类型的对象

class Program
    {
        private static int counter = 0;
        private static Object locker = new object();
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker) //注意 lock 要锁定同一个对象,而且必须是引用类型的对象
                    {
                        counter++;
                    }
                    Thread.Sleep(1);
                }
            });

            t1.Start();

            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    lock (locker)
                    {
                        counter++;
                    }
                    Thread.Sleep(1);
                }
            });

            t2.Start();
            //while (t1.IsAlive) { }
            //while (t2.IsAlive) { }

            t1.Join(); // 让当前线程等待t1线程执行结束
            t2.Join();
            Console.WriteLine(counter);
            Console.ReadKey();
        }
    }

 

 

 Abort--线程终止


 创建一个线程执行一段耗时的操作,如果想终止可以使用t1..Abort();方法终,但是会引发一个异常,可有用catch捕获处理,参数是ThreadAbortException类型

static void Main(string[] args)
        {

            for (int i = 0; i < 40; i++)
            {
                try
                {
                    Thread thread = new Thread((item) =>
                    {
                        Console.WriteLine(item);

                    });
                    thread.Abort(); // 会引发异常
                    thread.Start(i);
                }

                catch (ThreadAbortException ex)
                {
                    Console.WriteLine(ex);
                }
            }
        }

 

Interrupt--提前唤醒线程


一个正在线程sleep中,使用Interrupt以可提前唤醒线程,会引发一个异常

static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                Console.WriteLine("t1要睡了");
                try
                {
                    Thread.Sleep(5000);
                }
                catch (ThreadInterruptedException)
                {
                    Console.WriteLine("擦,叫醒我干啥");
                }
                Console.WriteLine("t1醒了");
            });
            t1.Start();
            Thread.Sleep(1000);
            t1.Interrupt();
            Console.ReadKey();
        }

 

Join--等待某个线程执行结束


t2线程等待t1线程执行结束后才开始执行

 static void Main(string[] args)
        {
            Thread t1 = new Thread(() => {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("t1 " + i);
                }
            });
            t1.Start();
            Thread t2 = new Thread(() => {
                t1.Join();//等着 t1 执行结束
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine("t2 " + i);
                }
            });
            t2.Start();
            Console.ReadKey();
        }

 

线程同步的三种解决办法


先看下面的代码,有一个静态变量money=10000,有一个方法取钱,让这个变量减一,创建两个线程都来取钱1000次,按正常逻辑,执行之后因该还剩8000

    /// <summary>
    /// 会有两个线程同时修改一个余额的情况
    /// </summary>
    class Program
    {
        static int money = 10000;
        static void QuQian(string name)
        {
            Console.WriteLine(name + "查看一下余额" + money);
            int yue = money - 1;
            Console.WriteLine(name + "取钱");
            money = yue;
            Console.WriteLine(name + "取完了,剩" + money);
        }
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t1");
                }
            });
            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + money);
            Console.ReadKey();
        }
    }

运行程序,发现结果并不是8000,还是线程同步问题,两个线程在同一时间修改了money变量

解决方法就是使用同步的技术避免两个线程同时修改一个变量,有以下三种方式

解决方法1:最大粒度——同步方法


QuQian 方法上标注[MethodImpl(MethodImplOptions.Synchronized)], 这样一个方法只能同时被一个线程访问。

class Program
    {
        static int money = 10000;      
        [MethodImpl(MethodImplOptions.Synchronized)]
        static void QuQian(string name)
        {
                Console.WriteLine(name + "查看一下余额" + money);
                int yue = money - 1;
                Console.WriteLine(name + "取钱");
                money = yue;
                Console.WriteLine(name + "取完了,剩" + money);          
        }
   
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t1");
                }
            });
            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + money);
            Console.ReadKey();
        }
    }

解决方法 2:对象互斥锁


创建一个对象,对修改money变量的操作加锁,同一时刻只能有一个线程进入同一个对象的 lock 代码块。必须是同一个对象才能起到互斥的作用。 lock 后必须是引用类型,不一定是 object,只要是对象就行。 

锁对象选择很重要,选不对起不到同步的作用;选不对可能会造成其他地方被锁,比如用字符串做锁(因为字符串拘留池导致可能用的是其他地方也在用的锁)两个方法如果都用一个对象做锁,

那么访问A的时候就不能访问B,因此锁选择很重要 (整理来自网络,侵删)。

 class Program
    {
        static int money = 10000;
        static object locker = new object();
        static void QuQian(string name)
        {
            lock (locker)
            {           
                Console.WriteLine(name + "查看一下余额" + money);
                int yue = money - 1;
                Console.WriteLine(name + "取钱");
                money = yue;//故意这样写,写成 money--其实就没问题
                Console.WriteLine(name + "取完了,剩" + money);
            }
        }
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t1");
                }
            });
            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + money);
            Console.ReadKey();
        }
    }

解决方法 3Monitor


其实 lock 关键字就是对 Monitor 的简化调用, lock 最终就编译成 Monitor,因此一般不不直接用 Monitor

class Program
    {
        static int money = 10000;
        
        static object locker = new object();
        static void QuQian(string name)
        {
          
            Monitor.Enter(locker);//等待没有人锁定 locker 对象,我就锁定它,然后继续执行
                                  //Monitor 有 TryEnter 方法,如果 Enter 的时候有人在占用锁,它不会等待,而是会返回false
            try
            {
                Console.WriteLine(name + "查看一下余额" + money);
                int yue = money - 1;
                Console.WriteLine(name + "取钱");
                money = yue;
                Console.WriteLine(name + "取完了,剩" + money);
            }
            finally
            {
                Monitor.Exit(locker);//释放 locker 对象的锁
            }          
        }
      
        static void Main(string[] args)
        {
            Thread t1 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t1");
                }
            });
            Thread t2 = new Thread(() =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    QuQian("t2");
                }
            });
            t1.Start();
            t2.Start();
            t1.Join();
            t2.Join();
            Console.WriteLine("余额" + money);
            Console.ReadKey();
        }
    }

三种方法执行的结果都是一样的正确的

 

 Monitor TryEnter 方法,如果 Enter 的时候有人在占用锁,它不会等待,而是会返回false

 static void F1(int i)
        {
            if (!Monitor.TryEnter(locker))
            {
                Console.WriteLine("有人在锁着呢");
                return;
            }
            Console.WriteLine(i);
            Monitor.Exit(locker);
        }

 

单例模式,懒汉模式


 单例模式就是让个程序的运行的所需要的对象只创建一个,构造函数私有,只在调用类的方法时创建返回实例

 class Person
    {
        private static Person instance = new Person();
        private Person()
        { }
        public static Person GetInstance()
        {
            return instance;
        }
    }

有时候需要真正第一次用到的时候再创建那个唯一实例(懒汉模式)。

 class User
    {
        private static User instance = null;

        private User() { }

        public static User GetInstance1()
        {
            if (instance == null)
            {
                return new User();
            }
            return instance;
        }
    }

这样写在多线程的环境下可能无法保证单例。用 lock 可以保证

 class Dog
    {
        private static Dog instance = null;
        private static object locker = new object();
        private Dog()
        { }
        public static Dog GetInstance()
        {
            lock (locker)
            {
                if (instance == null)
                {
                    instance = new Dog();
                }
                return instance;
            }
        }
    }

但是每次其实只有instancenull的时候的那次加锁时候有意义的,以后的千万次调用,每个线程都要锁定 locker,就会造成性能下降。 如下改造,进行双重检查(double-check

class Cat
{
    private static Cat instance = null;
    private static object locker = new object();
    private Cat()
    { }
    public static Cat GetInstance()
    {
        if (instance == null)
        {
            lock (locker)
            {
                if (instance == null)
                {
                    instance = new Cat();
                }
            }
        }
        return instance;
    }
}

 

WaitHandle


 除了锁之外, .Net 中还提供了一些线程间更自由通讯的工具,他们提供了通过“信号”进行通讯的机制,

通俗的比喻为“开门”、“关门”: Set()开门, Reset()关门, WaitOne()等着门

 static void Main(string[] args)
        {
             ManualResetEvent mre = new ManualResetEvent(false);
             //构造函数 false 表示“初始状态为关门”,设置为 true 则初始化为开门状态
             Thread t1 = new Thread(() => {
                 Console.WriteLine("开始等着开门");
                 mre.WaitOne();
                 Console.WriteLine("终于等到你");
             });
             t1.Start();
             Console.WriteLine("按任意键开门");
             Console.ReadKey();
             mre.Set();//开门
             Console.ReadKey();
        }

WaitOne()还可以设置等待超时时间:

  static void Main(string[] args)
        {
                       
            ManualResetEvent mre = new ManualResetEvent(false);
            //false 表示“初始状态为关门”
            Thread t1 = new Thread(() =>
            {
                Console.WriteLine("开始等着开门");
                if (mre.WaitOne(5000))  //WaitOne()还可以设置等待超时时间:
                {
                    Console.WriteLine("终于等到你");
                }
                else
                {
                    Console.WriteLine("等了 5 秒钟都没等到");
                }
            });
            t1.Start();
            Console.WriteLine("按任意键开门");
            Console.ReadKey();
            mre.Set();//开门
            Console.ReadKey();
        }
    }

ManualResetEvent 是一旦设定 Set()后就一直开门,除非调用 Reset 关门。 Manual:手动;Reset:关门。

 static void Main(string[] args)
        {

            ManualResetEvent mre = new ManualResetEvent(false);
            //false 表示“初始状态为关门”
            Thread t1 = new Thread(() => {
                while (true)
                {
                    Console.WriteLine("开始等着开门");
                    mre.WaitOne(5000);
                    Console.WriteLine("终于等到你");
                }
            });
            t1.Start();
            Console.WriteLine("按任意键开门");
            Console.ReadKey();
            mre.Set();//开门
            Console.ReadKey();
            mre.Reset();//关门
            Console.ReadKey();
        }

还有一个类 AutoResetEvent, 他是在开门并且一个 WaitOne 通过后自动关门, 因此命名为“AutoResetEvent”(Auto 自动-Reset 关门)

static void Main(string[] args)
        {
            
            AutoResetEvent are = new AutoResetEvent(false);
            Thread t1 = new Thread(() => {
                while (true)
                {
                    Console.WriteLine("开始等着开门");
                    are.WaitOne();
                    Console.WriteLine("终于等到你");
                }
            });
            t1.Start();
            Console.WriteLine("按任意键开门");
            Console.ReadKey();
            are.Set();//开门
            Console.WriteLine("按任意键开门");
            Console.ReadKey();
            are.Set();
            Console.WriteLine("按任意键开门");
            Console.ReadKey();
            are.Set();
            Console.ReadKey();
        }

ManualResetEvent 就是学校的大门, 开门大家都可以进, 除非主动关门; AutoResetEvent
就是火车地铁的闸机口, 过了一个后自动关门。

 

线程池


1、 线程池:因为每次创建线程、销毁线程都比较消耗 cpu 资源,因此可以通过线程池进行优化。线程池是一组已经创建好的线程,随用随取,用完了不是销毁线程,然后放到线程池中,供其他人用。
2、 用线程池之后就无法对线程进行精细化的控制了(线程启停、优先级控制等)
3ThreadPool 类的一个重要方法:
      static bool QueueUserWorkItem(WaitCallback callBack)
      static bool QueueUserWorkItem(WaitCallback callBack, object state)
      第二个重载是用来传递一个参数给线程代码的。
4、除非要对线程进行精细化的控制,否则建议使用线程池,因为又简单、性能调优又更好。

使用线程池的的线程执行一段代码

  static void Main(string[] args)
        {
            ThreadPool.QueueUserWorkItem(s =>
            {
                for (int i = 0; i < 100; i++)
                {
                    Console.WriteLine(i);
                }
            });

            for (int i = 0; i < 100; i++)
            {
                Console.WriteLine("Hi");
            }

            Console.ReadKey();
        }

线程池传参数

 static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                ThreadPool.QueueUserWorkItem(s =>
                {
                    Console.WriteLine(s);
                    int workThreads, completportThreads;
                    ThreadPool.GetAvailableThreads(out workThreads, out completportThreads);
                    Console.WriteLine($"workThreads={workThreads},completportThreads={completportThreads}");
                }, i);
            }
            Console.ReadKey();
        }

 

Winfom中的多线程--读取文件介面卡死和跨线程访问


 我们点击一个按钮从网上下载一个网页,在下载的过程中介面会卡死,知道下载完成才会不卡

 static void Main(string[] args)
        {
            for (int i = 0; i < 100; i++)
            {
                ThreadPool.QueueUserWorkItem(s =>
                {
                    Console.WriteLine(s);
                    int workThreads, completportThreads;
                    ThreadPool.GetAvailableThreads(out workThreads, out completportThreads);
                    Console.WriteLine($"workThreads={workThreads},completportThreads={completportThreads}");
                }, i);
            }
            Console.ReadKey();
        }

造成这种结果的原因是因为Winfom程序在执行的时候是它有一个UI线程也是主线程,到遇到下载文件的代码时,UI线程就会去执行下载的代码,这时候拖动界面就会卡顿,解决方法就是开一个新的线程执行这段下载的代码,如果要将下载的文本赋值给TextBox,会引起跨线程访问的错误,对于控件的操作只能在UI线程中启用,正确的做法应该是:  textBox1.BeginInvoke

 

使用 WebClient 获取一个网页然后显示到 WinForm 中,界面会卡。因为网络操作阻塞了主线程。因此我们应该把下载的过程放到单独的线程中:

 private void button1_Click(object sender, EventArgs e)
        {
            /* WebClient webClient = new WebClient();
            string html = webClient.DownloadString("http://www.github.com");
            textBox1.Text = html; */

            ThreadPool.QueueUserWorkItem(s => 
            {
                WebClient client = new WebClient();
                string html= client.DownloadString("http://www.github.com");
                // TextBox.CheckForIllegalCrossThreadCalls = false; //取消跨线程访问
                // textBox1.Text = html; // 对于控件的操作只能在UI线程中执行
                textBox1.BeginInvoke(new Action(()=> 
                {
                    textBox1.Text = html;
                }));
            });
        }

这样写的话,会在 textBox1.Text = s;报异常, 因为不能在其他线程中直接访问 UI 控件。 需要把对 UI控件的访问代码放到 BeginInvoke()中, BeginInvoke 的委托中的代码是运行在 UI 线程中的

但是也不能因此把 DownloadString 等也放到 BeginInvoke 中, 否则又会界面卡死了。在任意一个 UI 控件上都可以调用 BeginInvoke 方法

 

posted @ 2019-12-08 15:58  Jenkin_Tong  阅读(307)  评论(0编辑  收藏  举报