数据结构(逻辑结构,物理结构,特点)
-
集合
数组
栈
队列
链表
树
图
堆
散列表
C#多线程编程的同步也线程安全
前一篇文章记录了简单的多线程编程的几种方式,但是在实际的项目中,也需要等待多线程执行完成之后再执行的方法,这个就叫做多线程的同步,或者,由于多个线程对同一对象的同时操作造成数据错乱,需要线程安全。这篇文章主要记录多线程的同步异步如何实现线程安全的几种方式的笔记,如有错误,请大神不吝赐教。
因为代码里面有很详细的注释,所以下面直接附上代码,不做过多的解释,如有疑问可以百度相关主题的文章详细了解。
1、 Mutex
1.Mutex测试
Mutex互斥锁,用于多线程间的线程同步通过WaitOne等待当前锁定的线程执行完成,例如,线程B执行需要等待线程A执行结束的情况下,可以使用Mutex
同时Mutex还有一个比较有趣的功能就是可以设置实现客户端在同一太电脑上只能打开一个进程
//bool createNew = false;
//Mutex mutex = new Mutex(true, "MutexTest", out createNew);
//AutoResetEvent ae = new AutoResetEvent(false);//定义一个信号量,表示执行结束,可以释放互斥锁
参数1表示初始化的时候当前互斥锁是否已被获取,false代表未被获取,
参数2标识当前互斥锁的名称,指定一个名称,配合参数3即可实现只能开启一个进程的效果
参数3表示是否创建了一个新的互斥锁
//Thread t1 = new Thread(new ThreadStart(() =>
//{
// Console.WriteLine("我是线程1");
// Console.WriteLine("线程1开始执行!");
// Thread.Sleep(1000);//线程休眠1秒钟,用于模拟需要较长时间执行的功能
// Console.WriteLine("线程1执行结束!");
// ae.Set();
//}));
//Thread t2 = new Thread(() =>
// {
// Console.WriteLine("我是线程2");
// Console.WriteLine("线程2开始执行!");
// mutex.WaitOne();//等待互斥锁被释放,模拟实际项目中需要其他线程执行完毕方可执行的功能
// Console.WriteLine("线程2执行结束!");
// });
因为是多线程执行,所以线程1与线程2的谁先开始执行,以上代码中未进行控制,
但线程2一定是在线程1执行完成之后才能结束
//t1.Start();
//t2.Start();
//ae.WaitOne();//等待释放信息
//mutex.ReleaseMutex();//释放互斥锁
AutoResetEvent的功能类似于一个红绿灯信号,当达到可以释放的条件的时候,调用Set方法来通知后续代码可以执行了,
此处为何需要一个信号,是因为Mutex定义在主线程中,如果在异步线程中释放,会报一个错,提示在不安全的代码块中执行
互斥锁,所以此处使用信号来通知主线程可以释放互斥锁了
2、AutoResetEvent
///
/// 通过AutoRestEvent实现线程同步
///
public void TestAutoResetEvent()
{
AutoResetEvent[] autoResetEvents = new AutoResetEvent[3];
autoResetEvents[0] = new AutoResetEvent(false);//定义初始信号为关
autoResetEvents[1] = new AutoResetEvent(false);
autoResetEvents[2] = new AutoResetEvent(false);
//以下代码实现线程1结束之后线程2才能结束,线程2结束之后线程3才能开始,所有线程都结束之后主线程才能继续
Thread t1 = new Thread(new ThreadStart(() =>
{
Console.WriteLine("线程1开始!");
Thread.Sleep(1000);
Console.WriteLine("线程1结束!");
autoResetEvents[0].Set();
}));
Thread t2 = new Thread(new ThreadStart(() =>
{
Console.WriteLine("线程2开始!");
Thread.Sleep(1000);
autoResetEvents[0].WaitOne();
Console.WriteLine("线程2结束!");
autoResetEvents[1].Set();
}));
Thread t3 = new Thread(new ThreadStart(() =>
{
autoResetEvents[1].WaitOne();
Console.WriteLine("线程3开始!");
Thread.Sleep(1000);
Console.WriteLine("线程3结束!");
autoResetEvents[2].Set();
}));
t1.Start();
t2.Start();
t3.Start();
Console.WriteLine("主线程开始等待......");
autoResetEvents[2].WaitOne();//等待所有线程结束
//AutoResetEvent从字面即可知道是自动信号,意思为当信号被捕捉之后会自动重置为关闭状态
//对应的ManualResetEvent为手动信号,使用方法相同但是在被捕捉之后不会被重置为关闭状态
//需要手动调用Reset方法关闭信号,如果是简单的同步,使用自动信号即可,如果需要很复杂的流程控制
//可以使用自动信号,同时可以配合WaitHandle来实现线程的同步,WaitHandle拥有WaitAny方法等待任意一个信号
//WaitAll方法等待所有信号,使用方法与信号的WaiOne相似,此处不再进行举例,可以查看相关文章具体了解
Console.WriteLine("主线程执行结束!");
}
3、 lock与Monitor
///
/// 测试lock和Monitor实现线程安全的多线程
///
public void TestLockAndMonitor()
{
//lock与monitor实现相同的功能,多线程的线程安全
//lock实际上就是Monitor.Enter与Monitor.Exit的语法糖
object obj = new object();//创建一个应用类型用于lock
int count = 1;
int sum = 0;
for (int i = 0; i < 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
for (int j = 0; j < 1000; j++)
{
//此处保证线程安全的原理是,当前多个线程同时访问count的时候,如果不lock
//可能多个线程访问到的count是相同的值,这样虽然多个线程都执行了count++但是
//结果却没有加上去,造成最终的结果错误,当lock之后,lock内部的代码每次只能
//有一个线程访问,所以每个线程获取的count都不可能相同,这样就能保证最后的结果一定是正确的
//lock (obj)//取消此句代码测试多线程的不安全性,取消之后可能每次执行的结果都不一样
//{
// sum += count;
// count++;
//}
//使用下面的方法与使用lock的功能相同
//Monitor.Enter(obj);
//sum += count;
//count++;
//Monitor.Exit(obj);
}
}));
t.Start();
}
Thread.Sleep(3000);//延时3秒保证异步线程全部执行完成
Console.WriteLine(sum);
}
4、信号量
///
/// 测试信号量实现线程安全
///
public void TestSemaphore()
{
//Semaphore 类似于线程池,用于设置同时可以有多少个线程执行
//当线程超过信号量运行的最大值之后,后续的线程就需要等待
Semaphore semaphore = new Semaphore(2, 2);//用于设置最大可以有两个线程同时执行,初始时有两个位置空闲
for (int i = 0; i <= 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
semaphore.WaitOne();//等待信号释放,若未超过信号的最大数值,则不需等待
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Random random = new Random();
Thread.Sleep(random.Next(1,5) * 1000);//随机休眠1-5秒
semaphore.Release();//释放当前信号
}));
t.Start();
}
}
5、 自旋锁
///
/// 测试自旋锁
///
public void TestSpinLocked()
{
//自旋锁与lock实现的功能相同,但是lock锁住对象开销比较大
//相反自旋锁开销比较小,效率相对也比lock高,当锁住的次数比较多,同时锁的时间比较短的时候,可是使用自旋锁
int count = 1;
int sum = 0;
SpinLock spinLock = new SpinLock();
for (int i = 0; i < 20; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
bool lockTaken = false;
//使用下面的方法与使用lock的功能相同
//申请获取锁
spinLock.Enter(ref lockTaken);
for (int j = 0; j < 1000; j++)
{
sum += count;
count++;
}
if (lockTaken) //判断当前线程是否锁住,如果锁住则释放它,防止出现死锁的情况
{
spinLock.Exit();
}
}));
t.Start();
}
Thread.Sleep(3000);//延时3秒保证异步线程全部执行完成
Console.WriteLine(sum);
}
6、 原子操作
///
/// 测试InterLocked
///
public void TestInterLocked()
{
//InterLocked拥有几个方法来保证线程安全,每个操作都是原子级的,所以效率高,线程安全
//此方法使用InterLocked实现类似于自旋锁的功能
//关于InterLocked的更多用法请参考MSDN
double current = 0;
for (int i = 0; i < 10; i++)
{
Thread t = new Thread(new ThreadStart(() =>
{
while (Interlocked.Exchange(ref current, 1) == 1)
{
//此循环用于等待当前捕获current的线程执行结束
}
Console.WriteLine(Thread.CurrentThread.ManagedThreadId.ToString() + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
Random random = new Random();
Thread.Sleep(random.Next(1, 3) * 1000);//随机休眠1-3秒
Interlocked.Exchange(ref current, 0);//将current重置为0
}));
t.Start();
}
}
C#多线程编程笔记
在开发中经常有遇到因为程序执行的时间过长,而造成程序假死的情况,这是因为我们的程序是同步执行的,当执行到需要长时间的操作时,程序就会等待当前的操作完成,从而造成程序假死。C#的异步与多线程就是为了解决这个问题的。
- 什么是多线程,举个简单的例子,我们在做饭的时候,可以先煮好饭,然后炒菜,然后洗餐具,然后完成,每一个操作都是在前一个操作完成之后才能进行,这就叫做同步执行,我们也可以在边煮饭的同时炒菜,洗餐具,当所有的工作都做完的时候,饭也就做好了,在这个过程中,煮饭,炒菜是同时进行的,这个就是异步,多线程就类似于主线程在煮饭,然后另开一个新的线程用来炒菜。
- 多线程的优缺点,由上所述,多线程可以在新开的线程中执行操作,所以他不需要等到主线程完成,也不收主线程的影响,自然,也就不会造成程序假死的情况出现了。这样可以提高用户的交互体验,防止用户以为程序崩溃也不断重启。但是由于我们直接感受到的是主线程,而新开的线程其实是在后台执行的,所以,如果新开的线程出现了异常,我们很难再主线程中捕获,或者处理,同时新开一个线程也需要计算机硬件的支持,如果线程过多,可能会造成系统变得很卡,资源消费过多的现象。
- 什么情况下需要使用多线程的技术?根据多线程的特点,其实类似于一个后台执行的任务,所以一般在以下情况中会使用多线程技术。1、程序执行时间比较长,同时程序的结果不是那么重要,不应该是主线程等待结果的情况下,可以使用多线程异步执行。例如,登录之后的验证过程。2、需要定时刷新的功能,这些功能是定时循环执行,所以可以放在后台去异步执行,这样既能保证功能执行了,同时在执行的过程中也不会造成程序卡顿的现象。3、后台任务,程序只需要执行相关的功能,不需要接收执行结果。例如发送一条短信,我们只需要发送出去即可,不需要知道用户是否接收到了短信,这样的情况下可以使用异步发送。其他情况可以参考以上的情况来决定是否需要使用多线程。
多线程编程示例
.Net提供了多种方式实现多线程的编程,包括线程池,Thread,Task等方法,下面对应这些方法给出简单的示例。
首先,建立一个功能类,此类的作用是多线程需要执行的方法,方法包括,无参数无返回值,有参数无返回值,无参数有返回值,有参数有返回值四种情况。
///
/// 此类用于多线程测试的公用方法的定义
///
public class CommonClass
{
//注意多线程使用的方法对应的参数类型应该为object
///
/// 无返回值有参数的方法,用于无返回值的多线程的类型的使用
///
///
public void ShowName(object name)
{
Console.WriteLine("Your Name Is: {0}", name);
}
///
/// 有返回值的方法,用于有返回值的多线程类型的使用
///
///
///
public string GetName(object name)
{
return name + "English's name is Lily";
}
}
上面的代码只有两个方法,都带参数,但是一个有返回值,一个没有返回值。对于无参数的方法,只需要把传入的参数设置为null即可。
- 使用ThreadPool实现多线程
ThreadPool顾名思义就是线程池,由于创建一个新的线程的代价比较大,所以如果没有必要,就不需要创建一个新的线程。线程池就是为此设计的,当新创建一个线程的时候,首先到线程池中查询是否有空闲线程,如有,则直接使用此线程,这样就避免了新创建一个线程,若无,则新创建一个线程,并加入到线程池中,当线程执行结束之后,当前线程变为空闲线程,并放入线程池,等待下一个调动。当创建的线程数超过线程池允许的最大线程数之后,线程就需要排队,等待空闲线程的出现。
///
/// 线程池实现多线程的定义
/// 线程池与Thread的区别在于,Thread每次都是新建一个线程,执行完成后就销毁
/// 而线程池有一个最大线程数,会在池内空闲线程数不够的时候创建新的线程,并存入
/// 线程池,线程执行完成后并不会销毁,而是存入线程池,作为空闲线程,等待下一次调用,
/// 超过线程池最大线程数时,不会再创建新的线程,而是排队等待新的空闲线程,因此,
/// 线程池比Thread的性能更好
///
public class ThreadPoolTest
{
//和Thread一样,线程池只能创建无返回值和最多带一个参数的多线程方法
public void CreateThread()
{
//ThreadPool.SetMaxThreads(10, 10);//设置线程池最大线程数的方法
//ThreadPool.SetMinThreads(5, 5);//设置线程池最小线程数的方法
var com = new CommonClass();
for (int i = 2; i < 7; i++)
{
//往线程池中添加5个方法,但是线程池中不一定会创建5个线程
ThreadPool.QueueUserWorkItem(new WaitCallback(com.ShowName), i.ToString());
}
}
}
线程池的方法可以有参数,但是不能有返回值,或者只能返回void。
- Thread类实现多线程
Thread用于创建一个线程,他与线程池的区别就在于,他每次都会新创建一个线程,执行完成之后销毁线程。不存在等待空闲线程的概念,性能取决于硬件设备的性能。
///
/// 此类用于多线程的Thread类实现测试
///
public class ThreadTest
{
//Thread不能创建带有返回值的多线程方法,如果需要请使用Task
///
/// 用于创建多线程并行的代码
///
public void CreateThread()
{
var com = new CommonClass();
for (int i = 0; i < 5; i++)
{
//ParameterizedThreadStart类型用于定义一个带参数的方法的线程,如果需要定义不带参数的多线程实现,请使用ThreadStart
//同时参数只能有一个,如果需要使用多个参数,请使用其他多线程实现方法,或将多个参数直接封装为一个Class进行传递
Thread t = new Thread(new ParameterizedThreadStart(com.ShowName));
t.Start(i.ToString());
}
}
}
上面代码中红字部分为要执行的方法,从字面意思即可知道,这个方法是需要定义参数的,如果需要定义无参数的方法,则需要使用new ThreadStart(方法名),t.Start()方法用于开始执行线程,方法的参数即为调用的方法需要传入的参数。在上面的示例中,传入的参数即为ShowName方法所需的参数。和ThreadPool一样,Thread也是不能有返回值的。
- Task实现多线程
Task即为任务,也是实现多线程的一种方式,他定义的方法必须带一个object的参数,同时可以有返回值。
///
/// 通过Task实现多线程的方法
/// Task相比Thread和ThreadPool的区别最直观的就在于可以实现带返回值的方法
///
public class TaskTest
{
///
/// 使用Task创建不带返回值的多线程
/// 此处的void也可以为Task,在async的异步编程中必须为Task
/// 具体实现请参考CreateReturTaskThread
///
public void CreateNoReturnThread()
{
for (int i = 0; i < 5; i++)
{
//此处会形成一个闭包,所以多次返回的结果一样
Task.Run(() =>
{
Console.WriteLine("This Number is {0}", i);
});
//可以通过如下委托的方式解决上面的问题
//Action
//{
// Task.Run(() =>
// {
// Console.WriteLine("This Number is {0}", a);
// });
//};
//act(i);
}
}
///
/// 返回Task的方法
///
///
public Task CreateReturTaskThread()
{
return Task.Run(() =>
{
Console.WriteLine("This is a Method for return Task!");
});
}
///
/// 创建返回具体值的方法
///
///
public Task
{
return Task.Run(() =>
{
CommonClass com = new CommonClass();
string name = com.GetName("HoS ");
Thread.Sleep(5000);
return name;
});
}
}
在上面的示例中,既有返回void的方法,也有返回具体值的方法,使用Task.Run方法即可定义一个异步执行的方法。Run内部需要传入一个委托,来定义需要异步执行的功能,相比较Thread,代码的是想相对复杂,但是可以有返回值,同时创建一个任务相比建创建一个线程的开销小很多。
- async与await关键字
在C#4.0以后为简化异步操作,添加可async与await两个关键字,这两个关键字的内部也是通过Task来实现异步操作,但是如果不添加这两个关键字,那么方法就会以同步执行的方式来执行。同时await会等待方法执行的结果,但是在等待的过程中,不会阻塞主线程,也就不会造成程序假死的现象。
///
/// 此类用于测试.Net4.0的async实现异步编程的方法
///
public class AsyncTest
{
//1.定义一个返回值为Task
public Task
{
return Task.Run(() =>
{
return list.Sum();
});
}
//2.定义一个标识了async的方法
///
/// 此方法用async标识,代表这是一个异步执行的方法,此方法不会阻塞当前线程
///
public async void ShowSum()
{
List
//此代码加了await标识,与async配合使用,代表这是一个异步的方法,
//如果不加await方法,则此处代码会同步执行
//需要注意的是await后面的方法需要等到await执行完成之后才会继续执行,
//而不是和后面的代码一起执行,也就是说这里实现了间接的多线程的同步的功能,
//同时不会卡住主线程,可以解决Winform项目中界面的假死的问题
//因为使用了await关键字,所以不需要在使用.Result来获取Task的结果了
int result = await GetSum(list);
Console.WriteLine("Result is {0}",result);
}
}
如上代码所示async是需要添加在方法的定义上面,同时微软建议,所有的async标识的方法返回的都应该是Task
以上就是几种实现多线程的方式。调用的方法也很简单
//注意:多线程由于是异步执行,所以可能造成无法预知的执行顺序,即以下代码每次执行的结果都可能不同
//1.Thread实现
//ThreadTest.Common.ThreadTest tt = new Common.ThreadTest();
//tt.CreateThread();
//2.ThreadPool实现
//ThreadPoolTest tpt = new ThreadPoolTest();
//tpt.CreateThread();
//3.Task实现
//3.1 无返回值的实现
//TaskTest tt = new TaskTest();
//tt.CreateNoReturnThread();
//3.2 返回一个Task的实现
//var result = tt.CreateReturTaskThread();
//3.3 返回一个Task
//var result = tt.CreateReturnNameThread();
//Console.WriteLine(result.Result);
//4.async的实现
//var at = new AsyncTest();
//at.ShowSum();
//5.多线程的同步实现
//5.1 AutoResetEventTest
//LockTest lt = new LockTest();
//lt.AutoResetEventTest();
//5.2 MutexTest
//lt.MutexTest();
//5.3 MulLock
//lt.MulLock();
//5.3 ManualResetEvent实现线程的手动挂起
//lt.MulLockM();
AsyncTest at = new AsyncTest();
at.ShowSumNo();
Console.ReadKey();
上面的代码仅供学习使用,如果需要更加深入的了解各种方式,请参考相关的教程,谢谢!
1.其一
在运行速度方面:StringBuilder > StringBuffer > String
上实例
class Program { static void Main(string[] args) { TheTestString(); } ////// string /// public static void TheTestString() { string str = null; //创建计时器 头一回见 引这个 System.Diagnostics 命名空间 Stopwatch stopwatch = new Stopwatch(); //计时器 START stopwatch.Start(); for (int i = 0; i < 100; i++) { str += i; } //计时器 END stopwatch.Stop(); //输出STR Console.WriteLine(str); //输出程序运行时间 Console.WriteLine(stopwatch.Elapsed); //控制台程序闪退的坑 Console.ReadKey(); } }
class Program { static void Main(string[] args) { //TheTestString(); StringBuilder(); } #region StringBuilder public static void StringBuilder() { StringBuilder stringBuilder = new StringBuilder(); Stopwatch stopwatch = new Stopwatch(); //计时器 START stopwatch.Start(); for (int i = 0; i < 100; i++) { stringBuilder.Append(i); } //计时器 END stopwatch.Stop(); //输出STR Console.WriteLine(stringBuilder); //输出程序运行时间 Console.WriteLine(stopwatch.Elapsed); //控制台程序闪退的坑 Console.ReadKey(); } #endregion }
上述两个例子只是for循环 从0 到99 博主笔记本有点卡 当数据庞大时优势就展示出了 铁证如山 ,就是有图有真相的那种 那么,
为什么会出现这种运行速度的差别,引出其二区别
2.其二
数据类型
string :乃字符串常量 引用数据类型,一旦创建即不可更改,stringbuilder:StringBuilder是一个可变的字符序列(百度百科)
又如
1 string str="张"; 2 console.writeline(str); 3 str+="三"; 4 console.writeline(str);
貌似从张变成了张三 但却为两个对象,变量名称相同但是 指向的堆中数据不同 一个指向 “张“,另一个指向 “张三“”
而反观stringbuilder以Char为单位向字符串分配空间,所以操作中不需要进行额外的内存分配
3.其三
在线程安全上,StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,
但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线
程的情况下,还是建议使用速度比较快的StringBuilder。(csdn)
ps:看一哈vs里面的监视器,今天OVER。
一,何为数据结构,算法?
广义上来说明,数据结构就是指一组数据的存储结构,算法即为操作数据的方法。(倒是比书籍跟百科上讲的通俗的多)
举个栗子--都烂大街了不过好使
图书馆存放书籍 : 图书管理员会将书籍按照,类目,编号进行存储,存储书籍的这种方式叫做存储结构 ,有存储就有查找,无论你是按照类目,编号来定位书架,进而查找到你想要的那本书,查找你想要的书籍所用的方法就是算法。
狭义 某些著名的数据结构跟算法
二,数据结构与算法有何关系?
相辅相成,数据结构为算法服务,算法要作用在特定的数据结构之上。无法分割,不可孤立。如若,常用二分查找算法来存储数组中的数据,若选用链表数据结构,则二分查找算法不适用。
三,数据结构与算法中一个概念--复杂度分析。
数据结构与算法解决的是如何快速存储和处理数据的问题,因此需要一个考量效率和资源消耗的方法——即复杂度分析方法
四,常见的数据结构与算法
十个数据结构:
数组,
栈队列,
散列表,
二叉树,
堆,
跳表,
图,
Trie树
十个算法:
递归,
排序,
二分查找,
搜索,
哈希算法,
贪心算法,
分治算法,
回溯算法,
动态规划,
字符串匹配算法