线程同步:Lock 锁 线程并发处理 | 异步调用委托的使用

线程同步:同步就是协调步调,线程按照预定的先后次序进行运行

比如:你做我这件事,我再做,即:阻塞式运行,它主要用来解决线程的安全性问题的。

lock锁:两个(多个)线程访问一个变量,存在并发性的问题

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static int max=10000000;
        static long _count = 0;

        static void Main(string[] args)
        {
            //Console.WriteLine("主线程运行");

            //t1线程递减
            Thread t1 = new Thread(new ThreadStart(() =>
            {
                for (int i = 0; i < max; i++)
                {
                    //_count--;
                    _count = _count - 1;
                }

            }));
            t1.Start();
            Console.WriteLine("t1线程已经启动,开始对_count变量++");
            Console.WriteLine("主线程继续执行.......");
            
            //主线程递增
            for (int i = 0; i < max; i++) 
            {
                _count++;
            }

            t1.Join();
            Console.WriteLine("_count的值是" + _count);
            Console.ReadKey();
        }
    }
}

 

程序的执行结果,并没有我们想象中的_count=0

原因如下:

其实_count++ 的意思是:_count=_count+1

总共分为三步: 第一步:把_count变量中的值取出来   第二步:将取出来的值加1   第三部:将加1后的值在赋值给_count
 

现在是主线程执行_count++的任务,而子线程t1执行的是_count-- 的任务

因为我们不只是CPU什么时候调度主线程,什么时候调度子线程,那我们可以做个假设。假如cpu最先调度主线程把0从_count中取出,然后++变成了1 然后又把1赋值给_count ,而当主线程执行了这三步了,而子线程t1一次都没被调用,单主线程执行第四部在把把1从_count中取出,t1这个子线程还是没有被CPU调度执行。

在第五步的时候,此时cpu调度了子线程t1,而没有调度主线程,此时t1这个线程里执行的是 从_count中把1取出 ,而就在t1从_count中把1取出后,CPU又去调度主线程去了

所以第六步 ,主线程执行的是 把取出的1加加结果是2,然后第七步CPU又没有调度子线程,而是调度的是主线程,此时主线程执行的是:把2赋值给_count 

当第八步的时候,CPU终于调度子线程t1了。因为在第五步的时候t1已经从_count中把1取出 所以 第八步的时候子线程执行--操作,把取出的1递减结果是0  而第九步CPU有是调度t1这个子线程 ,子线程t1执行的是 把0赋值给_ount

以上结论是 假如主线程和子线程里的循环都循环1千万次,当主线程调用了6次的时候将_count从0自增到了2   而子线程仅仅执行了3次就将“2”--到0,其实这样说不对,应该说

将2直接赋值为0了

 

 

解决并发操作的的问题的方法   lock 锁

lock锁的原理:

每个对象其实在内存中都会分配一个对象头空间,其中的最后2个bit就是用来存放锁标识的。

private static readonly object obj = new object();
lock(obj) 
{

}


我们在lock方法中传递了一个引用类型的对象,我们知道这个引用类型对象objt的对象头空间的最后2个bit是用来存放锁标识的,那么这个lock就是为这2个bit空间存一个值。(既然是bit,它的值要么是0要么是1)
如果这2个bit空间的值是0标识未锁住,如果是1就表示已上锁。

当线程调度器去标记为可执行的线程对列中取一个线程交给CPU执行的时候,如果碰到lock,它就会去这个ojb对象读这个obj对象头空间的最后那2个bit空间的值,发现如果是不是0表示没有权限访问,就把这个线程调度器就就把这个线程又丢到“标记可执行的线程队列当中去”

 

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    class Program
    {
        static int max=10000000;
        static long _count = 0;
        
        //锁的使用步骤:
        //1.建立一个锁的对象,锁对象必须是引用类型。(而且一般是只读的)
        //如果使用值类型,会在加锁和解锁的时候造成装箱和拆箱的问题,加锁的时候使用一个对象,而解锁的时候使用的是另外一个对象
        
        //一个项目可以建立多个锁
        //锁的使用:
        //假如有3个线程并发访问变量A,那么就为这个3个线程建立一个锁。 假如又有5个线程并发访问变量B,那么就再为这个5个线程再建立一个锁

        //使用锁会导致程序变慢(变慢的主要原因就是cpu调度线程的时候进行线程切换需要时间),所以只有在防止并发的时候才加锁
        static readonly object objSync = new object();//建立这个objSync对象的意义,就是让lock里面的代码同步,没有其他的任何意义。
        static void Main(string[] args)
        {
            //Console.WriteLine("主线程运行");

            //t1线程递减
            Thread t1 = new Thread(new ThreadStart(() =>
            {
                for (int i = 0; i < max; i++)
                {
                    //在lock块中的代码同时只能有一个线程来访问(因为主线程和t1线程会并发访问_count这个变量,所以在使用_count的时候都将他们锁起来)
                    lock (objSync)
                    {
                        //_count--;
                        _count = _count - 1;
                    }

                    //下面解释为什么锁对象必须是引用类型
                    //通过反编译得知 lock语句其实被编译成了下面的代码:
                    /*
                    bool isLockOK = false;//标记当前变量是否已经上锁
                    
                    Monitor.Enter(objSync,ref isLockOK); //上锁(因为Enter的参数类型都要求的是object类型,如果你的锁定义为值类型,而这里又需要一个引用类型,当上锁的时候,假如此时你传递一个值类型10进来,由于Enter的参数是引用类型,就会发生装箱,装箱的话,堆里就参数一个新的对象了。然后再下面的解锁过程中,你再把值类型10传递进来,又发生一次装箱,堆里又生成一个对象了。这样就导致加锁和解锁不是同一个对象)
                    try
                    {
                        _count--;
                    }
                    finally 
                    { 
                        if(isLockOK)
                        {
                            Monitor.Exit(objSync); //解锁 (Exit的参数类型都要求的是object类型)
                        }
                    }
                    */
                    
                }

            }));
            t1.Start();
            Console.WriteLine("t1线程已经启动,开始对_count变量++");
            Console.WriteLine("主线程继续执行.......");
            
            //主线程递增
            for (int i = 0; i < max; i++) 
            {
                //在lock块中的代码同时只能有一个线程来访问
                lock (objSync)
                {
                    _count++;
                }
               
            }

            t1.Join();
            Console.WriteLine("_count的值是" + _count);
            Console.ReadKey();
        }
    }
}

上了锁后,发现_count的值最后为0 了

 


 

线程池

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 线程池
{
    class Program
    {
        static void Main(string[] args)
        {
            //使用线程池的目的就是它能内部帮我维护,并且写起来简单。不好的地方就是,因为它里面是内部创建的线程,我们不能手动控制比如Sleep,Join等等
            //线程池的使用:比较耗时,比较长的任务不要使用线程池,还是自己去创建一个线程,不要用线程池。只有比较短的,比较不耗时的就使用线程池
            //ThreadPool线程池里面有一个QueueUserWorkItem方法,在这个方法里,可以把我们要异步执行的方法放到这个QueueUserWorkItem队列里面,放到这个队列里面,在它内部它会自动帮你启动这个线程,帮你执行,并且线程池中创建的所有线程都是后台线程,没有前台线程
            ThreadPool.QueueUserWorkItem(new WaitCallback((obj) => {

                //这里的方法就是要进行异步执行的方法
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(obj);//这里会输出10个"你好"
                    Thread.Sleep(1000);
                }
            }), "你好");//第二个参数其实就是第一个参数,即方法使用的数据对象。因为WaitCallback这个委托的第一个参数是一个方法,第二个参数是这个方法的参数


            //在创建一个线程池
            ThreadPool.QueueUserWorkItem(new WaitCallback((obj) =>
            {

                //这里的方法就是要进行异步执行的方法
                for (int i = 0; i < 10; i++)
                {
                    Console.WriteLine(obj);//这里会输出10个"你好"
                    Thread.Sleep(1000);
                }
            }), "我好");


            Console.ReadKey();
            
        }
    }
}

 

异步委托的使用(异步委托使用的是线程池中的线程,所以它是一个后台线程)

先来看看正常的委托是怎么使用的:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 异步调用委托
{
    //声明一个委托
    public delegate int Mydelegate(int n1, int n2);
    class Program
    {
        static void Main(string[] args)
        {
            //正常的委托是这样使用的
            Mydelegate md = new Mydelegate(GetSum);
            int a=md(1, 100); //这里通过int a就可以获取md这个委托的返回值了
            Console.ReadKey();
        }
        static int GetSum(int a1,int a2)
        {
            int sum=0;
            for (int i = a1; i <= a2; i++)
            {
                sum += i;
                Thread.Sleep(10);//每循环一次,就让这个主线程休息0.01秒钟
            }
            Console.WriteLine("方法中输出的结果是:{0}", sum);
            return sum;
        }
    }
}


在来看看异步委托如何使用:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 异步调用委托
{
    //声明一个委托
    public delegate int Mydelegate(int n1, int n2);
    class Program
    {
        static void Main(string[] args)
        {
            //正常的委托是这样使用的
            //Mydelegate md = new Mydelegate(GetSum);
            //int a=md(1, 100); //这里通过int a就可以获取md这个委托的返回值了
            //Console.WriteLine(a);


            //在来看看异步委托的是如何使用的:
            Mydelegate md1 = new Mydelegate(GetSum);

            //开始异步调用委托
            IAsyncResult result = md1.BeginInvoke(1, 100, null, null);//我们发现BeginInvoke这个方法是有放回值的,返回值是IAsyncResultl接口类型的(BeginInvoke()方法的返回值就是返回了一个对象,这个对象实现了IAsyncResult接口,并且该对象中封装了一些关于担起异步执行的委托的一些状态信息)


            //在调用BeginInvoke方法与调用EndInvoke方法之间我们可以做我们想做的任何事情;例如:
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("主线程继续执行......");
            }


            // 注意:EndInvoke方法既然要拿到异步调用的委托的放回值,就需要等待异步委托(方法)执行完毕,所以它就会阻塞线程
            //  ↓
            //获取异步调用的委托(方法)的返回值(EndInvoke是调用结束的意思,这里是调用结束并获取异步调用的方法的返回值)
            int aa = md1.EndInvoke(result); //我们发现EndInvoke这个方法的参数是一个IAsyncResult类型。 所以这里我们将result传递进来。(注意:EndInvoke方法的返回值类型是根据md1这个委托的返回值类型来确定的,因为md1这个委托的返回值类型是int类型,所以此时EndInvoke的返回值类型是int类型。)

            Console.WriteLine("异步调用的委托(方法)的返回值为{0}", aa);

            Console.WriteLine("主线程继续执行.......");
            Console.ReadKey();
        }
        static int GetSum(int a1, int a2)
        {
            int sum = 0;
            for (int i = a1; i <= a2; i++)
            {
                sum += i;
                Thread.Sleep(10);//每循环一次,就让这个主线程休息0.01秒钟
            }
            Console.WriteLine("方法中输出的结果是:{0}", sum);
            return sum;
        }
    }
}


 

我们希望开启这个异步调用委托后,异步调用的委托该干什么就干什么,而主线程也是该干什么就干什么(不想被阻塞)。等子线程执行完毕这个异步调用的委托后,告诉我执行的结果(返回值)就行了


可是因为当我调用EndInvoke的时候会阻塞主线程,虽然我们可以在调用EndInvoke之前做一些我想要做的事,可是只要调用EndInvoke,只要异步委托没有执行完毕就会阻塞主线程,那么我现在就不想让它阻塞线程。当BeginInvoke开始异步调用委托后,等这个委托执行完毕后返回值告诉我,我这里一下都不想等你(即:不想等异步调用的委托执行完毕)怎么办呢?


这时候我们可以使用BeginInvoke方法的第三个参数:AsyncCallback委托,这个委托执行是回调方法;这个回调方法的作用是:等异步调用的委托执行完毕后执行此方法

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Remoting.Messaging;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace 异步调用委托
{
    //声明一个委托
    public delegate int Mydelegate(int n1, int n2);

   
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("主线程的线程Id:{0}", Thread.CurrentThread.ManagedThreadId);

            //正常的委托是这样使用的
            //Mydelegate md = new Mydelegate(GetSum);
            //int a=md(1, 100); //这里通过int a就可以获取md这个委托的返回值了
            //Console.WriteLine(a);


            //在来看看异步委托的是如何使用的:
            Mydelegate md1 = new Mydelegate(GetSum);

            //开始异步调用委托
            IAsyncResult result = md1.BeginInvoke(1, 100, new AsyncCallback(Callback),"第四个参数是第三个参数的参数,注:第三个参数是一个委托");//我们发现BeginInvoke这个方法是有放回值的,返回值是IAsyncResultl接口类型的
       

            Console.WriteLine("主线程继续执行完毕");
            Console.ReadKey();
        }

        //自定义一个回调函数,这个回调函数由系统自动调用,当指定的异步委托调用完毕后,自动调用该方法
        static void Callback(IAsyncResult syncResult)
        {

            Console.WriteLine("回调函数中的线程Id:{0}", Thread.CurrentThread.ManagedThreadId);

            Console.WriteLine("回调函数的参数:{0}", syncResult.AsyncState);//因为我这个测试无需用到第三个参数的参数,所以第四个参数无意义,这里就打印出来看看而已

            //在回调函数中调用EndInvoke()方法就可以获取返回值

            //需要把接口类型转换成具体的对象
            AsyncResult ar = syncResult as AsyncResult;

            //注意:在这个回调函数中调用EndInvoke方法不会阻塞线程,因为异步委托调用完毕后才执行这个方法

            //AsyncDelegate是获取在其上调用异步调用的委托对象。这里先是将ar.AsyncDelegate转换成Mydelegate类型
            int sum = ((Mydelegate)ar.AsyncDelegate).EndInvoke(ar);

            Console.WriteLine("返回值是:{0}", sum);
        }
        static int GetSum(int a1, int a2)
        {

            Console.WriteLine("执行GetSum()方法的线程Id:{0}", Thread.CurrentThread.ManagedThreadId);
            int sum = 0;
            for (int i = a1; i <= a2; i++)
            {
                sum += i;
                Thread.Sleep(10);//每循环一次,就让这个主线程休息0.01秒钟
            }
            Console.WriteLine("方法中输出的结果是:{0}", sum);
            return sum;
        }
    }
}


打印结果得知:

执行GetSum()方法的线程Id与回调函数中的线程Id是一样的,即执行它们的是同一个线程(非主线程)

 

 

手写一个死锁

//这样会触发死锁,首先线程1将obj1锁了,然后线程1休眠100毫秒,这时候线程2启动,又将线程2锁了,继续往下运行,发现线程1被锁,这时候线程休眠100毫秒后继续执行,执行到lock (obj2)的时候,发现线程2又被锁了,周而复始。形成死锁 (CPU调度每个线程执行任务,执行任务都有一个时间间隔的,比如线程1执行20毫秒,即便任务没有执行完,也会将暂停这个任务,然后调度线程2执行任务,线程2执行20毫秒,即便任务没有执行完,也会暂停,然后又会调度线程1来继续,周而复始)
static void Main(string[] args)
{
    //线程1
    new Thread(() =>
    {
        Console.WriteLine("线程1获取锁obj1");
        lock (obj1)
        {
            Thread.Sleep(100);//休眠100毫秒很重要,如果不休眠的话,很有可能线程1加锁1后,又马上加锁2。这样就很有可能不会死锁
            lock (obj2)
            {
                Console.WriteLine("线程1获取锁obj2");
            }
        }

    }).Start();
    //线程2
    new Thread(() =>
    {
        lock (obj2)
        {
            Console.WriteLine("线程1获取锁obj2");
            lock (obj1)
            {
                Console.WriteLine("线程1获取锁obj1");
            }
        }
    }).Start();

    Console.ReadKey();
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值