C#线程 ---- 线程同步详解

线程同步

说明:接上一篇,注意分享线程同步的必要性和线程同步的方法。

测试代码下载:https://github.com/EkeSu/C-Thread-synchronization-C-.git

一、什么是线程同步:

在同一时间只允许一个线程访问资源的情况称为线程同步。

二、为什么需要线程同步:

  • 避免竞争条件;
  • 确保线程安全;(如果两个线程同时访问一个资源并对那个资源做修改,就不安全了)

现在的计算机变得越来越多核,每一个CPU可以独立工作,但是对于内存和外部资源、数据库的访问却可能因为不同线程的访问使数据产生异常,常见的例子就是银行的转账的例子不再赘述。

 

三、线程同步的方法:

  • 同步代码中重要的部分;
  • 使对象不可改变;
  • 使用线程安全包装器;

    注意:局部变量、方法参数和返回值是放在堆栈中的,本身是线程安全的。

四、线程不安全的演示:

背景:在数据库的user_blance表插入两条数据,两人的balance值都为1000.00,整个user_balance表的balance总值为2000.00

        static string connectionStr = "Server=127.0.0.1;Port=3306;Stmt=;Database=exe_dev; User=root;Password=123456";public static void UnSafeThread() {Thread ThreadOne = new Thread(new ThreadStart(DrawMoney));ThreadOne.Name = "A001";Thread ThreadTwo = new Thread(new ThreadStart(DrawMoney));ThreadTwo.Name = "A002";ThreadOne.Start();ThreadTwo.Start();}private static void DoDrawMoney(){Random random = new Random();int money = random.Next(100);string userId = Thread.CurrentThread.Name;string selectSql = "select balance from user_balance where user_id=@UserId";string updateSql = "update user_balance set balance=@Balance+@Money where user_id=@UserId";string updateSql2 = "update user_balance set balance=@Balance-@Money where user_id<>@UserId";using (MySqlConnection conn= MySqlConnectionHelper.OpenConnection(connectionStr)){var balance = conn.ExecuteScalar(selectSql, new { UserId = userId });if (balance != null){conn.Execute(updateSql, new { Money = money, Balance=balance, UserId = userId });conn.Execute(updateSql2, new { Money = money, Balance = balance, UserId = userId });}}}private static void DrawMoney() {for (int i = 0; i < 100; i++){DoDrawMoney();}}

运行结果:

程序中有三条线程在跑:两条支线程,一条主线程,主线程负责统计钱的总数,两条支线程模拟两个人赚钱,赚过来赚过去,哈哈哈,依据查询成果可以看到,钱的总数原本是2000.00,但是之后开始减少。当然上面的异常也可以通过加事务解决,或者改变sql的实现方式balance=balance+money,不过这个不是我们讨论的重点,不展开。

 五、线程同步:
1、MethodImplAttribute:同步方法

  • 对象:方法。
  • 使用方式:放在方法上,作为方法的属性

MethodImplAttribute是一个属性,它用来告诉CLR方法是如何实现的,MethodImplAttribute的一个构造函数把MethodImplOptions的枚举值作为参数,MethodImplOptions的枚举值Synchronized告诉CLR,这个方法该一次性只能在一个线程上执行。 静态方法在类型上锁定,而实例方法在实例上锁定。 只有一个线程可在任意实例函数中执行,且只有一个线程可在任意类的静态函数中执行。

使用方式:在需要同步的方法上添加属性

[MethodImpl(MethodImplOptions.Synchronized)]
        [MethodImpl(MethodImplOptions.Synchronized)]private static void DoDrawMoney(){Random random = new Random();int money = random.Next(100);string userId = Thread.CurrentThread.Name;string selectSql = "select balance from user_balance where user_id=@UserId";string updateSql = "update user_balance set balance=@Balance+@Money where user_id=@UserId";string updateSql2 = "update user_balance set balance=balance-@Money where user_id<>@UserId";using (MySqlConnection conn= MySqlConnectionHelper.OpenConnection(connectionStr)){var balance = conn.ExecuteScalar(selectSql, new { UserId = userId });if (balance != null){conn.Execute(updateSql, new { Money = money, Balance=balance, UserId = userId });conn.Execute(updateSql2, new { Money = money, Balance = balance, UserId = userId });}}}

 

 2、SynchronizationAttribute 同步方法----同步上下文:

  • 对象:非静态类。
  • 使用方式:放在非静态类上,作为非静态类的属性,同时非静态类需要继承【ContextBoundObject】

 

代码演示(其余部分和不安全的演示代码完全一样):

    [Synchronization]class ThreadTestForSynchronization : ContextBoundObject{

上下文是一组属性或使用规则,这组属性或使用规则对执行时的相关对象都是通用的。我的理解是这些组成了程序运行的环境,我们使用【Synchronization】来在上下文中添加规则【ThreadTestForSynchronization】类是需要线程同步的,所以程序在运行的时候就是线程同步的。

注意:当前我使用的环境是VS2017, .Net Framework4.61,C#版本应该是6.0,SynchronizationAttribute属性和更早版本是发生了很大变化,更早版本的构造函数需要填入一个枚举值。

 3、使用Monitor同步----同步重要代码块:

  • 对象:代码块;
  • 使用方式:使用Monitor.Enter(object obj),Monitor.Exit(object obj);这两个是配套使用的,两个方法之间是需要同步的重要代码块,Enter放入的对象和Exit释放的对象应该一致。Enter使对象获得锁,Exit释放锁。
  • 注意事项:这两个方法的参数是object,所以不能锁住值类型参数,因为会导致发生装箱,装箱之后的数值是一样的,但是已经不是同一个东西了。

代码演示(下面的例子锁住number会报错,因为发生装箱):

        private int number;public void MonitorThread(){Thread ThreadOne = new Thread(new ThreadStart(PrintNumber));ThreadOne.Name = "梁山伯";Thread ThreadTwo = new Thread(new ThreadStart(PrintNumber));ThreadTwo.Name = "祝英台";ThreadOne.Start();ThreadTwo.Start();}private void PrintNumber(){Console.WriteLine(string.Format("Thread {0} enter Method:",Thread.CurrentThread.Name));Monitor.Enter(this);for (int i = 0; i < 10; i++){Console.WriteLine(string.Format("Thread {0} increase number value:{1}", Thread.CurrentThread.Name, number++));}Monitor.Exit(this);Console.WriteLine(string.Format("Thread {0} exit Method:", Thread.CurrentThread.Name));}

 输出结果会是很工整的,占篇幅就不贴出来了。

Monitor.Enter()方法是去争取锁,还有另外一个方法是可以去争取锁并且进行等待的,就是TryEnter()方法,该方法可以有一个返回值,返回是否成功获得锁。同时该方法有三个重载:
bool TryEnter(object obj);   ---- 有返回值不等待

bool TryEnter(object obj, int millisecondsTimeout);  ----- 有返回值且会等待锁

void TryEnter(object obj, TimeSpan timeout, ref bool lockTaken);   ----- 等待锁,成功则返回true到lockTaken.

下面看一下被改编的代码:

        private void PrintNumber(){Console.WriteLine(string.Format("Thread {0} enter Method:", Thread.CurrentThread.Name));bool isLock = Monitor.TryEnter(this,1000);Console.WriteLine(string.Format("Thread {0} Get Lock {1}", Thread.CurrentThread.Name, isLock));for (int i = 0; i < 10; i++){Thread.Sleep(100);Console.WriteLine(string.Format("Thread {0} increase number value:{1}", Thread.CurrentThread.Name, number++));}if (isLock){Monitor.Exit(this);}Console.WriteLine(string.Format("Thread {0} exit Method:", Thread.CurrentThread.Name));}

演示结果:

Thread 梁山伯 enter Method:
Thread 梁山伯 Get Lock True
Thread 祝英台 enter Method:
Thread 梁山伯 increase number value:0
Thread 梁山伯 increase number value:1
Thread 梁山伯 increase number value:2
Thread 梁山伯 increase number value:3
Thread 梁山伯 increase number value:4
Thread 梁山伯 increase number value:5
Thread 梁山伯 increase number value:6
Thread 梁山伯 increase number value:7
Thread 梁山伯 increase number value:8
Thread 祝英台 Get Lock False
Thread 梁山伯 increase number value:9
Thread 梁山伯 exit Method:
Thread 祝英台 increase number value:10
Thread 祝英台 increase number value:11
Thread 祝英台 increase number value:12
Thread 祝英台 increase number value:13
Thread 祝英台 increase number value:14
Thread 祝英台 increase number value:15
Thread 祝英台 increase number value:16
Thread 祝英台 increase number value:17
Thread 祝英台 increase number value:18
Thread 祝英台 increase number value:19
Thread 祝英台 exit Method:

分析:线程梁山伯先进入方法PrintNumber,它先获得锁,并且去执行函数,线程祝英台等待的时间为1000毫秒,线程祝英台每次循环都要睡眠100毫秒,循环10次需要睡眠1000毫秒,所以线程祝英台等待1000毫秒是无法获得锁的,那么线程祝英台什么时候执行呢,就是趁着线程梁山伯睡眠的时候执行,所以等待1000毫秒之后,线程祝英台趁梁山伯睡眠的时候执行了。如果我们把祝英台等待锁的时间延长到2000毫秒,那么她就可以等待锁成功。需要注意的是,等待时间是1000毫秒的时候祝英台是没有获得锁的,所以不能执行Monitor.Exit(this)操作,没有获得锁自然无法获得锁,所以需要加一个 if 的判断。

笔者理解:线程梁山伯睡眠的时候祝英台开始执行了,但是并没有获得锁,就进入了被保护的代码块,说明,线程睡眠的时候是会去释放锁,睡眠之后是或重新获得锁的,这里面应该有复杂的机制处理,值得研究。

 

4、使用Monitor同步重要代码块,并使用Wait,Pulse方法做线程间的交互-----等待和发出脉冲机制:

  • 对象:代码块;
  • 使用方式:Wait,Pulse在Enter和Exit方法之间调用,Wait方法:当在对象上调用Wait方法时,正在访问被Monitor对象的线程会释放锁并将进入等待状态(包括调用它的线程自己);Pulse方法:发出一个信号通知正在等待的线程可以继续执行了,即等待的线程可以重新竞争获得锁继续执行。
  • 个人对获得锁的理解:锁就是权力,有锁的人就有权力执行程序

演示程序:

 

    class ThreadTestForMonitorWait{private int result;private LockData lockData;public ThreadTestForMonitorWait(){this.lockData = new LockData();}public void MonitorWaitThread(){Thread ThreadOne = new Thread(new ThreadStart(WaitFirstThread));ThreadOne.Name = "WaitFirstThread";Thread ThreadTwo = new Thread(new ThreadStart(PulseFirstThread));ThreadTwo.Name = "PulseFirstThread";ThreadOne.Start();ThreadTwo.Start();}private void WaitFirstThread(){Monitor.Enter(lockData);Console.WriteLine(string.Format("Thread {0} enter MonitorWaitThread",Thread.CurrentThread.Name));for (int i = 0; i < 5; i++){Monitor.Wait(lockData);Console.WriteLine(string.Format("Thread {0} increase number value {1}", Thread.CurrentThread.Name, result++));Monitor.Pulse(lockData);}Console.WriteLine(string.Format("Thread {0} exit MonitorWaitThread", Thread.CurrentThread.Name));Monitor.Exit(lockData);}private void PulseFirstThread(){Monitor.Enter(lockData);Console.WriteLine(string.Format("Thread {0} enter MonitorWaitThread", Thread.CurrentThread.Name));for (int i = 0; i < 5; i++){Monitor.Pulse(lockData);Console.WriteLine(string.Format("Thread {0} increase number value {1}", Thread.CurrentThread.Name, result++));Monitor.Wait(lockData);}Console.WriteLine(string.Format("Thread {0} exit MonitorWaitThread", Thread.CurrentThread.Name));Monitor.Exit(lockData);}}public class LockData { }

运行结果:

Thread WaitFirstThread enter MonitorWaitThread
Thread PulseFirstThread enter MonitorWaitThread
Thread PulseFirstThread increase number value 0
Thread WaitFirstThread increase number value 1
Thread PulseFirstThread increase number value 2
Thread WaitFirstThread increase number value 3
Thread PulseFirstThread increase number value 4
Thread WaitFirstThread increase number value 5
Thread PulseFirstThread increase number value 6
Thread WaitFirstThread increase number value 7
Thread PulseFirstThread increase number value 8
Thread WaitFirstThread increase number value 9
Thread WaitFirstThread exit MonitorWaitThread
Thread PulseFirstThread exit MonitorWaitThread

可见:运行结果是很工整的,在上面的程序中,WaitFirstThread 方法先被调用, WaitFirstThread 进入循环会调用Wait方法,这个时候他会失去锁,因此无法继续执行程序,而后方法 PulseFirstThread 被调用,它所在的线程获得锁,不管三七二十一先来发出一个脉冲通知正在被锁定的线程:兄弟你可以继续竞争获得锁了,在一次循环的末尾它又调用了Wait方法,使自己失去锁,其他的线程可以竞争得到锁,所以接着 WaitFirstThread 所在的线程就获得了锁,整个过程就变成两个线程之间锁给来给去,非常恩爱。

问题:其实这个程序是有问题的,就是必须要线程WaitFirstThread先执行,先释放锁,否则若是PulseFirstThread先执行,它先通知其他线程可以竞争锁了,之后执行一次循环把自己的锁释放掉,而线程竞争得到锁之后干的第一件事就是释放锁,这样就大家都没有锁了,死锁就产生了。有兴趣可以试试看。

5.使用lock关键字同步重要代码块 ---- 一块封装了Monitor的语法糖。

直接代码演示(改一下Monitor的PrintNumber方法的写法就行):

        private void PrintNumber(){Console.WriteLine(string.Format("Thread {0} enter Method:", Thread.CurrentThread.Name));lock (this){for (int i = 0; i < 10; i++){Console.WriteLine(string.Format("Thread {0} increase number value:{1}", Thread.CurrentThread.Name, number++));}}Console.WriteLine(string.Format("Thread {0} exit Method:", Thread.CurrentThread.Name));}

笔者觉得,比较好用的自然是lock,其实我几乎没有见过用Monitor的,但是一定要学会使用Monitor。

 6.使用ReaderWriterLock锁定文件:

我们一定都遇到过要写入一个文件返回该文件已经被其他程序锁定的错误,这个就是无法获得写锁,增加这个锁定可以防止这样的错误发生,使获得文件的写锁更有序。

对象:文件;

使用方式:

AcquireWriterLock(100);  尝试获取文件写锁;
ReleaseWriterLock();   释放文件写锁。
 

代码演示(运行时可以把下面绿色的代码注释掉以做比较会比较直观):

        private void WriterFileLock(){try{ rwl.AcquireWriterLock(100);using (StreamWriter writer = new StreamWriter("@ReadWriterLokTest.text")){for (int i = 0; i < 1000; i++){writer.WriteLine(i);}}Console.WriteLine("File Writer Finish");}catch (Exception ex){Console.WriteLine(ex.Message);}finally{ rwl.ReleaseWriterLock();}}

 

7、其他同步方式:

 (图片来源:C#线程参考手册-清华大学出版社)

 7.1 使用ManualResetEvent 同步

先看代码演示:

        static ManualResetEvent manualResetEvent = new ManualResetEvent(false);static Stopwatch stopwatch = new Stopwatch();public static void ManualResetEventThread(){stopwatch.Start();var success = manualResetEvent.WaitOne(1000, false);Console.WriteLine(Thread.CurrentThread.GetHashCode()+"获取信号:" + success + ",时间:"+ stopwatch.Elapsed);manualResetEvent.Set();success = manualResetEvent.WaitOne(1000, false);Console.WriteLine(Thread.CurrentThread.GetHashCode() + "获取信号:" + success + ",时间:" + stopwatch.Elapsed);}

输出结果:

1获取信号:False,时间:00:00:01.0425731
1获取信号:True,时间:00:00:01.0655502

ManualResetEvent 有两个方法:
Set():使状态变成有信号;

ReSet():使状态变成无信号。

其实ManualResetEvent 只是很简单的在不同线程同步了一个信号而已,并不会阻碍线程的向下继续执行,而WaitOne方法可以设置等待线程获取信号的时间,这个方法可以延缓线程的执行(一般不要这么玩)。

在使用上,可以根据WaitOne获取的状态来判断当前线程要不要继续执行。

另外需要介绍一下WaitAll和WaitAny的方法,首先是WaitAll:


我们先定义了三个方法:

        private static void PrintOne(){manualResetEvent1.Set();}private static void PrintTwo(){manualResetEvent2.Set();}private static void PrintThree(){manualResetEvent3.Set();}

      static ManualResetEvent manualResetEvent1 = new ManualResetEvent(false);
      static ManualResetEvent manualResetEvent2 = new ManualResetEvent(false);
      static ManualResetEvent manualResetEvent3 = new ManualResetEvent(false);

 

看WaitAll的测试:

        public static void WaitAllTest(){new Thread(new ThreadStart(PrintOne)).Start();new Thread(new ThreadStart(PrintTwo)).Start();new Thread(new ThreadStart(PrintThree)).Start();var isOk = ManualResetEvent.WaitAll(new WaitHandle[] { manualResetEvent1, manualResetEvent2, manualResetEvent3 }, 5000, false);if (isOk){Console.WriteLine("Oh,My God "+isOk);}}

输出结果:Oh,My God True。

WaitAll是WaitHandle的静态方法,它接收WaitHandle数组,当所有的WaitHandle的信号都返回true时,WaitAll才返回True.

而另外一个方法 WaitAny()方法与WaitAll的区别是,WaitAny当数组中一个WaitHandle获得信号的时候,就会返回,返回值为数组中有信号的WaitHandle的索引,当全部没有返回信号时,返回的是System.Threading.WaitHandle.WaitTimeout:

演示代码如下:

var hasSignalIndex = ManualResetEvent.WaitAny(new WaitHandle[] { manualResetEvent1, manualResetEvent2, manualResetEvent3 }, 2000, false);
if (hasSignalIndex == System.Threading.WaitHandle.WaitTimeout)
{
Console.WriteLine("Oh, fail");
}
else
{
Console.WriteLine("Oh,My God " + hasSignalIndex);
}

 7.2 使用AutoResetEvent同步 --- 与ManualResetEvent 类似,不做演示。

 

 7.3 使用Mutex线程同步

对象:线程

使用方式:一次只有一个线程能够获得锁,只有获得锁的线程释放了锁其他的线程才能够获得锁。

 

代码演示:

        static Mutex myMutex;public static void MutexThread(){myMutex = new Mutex(true, "myMutex");new Thread(new ThreadStart(PrintNo)).Start();for (int i = 6; i < 10; i++){Console.WriteLine(i);}myMutex.ReleaseMutex();}private static void PrintNo(){myMutex.WaitOne();for (int i = 0; i < 6; i++){Console.WriteLine(i);}}

输出结果:

6
7
8
9
0
1
2
3
4
5

在上面的演示中,主线程先获得锁,接着启动线程,但是线程是没有锁的,所以先执行主线程的打印循环,当主线程释放锁的时候,线程才获得锁,这时候线程才执行。Mutex.WaitOne也有其他的重载方法,可自行探索。

 7.4 使用InterLocked同步Int类型变量:

对象:Int类型变量;

使用方式:InterLocked.Increment(ref a)等方法;

代码演示:

        private static int a = 0;public static void InterLockThread(){for (int i = 0; i < 100; i++){new Thread(new ThreadStart(IncreaseInt)).Start();}for (int i = 0; i < 100; i++){new Thread(new ThreadStart(IncreaseInt)).Start();}}private static void IncreaseInt(){for (int i = 0; i < 1000; i++){//a++;Interlocked.Increment(ref a);}Console.WriteLine(string.Format("Thread:{0}  value: {1}", Thread.CurrentThread.GetHashCode(), a));}

在上面的 IncreaseInt 方法中有 a++ 和 Interlocked.Increment(ref a)两种方式,其中 Interlocked.Increment(ref a) 是原子操作。可能有人会想a++也才一句语句,怎么会不是原子操作,但是实际上我们的程序最后都是编译成了计算机能够认得的计算机指令,一句a++是有很多指令组成的。

结果演示:可以自己试着把a++和 Interlocked.Increment(ref a)切换注释一下运行,结果很明显

 

 7.5 使用ThreadStatic属性为类的静态变量创建副本,使得每一个线程的变量独立

对象:类静态变量;

使用方式:把要设置多个副本的类静态变量标记属性【ThreadStatic】

效果:标记了ThreadStatic的类静态变量各个线程独立不会因为其他线程堆值的改变而共享。

代码演示:

        [ThreadStatic]static int x;static int y;public static void StaticAttributeThread(){Task.Run(() => { IncreaseInt(); });Task.Run(() => { IncreaseInt(); });}private static void IncreaseInt(){for (int i = 0; i < 5; i++){Console.WriteLine(string.Format("current thread {0},x={1}, y={2}",Thread.CurrentThread.GetHashCode(),x++,y++));}}

输出结果:

current thread 3,x=0, y=0
current thread 3,x=1, y=2
current thread 3,x=2, y=3
current thread 3,x=3, y=4
current thread 3,x=4, y=5
current thread 4,x=0, y=1
current thread 4,x=1, y=6
current thread 4,x=2, y=7
current thread 4,x=3, y=8
current thread 4,x=4, y=9

可以看到,在不同的线程,y值是共享了修改结果的,而x是没有的。

 

 

 

 

 

转载于:https://www.cnblogs.com/heisehenbai/p/9960978.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/366655.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

响应式方案调研及前端开发管理思考

网易首页响应式风格实现技术调研网易首页实现页面&#xff08;字体&#xff09;响应式风格的方式是在不同尺寸的视口中使用不同的容器类&#xff0c;如图 1所示。当视口大于等于1420px时&#xff0c;使用大尺寸容器类 &#xff08;index2017_1200_wrap&#xff0c;width: 1200p…

linux nexus启动_Linux一键部署Nexus 3私服仓库自动化部署脚本

此脚本是Linux一键部署Nexus 3私服仓库自动化脚本&#xff0c;有需要朋友可以参考&#xff0c;脚本内容如下&#xff1a;环境准备&#xff1a;操作系统&#xff1a;CentOS Linux release 7.8.2003软件版本&#xff1a;Docker&#xff1a;docker-ce-19.03.12[rootlocalhost ~]# …

flex.css快速入门,极速布局

什么是flex.css? css3 flex 布局相信很多人已经听说过甚至已经在开发中使用过它&#xff0c;但是我想我们都会有一个共同的经历&#xff0c;面对它的各种版本&#xff0c;各种坑&#xff0c;傻傻的分不清楚&#xff0c;flex.css就是对flex布局的一种封装&#xff0c;通过简洁…

优化Angularjs的$watch方法

Angularjs的$watch相信大家都知道&#xff0c;而且也经常使用&#xff0c;甚至&#xff0c;你还在为它的某些行为感到恼火。比如&#xff0c;一进入页面&#xff0c;它就会调用一次&#xff0c;我明明希望它在我初始化之后&#xff0c;值再次变动才调用。这种行为给我们带来许多…

JavaFX中的塔防(2)

在上一部分中&#xff0c;我们创建了一个简单的编辑器&#xff0c;让我们放置炮塔。 现在&#xff0c;我们将在敌人起源的地方添加一个生成点&#xff0c;并为其定义攻击目标。 首先&#xff0c;我将通过对象层向地图添加更多信息。 这是标准的TMX&#xff0c;因此我们可以在Ti…

12面魔方公式图解法_三阶魔方入门

一、魔方的构造这里只讲常见的普通三阶魔方。三阶魔方一共有26个色块&#xff0c;分三个层&#xff0c;从上到下分别为顶层、中间层、底层。26个色块按位置分为中心块、角色块、棱色块。中心块6个&#xff0c;角色块8个&#xff0c;棱色块12个。中心块为每一个面最中央的色块。…

Linux ls命令详解

ls常见命令参数 ls: -F 给不同的文件添加不同表示,添加帽子 d/ l* s -a: 显示隐藏文件 以.开头的文件 -p: 只给目录添加/ -t: 按照修改时间排序 time --time-stylelong-iso: ls -l --time-stylelong-iso 显示友好长格式时间 -r: 倒着排序 reverse -S: 按照文件…

caffe 人脸关键点检测_人脸检测关键点新增至81个,比Dlib更精准、更贴边

人脸关键点检测是人脸识别和分析领域中的关键一步&#xff0c;它是诸如自动人脸识别、表情分析、三维人脸重建及三维动画等其它人脸相关问题的前提和突破口。虽然人脸的结构是确定的&#xff0c;由眉毛、眼睛、鼻子和嘴等部位组成&#xff0c;近似是一个刚体&#xff0c;但由于…

美团点评云真机平台实践

背景 随着美团点评业务越来越多&#xff0c;研发团队越来越庞大&#xff0c;对测试手机的需求显著增长。这对公司来说是一笔不小的开支&#xff0c;但现有测试手机资源分配不均&#xff0c;利用率也非常有限&#xff0c;导致各个团队开发、测试过程中都很难做到多机型覆盖。怎…

微型计算机和pc的概念,微型计算机IBM-PC(0520)系统原理及应用

本书是周明德教授的《微型计算机系统原理及应用》的第六版。曾获全国畅销书一等奖。根据微处理器的新发展&#xff0c;本书从80x86系列微处理器整体着眼&#xff0c;落实到基本的处理器8086&#xff0c;介绍了微型计算机系统原理、80x86系列微处理器结构、8086指令系统和汇编语…

volta架构 微型计算机,性能大爆炸 NVIDIA新GPU架构曝光

一年一度的GTC大会目前正在大洋彼岸的美国加利福尼亚州圣何塞市召开&#xff0c;这是由NVIDIA主办的GPU通用计算技术大会&#xff0c;号称是“图形技术巫师”们的聚会。几乎每次GTC大会上NVIDIA都会拿出来些压箱底的东西震场面&#xff0c;这届自然也不会例外。NVIDIA在GTC大会…

有一本书,适合零到十年经验的程序员看

这本书就是《代码大全》。这书名看起来就不想读&#xff1f; 我第一次看到这个书名的时候&#xff0c;心想难道这本书要把所有编程语言都讲一遍吗&#xff1f;但是当我深入阅读这本书之后&#xff0c;简直爱不释手。 这本书太厚了&#xff0c;你看不下去&#xff1f; 是的&a…

西门子数控面板图解_学好四要点让你迅速成为数控机床“操作高手”

当前国内许多刚刚从事数控机床操作人员的分类来说&#xff0c;一部分操作者是&#xff0c;对机械加工非常熟悉&#xff0c;但对于数控机床的编程是比较陌生的&#xff1b;一部分是刚毕业的学生&#xff0c;他们对机械加工知识&#xff0c;数控加工和编程的理论比较熟悉&#xf…

Android Service、IntentService,Service和组件间通信

Service组件 Service 和Activity 一样同为Android 的四大组件之一&#xff0c;并且他们都有各自的生命周期&#xff0c;要想掌握Service 的用法&#xff0c;那就要了解Service 的生命周期有哪些方法&#xff0c;并且生命周期中各个方法回调的时机和作用 什么是service&#xff…

生物计算机科学家,科学家开发细胞计算机 人体就是一台大型计算机

原标题&#xff1a;科学家开发细胞计算机 人体就是一台大型计算机欢迎收看新一期“新奇榜”&#xff0c;新鲜科技、奇闻怪事尽在新奇榜。近日&#xff0c;瑞士研究人员成功制造出了一种功能强大、类似计算机的人体细胞。这种细胞可能被用来帮助监测一个人的健康状况&#xff0c…

k8s源码分析 pdf_rook源码分析之一:rook架构解析

rook简介Rook是一款云原生环境下的开源分布式存储编排系统&#xff0c;目前支持 Ceph、NFS、Edegefs、Cassandra、CockroachDB等存储系统。它实现了一个自动管理的、自动扩容的、自动修复的分布式存储服务。Rook 支持自动部署、启动、配置、分配、扩容/缩容、升级、迁移、灾难恢…

weex eros框架源码解析

weex eros是基于alibaba weex框架进行二次封装的客户端跨平台开发框架&#xff0c;主要是为前端开发者&#xff08;可以不用熟悉客户端开发)提供的一站式客户端app开发解决方案。官网地址为&#xff1a;https://bmfe.github.io/eros-docs/#/。为了方便前端开发者和客户端开发者…

多选一的图片和文字

利用 radio 做单选事件&#xff0c;js 兄弟选择 nextSibling 获取邻近的图片对象&#xff0c;然后进行改变 例子&#xff1a; CSS <style type"text/css">input[type"radio"] {display: none;}label{font-size: 16px;}.choose_or{width: 1.2rem;heig…

【每日一题】收集足够苹果的最小花园周长

文章目录 Tag题目来源解题思路方法一&#xff1a;二分枚举答案 写在最后 Tag 【二分枚举答案】【二维网格】【2023-12-24】 题目来源 1954. 收集足够苹果的最小花园周长 解题思路 方法一&#xff1a;二分枚举答案 思路 通过如下过程&#xff0c;我们可以求出边长为 2n 时&…

小数前的0在html不显示,jsp小数显示问题 例如 我在oracle 数据库中查询出来的是 0.01 但是在jsp页面上就显示成 .01 没有前面的0...

满意答案1234junling2013.08.28采纳率&#xff1a;56% 等级&#xff1a;12已帮助&#xff1a;6022人控制保留几位有效小数的js函数//Code CreateBy abandonship 2007.03.12function FormatAfterDotNumber( ValueString, nAfterDotNum ){var ValueString,nAfterDotNum ;var r…