C#多线程的使用与安全问题的学习

C#多线程学习

1 使用委托创建线程

     我们先使用委托来创建一个线程,关于委托入门学习可以看我的另一篇文章。
     第一步是创建一个无返回值的方法

 public static void DoSomething(string name)
{
   Console.WriteLine("DoSomething begin {0} {1} {2}", name, Thread.CurrentThread.ManagedThreadId.ToString("00"),
   DateTime.Now.ToString("HHmmss:fff"));
    long result = 0;
   for (int i = 0; i < 10000000; i++)
   	result += i;

   Thread.Sleep(2000);

   Console.WriteLine("DoSomething end {0} {1} {2}", name, Thread.CurrentThread.ManagedThreadId.ToString("00"),
   DateTime.Now.ToString("HHmmss:fff"));
}

记得对Thead的引用

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

然后使用Action委托来指向这个方法。

Action<string> action = new Action<string>(DoThread.DoSomething); 

再声明一个多线程的回调方法。

//声明异步调用的回调函数 AsyncState为在BeginInvoke时第三个参数
AsyncCallback callback = new AsyncCallback(ary =>
{
     Console.WriteLine(ary.AsyncState + "计算完成" + Thread.CurrentThread.ManagedThreadId.ToString("00"));
 });

接下来我们就可以用BeginInvoke来启动这个一个线程来执行这个委托

//异步调用   参数为 给委托的实际参数 回调函数  给回调函数使用的内容
IAsyncResult res = action.BeginInvoke("123", callback, "ABC");

在这里插入图片描述
     其中BeginInvoke()的第三个参数是用来给回调函数的参数的AsyncState方法使用。
接下来系统就会启动一个新线程来执行委托方法,主线程可以去执行其他任务,如果希望阻塞主线程来等替代这个新线程,可以使用IAsyncResult的WaitOne()方法。
res.AsyncWaitHandle.WaitOne(2000);

上面我们使用的是无返回值的委托,接下来使用Func<>来指向有返回值的委托。

 Func<int> func = new Func<int>(() =>
            {
                Thread.Sleep(2000);
                return DateTime.Now.Year;
            });
IAsyncResult iarA = func.BeginInvoke(ar =>
{
     int funcRes = func.EndInvoke(ar);
     Console.WriteLine("func委托运行结果" + funcRes.ToString());
}, null);

我们可以看到, 使用委托的EndInvoke()可以获取到委托运行的结果。
在这里插入图片描述

2 使用TaskFactory创建多线程

     另一个更加方便的方法就是使用TaskFactory来创建多线程,具体就是使用TaskFactory的StartNew()来创建并启动一个线程。

List<Task> taskList = new List<Task>();
//接下来开始多线程执行
TaskFactory factory = new TaskFactory();
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("A"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("B"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("C"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("D"); }));
taskList.Add(factory.StartNew(() => { DoThread.DoSomething("E"); }));

在这里插入图片描述

3 线程的顺序控制

接下来介绍几个用来控制线程流程的方法。
如果想让主线程等待所有其他线程结束后再执行其它任务,可以使用WaitAll(),若是只等其中的一个可以用WaitAny()

 //waitAll是说有线程都结束才继续执行
//WaitAny是有一个线程结束主线程就开始继续执行
Task.WaitAll(taskList.ToArray());

     这样做的缺点是主线程会卡住,我们可以新创建一个线程来等待之前的那些线程。其中ContinueWhenAll是在所有线程都结束后执行回调函数,而ContinueWhenAny是只要有一个线程完成后就会执行回调函数。

//将这个新线程也加入到集合中  Task.WaitAll就会等待它结束
taskList.Add(factory.ContinueWhenAll(taskList.ToArray(), ask => {    Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + "  Continue完成"); }));

     如果在启动线程时,希望传递一个参数给回调函数,那可以将这个参数值作为StartNew()的参数。其中StartNew()的第一个参数为Action,在这个Action中调用该线程要执行的函数。

 taskList.Add(taskFactory.StartNew(o => DoSonmeThingLong(para),  para));

     然后在回调函数中,通过iar的AsyncState属性来获取刚才的参数。

 //创建了一个新的线程去等待其它线程
taskList.Add(taskFactory.ContinueWhenAny(taskList.ToArray(), ar =>
{
      Console.WriteLine("至少有一个线程已结束 " + ar.AsyncState);
}));

    &nbsp通过下来的例子我们来看一下ContinueWhenAny和ContinueWhenAll的效果,他们是非阻塞式的。

for (int i = 0; i < 5; i++)
            {
                int j = i + 1;
                string para = "任务" + j.ToString();

                //第一个参数为一个无参数的Action 所以要通过一个无参的action去调用DoSonmeThingLong
                taskList.Add(taskFactory.StartNew(o => DoSonmeThingLong(para), para));
                // taskList.Add(taskFactory.StartNew(() => { DoSonmeThingLong("任务" + j.ToString()); }));
            }
                     
            //创建了一个新的线程去等待其它线程
            taskList.Add(taskFactory.ContinueWhenAny(taskList.ToArray(), ar =>
            {
                Console.WriteLine("至少有一个线程已结束 " + ar.AsyncState);
            }));

            //等待所有线程都完成
            taskFactory.ContinueWhenAll(taskList.ToArray(), ar =>
             {
                 Console.WriteLine("所有线程都已结束\n");
             });
            Console.WriteLine("主线程结束 {0}", Thread.CurrentThread.ManagedThreadId.ToString("00"));
        }

4 控制线程数量

     假如当前有1000个任务可以并发地去执行,那我们就可以用多线程去执行他们,但是这样系统可能就会创建非常多的线程去执行它们,这样就会占用大量的系统资源,所以我们需要控制线程的数量。
     具体的做法就是,在当前线程的数量已到达我们的预设值事,就不创建新线程了,除非已有的线程完成一个后,才会再创建一个线程。这样就保证了线程的总数量不会超过我们的预设值。这里就会用到我们上面提到的ContinueWhenAny()。

 List<int> list = new List<int>();
            for (int i = 0; i < 100; i++)
            {
                list.Add(i);
            }
            //实例化一个委托
            Action<int> action = (k) =>
            {
                Console.WriteLine(k.ToString() + " " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
            };
            TaskFactory taskFactory = new TaskFactory();
            List<Task> taskList = new List<Task>();
            //控制线程数量
            foreach (int i in list)
            {
                int j = i;
                //通过委托启动线程
                taskList.Add(taskFactory.StartNew(() => action.Invoke(j)));
                if (taskList.Count > 10)
                {
                    //有线程已经结束
                    Task.WaitAny(taskList.ToArray());
                    taskList = taskList.Where(t => t.Status != TaskStatus.RanToCompletion).ToList();
                    Console.WriteLine("当前线程数量 " + taskList.Count);
                }
            }

在这里插入图片描述

5 多线程的异常处理

       在多线程中处理异常时,并不能仅仅在主程序中去捕获异常,因为可能在程序运行结束后,其它线程才开始。所以要在每个线程执行的行为里面去捕获异常,即在Action中去捕获异常。

TaskFactory taskFactory = new TaskFactory();
                List<Task> taskList = new List<Task>();
                for (int i = 0; i < 20; i++)
                {
                    string name = "thread " + i.ToString();
                    Action<object> action = t =>
                    {
                        try
                        {
                            if (t.Equals("thread 11") || t.Equals("thread 12"))
                                throw new Exception(t + " 执行失败");
                            
                            Console.WriteLine("正在执行 " + t + " " + Thread.CurrentThread.ManagedThreadId.ToString("00"));
                            Thread.Sleep(2000);
                        }
                        catch (Exception ex)
                        {
                            Console.WriteLine(ex.Message);
                        }
                    };
                    taskList.Add(taskFactory.StartNew(action, name));
                }

在这里插入图片描述

6 多线程的取消

       当多个线程相继启动去执行时,当有一个线程中发生了异常时,其他线程应停止运行,未启动的线程应取消启动。
       我们可以用一个信号量来CancellationTokenSource来保存当前进程的运行情况。每当一个线程开始启动时,就先判断下信号量是否正常,若信号量是异常的就停止该线程。同时应在启动这些线程的线程中捕获异常, 这样就能获取线程未启动的消息,所以要使用Task.WaitAll(taskList.ToArray())来阻塞这个线程。
       先来创建一个函数,用来运行上述过程。

  private void TestThreadCancel()
        {
            #region 多线程异常处理 取消
            TaskFactory taskFactory = new TaskFactory();
            List<Task> taskList = new List<Task>();
            //保存多线程运行是否正常的信号量
            CancellationTokenSource cts = new CancellationTokenSource();
            try
            {
                for (int i = 0; i < 20; i++)
                {
                    string name = "thread " + i.ToString();
                    Action<object> action = t =>
                    {
                        try
                        {
                            if (t.Equals("thread 11") || t.Equals("thread 12"))
                                throw new Exception(t + " 执行失败");

                            if (cts.IsCancellationRequested)
                            {
                                Console.WriteLine("取消执行 {0} {1} ", t, Thread.CurrentThread.ManagedThreadId.ToString("00"));
                                return;
                            }

                            Console.WriteLine("正在执行 {0} {1} ", t, Thread.CurrentThread.ManagedThreadId.ToString("00"));
                            Thread.Sleep(2000);
                        }
                        catch (Exception ex)
                        {
                            cts.Cancel();
                            Console.WriteLine(ex.Message);
                        }
                    };
                    taskList.Add(taskFactory.StartNew(action, name, cts.Token));
                }
                //需要在所有系线程执行完毕后来捕获AggregateException 所以要阻塞
                Task.WaitAll(taskList.ToArray());
            }
            //捕获多线程信号量异常
            catch (AggregateException aex)
            {
                foreach (var item in aex.InnerExceptions)
                {
                    Console.WriteLine(item.Message);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            #endregion
        }

为了不阻塞主线程,我们并不能直接运行该函数,而是启动一个新线程来运行它。

 Action action = TestThreadCancel;
 action.BeginInvoke(null, null);

在这里插入图片描述

7 多线程的安全问题

       在运行多个线程时,如果它们都要访问同一个变量,那就会出现问题,我们先演示一个错误的写法。

int iNumber = 0;
            //多个线程访问共享变量 存在值覆盖的问题
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 10000; i++)
            {
                int k = i;
                taskList.Add(taskfactory.StartNew(() =>
                 {
                        iNumber++;
                 }));
            }

在这里插入图片描述
     正是因为多线程访问共享变量,存在了值得覆盖问题。所以,我们需要用的锁来保护共享变量。
     首先声明一个静态的私有的引用型变量

private static readonly Object lock_object = new object();

然后将上述代码稍加改动即可

int iNumber = 0;
            //多个线程访问共享变量 存在值覆盖的问题
            List<Task> taskList = new List<Task>();
            for (int i = 0; i < 10000; i++)
            {
                int k = i;
                taskList.Add(taskfactory.StartNew(() =>
                 {
                    //lock要减少作用范围
                    lock (lock_test)
                    {
                         iNumber++;
                    }
                 }));
            }

     如果使用lock(this)就需要特别注意,如果只有一个多线程对象的实例在运行,那么共享变量是安全的,如果有多个实例对象的运行,那么this只能指代某一个实例,那么共享变量还是会被非法访问。

 //对于该对象的不同实例去调用时 如果使用Lock(this) 则不互斥  只在某个实例内部互斥
//应为在不同额实例中 不同的this不同变量
TaskSafe taskSafe1 = new TaskSafe();
TaskSafe taskSafe2 = new TaskSafe();
taskSafe1.LockTest("taskSafe1");
 taskSafe2.LockTest("taskSafe2");

在这里插入图片描述
     可以看到这些线程并没有互斥地运行,所以推荐使用私有的静态引用型变零来代替this。

     另一个比较让人疑惑的是,在lock()的代码段中,使用递归是否会发生死锁。我们就一起来看一下。

//此时虽然有递归 但是在进入递归时 this是同一个   并且线程也是同一个
        //锁是为了防止不同的进程同时访问一个变量
        //但此时只有一个线程 并且this也是这个线程创建的
        private int iNumber = 0;
        //递归锁
        public void Lock_this()
        {
            iNumber++;
            lock (this)
            {
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + " Start");
                Thread.Sleep(2000);
                Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString("00") + " End");
                if (iNumber < 10)
                    Lock_this();   //this不会发生变化
                else
                    Console.WriteLine("End ");
            }
        }

在这里插入图片描述
      事实证明并没有发生死锁,因为锁是为了避免多个线程同时访问某个共享变量,但在上述的递归运行过程中,一直都是同一个进程,所以不会发生死锁。

      以上就是我为大家介绍的多线程部分的相关知识,如果大家想了解更多相关内容,我会继续更新下去。

发布了19 篇原创文章 · 获赞 6 · 访问量 9427

猜你喜欢

转载自blog.csdn.net/u012712556/article/details/90750799