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,一经查实,立即删除!

相关文章

Spring中的类型转换

以下是一些需要类型转换的简单情况&#xff1a; 情况1。 为了帮助简化bean配置&#xff0c;Spring支持属性值与文本值之间的转换。 每个属性编辑器仅设计用于某些类型的属性。 为了使用它们&#xff0c;我们必须在Spring容器中注册它们。 案例2。 同样&#xff0c;在使用Sprin…

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

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

响应式html编辑器布局,基于Bootstrap响应式所见即所得的jQuery编辑器插件

LineControl Editor是一款基于Bootstrap的响应式、所见即所得的富文本编辑器jQuery插件。该富文本编辑器可以使用textarea元素或任何一个容器元素来生成&#xff0c;它拥有常见富文本编辑器的所有功能&#xff0c;使用快捷方便。插件依赖该富文本编辑器插件依赖于jQuery2.1.0和…

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 ~]# …

zabbix 模板 创建逻辑 + 主动模式-被动模式

模板通常包含了item、trigger、graph(图形)、application以及low-level discovery rule&#xff1b;模板可以直接链接至某个主机&#xff1b; 模板包含一系列的item&#xff0c;trigger等&#xff0c;可以快速地把多个item应用到host或者group。 参考&#xff1a;https://www.c…

JavaFX中的塔防(3)

在最后一部分中&#xff0c;您了解了如何创建Sprite&#xff0c;为其设置动画并赋予其Behavior。 但是动画效果不是很好&#xff0c;因为作为Insectoid&#xff0c;您应该始终看起来在飞行中。 记住&#xff1a;安全第一&#xff01; 我们可以通过创建自定义的TileSetAnimation…

day21 pickle json shelve configpaser 模块

1. 序列化:我们在网络传输的时候,需要我们对对象进行处理,把对象处理成方便存储和传输的格式,这个过程就叫序列化 序列化的方法不一定一样,三十目的都是为了方便储存和传输. 在python中有三种序列化方案: 1. pickle 可以将我们python中任意数据类型转化为bytes写入文件中…

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

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

计算机英语阅读路线,高考英语阅读理解真题解析·计算机运用

说明:引用此文请注明出处,并务请保留后面的有效链接地址,谢谢&#xff01;高考英语阅读理解真题解析计算机运用Computer people talk a lot about the need for other people to become“computer-literate.”But not all experts(专家) agree that this is a good idea.One pi…

优化Angularjs的$watch方法

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

JavaFX中的塔防(2)

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

微软edge浏览器不显示图片问题

用HBuider写的Web项目&#xff0c;项目名如果包含中文&#xff0c;edge下无法显示图片转载于:https://www.cnblogs.com/phoenixBlog/p/9964820.html

计算机辅助设计基础学什么,东大计算机辅助设计基础X20秋学期《计算机辅助设计基础》在线平时作业3资料...

计算机辅助设计基础X20秋学期《计算机辅助设计基础》在线平时作业36 e0 Y; q) j3 q3 c1.[单选题] 根据集成水平的不同&#xff0c;基于PDM的应用集成可分为3个层次&#xff0c;下面哪一个不在其中&#xff1f; 8 R- M/ w3 C& P" [ n. 答案资料下载请参考帮助中心说明…

在Python工作环境中安装包命令后加上国内源速度*15

example: pip install -r requests.txt -r https://pypi.tuna.tsinghua.edu.cn/simple/ --trusted-host pypi.tuna.tsinghua.edu.cn 转载于:https://www.cnblogs.com/xiangribai/p/9243426.html

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

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

Mongo 查询(可视化工具)

distinct MongoDB 的 distinct 命令是获取特定字段中不同值列表的最简单工具。 该命令适用于普通字段、数组字段以及数组内嵌文档&#xff08;集合对象&#xff09;。 db.getCollection(customer).distinct("customer_type")// chances字段的值是个集合&#xff0c;获…

使用JAX-RS的HTTP缓存

在上一个博客中&#xff0c;我们讨论了不同类型的缓存及其用例。 在本文中&#xff0c;我们将探讨如何利用HTTP响应标头和JAX-RS提供的支持来利用缓存。 过期标题 在HTTP 1.0中&#xff0c;一个名为Expires的简单响应头将告诉浏览器它可以缓存对象或页面多长时间。 在将来的某…

前端模块化,AMD与CMD的区别

原创 2016年08月03日 17:15:51标签&#xff1a;javascript /模块化 /前端21234最近在研究cmd和amd&#xff0c;在网上看到一篇不错的文章&#xff0c;整理下看看。 在JavaScript发展初期就是为了实现简单的页面交互逻辑&#xff0c;寥寥数语即可&#xff1b;如今CPU、浏览器性能…

上财的计算机专业408,【2020考研】上财408分经验分享

该楼层疑似违规已被系统折叠 隐藏此楼查看此楼数学&#xff0c;三月份到四月中旬把本科教材看了一遍。事实上还是有用的。比如&#xff0c;今年的二阶差分。看似超纲&#xff0c;其实不超纲。大纲要求认识二阶差分的形式&#xff0c;会解一阶差分方程。那道题恰好是用二阶差分表…

Linux ls命令详解

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