上一节中,我们详细的学习了和多线程有关的概念,像进程、线程、多线程、CPU内核、并发、并行、串行、队列、同步、异步等概念。这一节中,我们将用代码来实现多线程。
如果对多线程概念不太清楚的,可以参考上一节内容,链接如下:
详解多线程(概念篇——进程、线程以及多线程原理)
说明:源码亲测,拒绝搬砖,源码可下载。
源码地址:https://github.com/weiman152/Multithreading.git
在iOS中,多线程的实现方法有多种,有OC的也有C语言的,有常用的,也有不常用的。本节中,我们就先探究NSThread这个OC的类对于实现多线程是如何进行的。
多线程的实现方法
- NSThread(OC)
- GCD(C语言)
- NSOperation(OC)
- C语言的pthread(C语言)
- 其他实现多线程方法
1.NSThread(OC)
NSThread是苹果提供的面向对象的操作线程的方法。简单方便,可以直接操作线程对象。
我们查看一下NSThread的API,发现内容并不多,属性和方法不是特别多,我们一个个来看看(根据字面意思理解的)。
注:不想看的可以跳过哟,直接到下面看代码。
先看看类的声明:
NSThread继承自NSObject。
-
currentThread
声明的第一个属性,currentThread,当前上下文所在的线程。这也是我们非常常用的一个属性。
-
类方法创建线程
-
isMultiThreaded 判断是否有多个线程
threadDictionary 线程字典
每个线程都维护了一个键-值的字典,它可以在线程里面的任何地方被访问。你可以使用该字典来保存一些信息,这些信息在整个线程的执行过程中都保持不变。
-
让当前线程阻塞一段时间
-
退出线程
-
线程的优先级
-
这几个字面上看不出来干嘛的
-
线程的名字
-
栈的大小
-
是否是主线程和获取主线程
-
初始化线程
-
线程状态(正在执行、结束、被取消)
-
线程主函数 在线程中执行的函数 都要在-main函数中调用,自定义线程中重写-main方法
-
线程有关的通知
上面的API都是我根据字面意思理解的,不一定正确,下面我们就用代码来试验一下NSThread实现多线程的过程吧。
1》类方法创建子线程,并在子线程中执行想要的操作
//类方法创建线程
- (IBAction)createThreadC:(id)sender {
NSLog(@"------------detachNewThreadWithBlock-------");
//block创建,并在子线程进行想要的操作
[NSThread detachNewThreadWithBlock:^{
NSLog(@"--block--%@",[NSThread currentThread]);
}];
NSLog(@"------------detachNewThreadSelector-------");
//在子线程中执行某方法
[NSThread detachNewThreadSelector:@selector(printHi) toTarget:self withObject:nil];
}
-(void)printHi {
NSLog(@"---printHi---");
NSLog(@"Hi, 我要在子线程中执行");
NSLog(@"--Sel--%@",[NSThread currentThread]);
}
打印结果:
分析:
createThreadC在主线程中,因为开辟子线程需要耗费时间,所以会先打印主线程的:
------------detachNewThreadWithBlock-------
------------detachNewThreadSelector-------
然后在打印子线程的内容。因为子线程是并发的,谁先执行完并不确定,所以先打印哪个子线程的内容也是不确定的。
注意:如果主线程和子线程都有一个for循环,循环很多次,那么主线程和子线程中的for循环打印很可能是交叉进行的。
我们再次运行,看看结果是否与上次一样呢。
与上次结果不太一样哟,与我们上面的分析是一致的。
2》判断当前是否开启了多个线程 isMultiThreaded
我们分别在子线程和主线程中使用isMultiThreaded,看看结果:
子线程中:
打印结果:
结果是 YES,就是开启了多线程。我们把开启的子线程注释掉再看看。
看看打印结果:
结果也是1,也是YES,这是为什么呢?
多方搜索,也没有找到答案。
我想,因为我是在一个应用程序中,应用程序默认开启主线程,是不是应用程序默认还开启了别的线程?我们看一下系统的CPU占用情况:
上图是程序程序刚启动的时候CPU的使用情况,我们并没有开启线程,但是系统却开启了5个线程,并且线程2是有使用的,所以我们打印是否开启了多线程的时候,会是YES。
我们静置了一会儿,再看看系统的线程情况:
现在就剩下线程1和线程8了。
我们自己开启了线程之后,看看CPU中线程开启情况:
在图中我们找到了我们自己创建的线程一,编号为12 。
现在,我们明白了,为什么在应用程序中打印 [NSThread isMultiThreaded]结果为什么一直是YES了。
那么,我们新建一个控制台项目,打印看看:
果然,打印是0,也就是NO,认为没有多个线程。
3》是否是主线程,打印主线程
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"000 %d", [NSThread isMultiThreaded]);
NSLog(@"isMainThread: %d", [NSThread isMainThread]);
NSLog(@"currentThread: %@", [NSThread currentThread]);
}
4》对象方法创建子线程
对象方法初始化子线程,我们可以得到一个子线程对象,然后使用这个子线程对象。如果我们要开启子线程,一定要调用start方法,不然线程是不会开启的。
- (IBAction)createThreadO:(id)sender {
NSLog(@"新建多线程");
//对象方法创建多线程 一
self.thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"thread1: %@",[NSThread currentThread]);
for (int i=0; i<100; i++) {
NSLog(@"i= %d", i);
[NSThread sleepForTimeInterval:1];
}
}];
self.thread1.name = @"线程一";
//对象方法创建多线程 二
NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
thread2.name = @"线程二";
[thread2 start];
}
-(void)hello:(NSString *)name {
NSLog(@"你好!%@",name);
NSLog(@"当前线程是: %@",[NSThread currentThread]);
}
看看打印结果:
因为线程一没有开启,只是初始化了,所以不会执行线程一的内容。
使用对象方法创建子线程,要想让线程执行,必须调用start方法开启子线程。
5》取消线程——cancel,并不能取消一个子线程
我们在NSThread中找到一个方法叫做cancel,看起来像是可以取消一个线程,我们来试一试。
- (IBAction)createThreadO:(id)sender {
NSLog(@"新建多线程");
//对象方法创建多线程 一
self.thread1 = [[NSThread alloc] initWithBlock:^{
NSLog(@"thread1: %@",[NSThread currentThread]);
for (int i=0; i<10000; i++) {
NSLog(@"i= %d", i);
}
}];
self.thread1.name = @"线程一";
//对象方法创建多线程 二
NSThread * thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(hello:) object:@"小明"];
thread2.name = @"线程二";
[thread2 start];
}
- (IBAction)threadStart:(id)sender {
NSLog(@"thread1开始");
[self.thread1 start];
}
- (IBAction)threadCancel:(id)sender {
NSLog(@"thread1 取消");
[self printState:self.thread1];
[self.thread1 cancel];
NSLog(@"cancel 后:");
[self printState:self.thread1];
if([self.thread1 isCancelled]==YES){
NSLog(@"thread1 被取消了,开始销毁它");
[NSThread exit];
self.thread1 = nil;
}
}
执行后发现,根本不能取消,线程还是在执行完循环之后才停止的。我们看看该方法的官方文档:
Instance Method
cancel
Changes the cancelled state of the receiver to indicate that it should exit.
意思是说,这个方法只是把cancelled的属性置为YES,并不能真正的取消当前线程。
看看打印结果:
我们要想取消一个子线程,只是使用cancel是做不到的,cancel只是把属性isCancelled设置为YES,并不能真正的取消一个子线程。我们可以配合isCancelled属性,使用类方法exit,取消一个子线程。
注意:上面我们的案例中,由于使用的是按钮取消,按钮方法是在主线程中进行的,在主线程中执行exit是不会有效果的。所以,在这种状态下,我们的线程一是不能被取消的。要想取消线程一,我们需要在子线程内部进行。
例如:
//再次测试取消线程
- (IBAction)cancelThreadAgain:(id)sender {
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
- (void)run {
NSLog(@"当前线程%@", [NSThread currentThread]);
for (int i = 0 ; i < 100; i++) {
NSLog(@"i = %d", i);
if (i == 20) {
//取消线程
[[NSThread currentThread] cancel];
NSLog(@"取消线程%@", [NSThread currentThread]);
}
if ([[NSThread currentThread] isCancelled]) {
NSLog(@"结束线程%@", [NSThread currentThread]);
//结束线程
[NSThread exit];
NSLog(@"这行代码不会打印的");
}
}
}
看看结果:
只打印了前20个数字,说明线程取消了。
网上有人说,如果在线程中使用了sleep方法,就不能取消线程了,我们试一试:
看看结果:
跟之前一样,还是可以取消的。说明sleep是不会影响线程的取消退出操作的。
6》线程状态
使用NSThread创建的子线程,我们可以得到线程的三个状态:是否结束、是否取消、是否正在执行
-(void)printState:(NSThread *)thread{
NSLog(@"状态,isCancelled: %d",[thread isCancelled]);
NSLog(@"状态,isFinished: %d",[thread isFinished]);
NSLog(@"状态,isExecuting: %d",[thread isExecuting]);
}
7》让线程阻塞一段时间
有的时候,我们希望线程等待一会儿再执行,这个时候,我们可以使用
+(void)sleepUntilDate:(NSDate *)date;
+(void)sleepForTimeInterval:(NSTimeInterval)ti;
这两个方法,让线程阻塞一会儿在执行。观察后发现,这两个方法也是类方法,那么我们调用的时候,会阻塞当前线程,还是把所有线程都阻塞呢?我们试一试吧。
- (IBAction)sleepAction:(id)sender {
NSThread * threadA = [[NSThread alloc] initWithBlock:^{
//threadA 阻塞2秒后执行
[NSThread sleepForTimeInterval:2.0];
for (int i=0; i<10; i++) {
NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
}
NSLog(@"threadA 结束了");
}];
threadA.name = @"线程A";
[threadA start];
NSThread * threadB = [[NSThread alloc] initWithBlock:^{
for (int i=0; i<10; i++) {
NSLog(@"%@, i = %d", [NSThread currentThread].name, i);
}
NSLog(@"threadB 结束了");
}];
threadB.name = @"线程B";
[threadB start];
}
打印结果:
先打印了线程B的内容,说明sleep方法并不会阻塞所有的线程,只会阻塞当前的线程。
另一个方法传入一个日期类型,也就是等到某一个特殊日期的时候才会执行。
//让这个线程等到某个日期的时候在执行,这里给的是当前时间的2秒后执行,只是为了测试。
[NSThread detachNewThreadWithBlock:^{
NSDate * date = [NSDate dateWithTimeIntervalSinceNow:2];
[NSThread sleepUntilDate:date];
NSLog(@"终于等到这一天啦!我执行啦!");
}];
结果:
8》案例:售票问题
描述:
假如我们有三个售票员ABC同时都在售票,每售出一张票,就从库存中减去一张,直到所有的票售完。
我们用代码去模拟这个过程。
分析一下:三个售票员我们用三个线程模拟,设置总票数为100,每个线程都执行一个总票数减1的操作,直到总票数为0 。
实现代码如下:
//售票
- (IBAction)sellTickets:(id)sender {
self.totalTickets = 100;
NSThread * t1 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t1.name = @"售票员:王美美";
[t1 start];
NSThread * t2 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t2.name = @"售票员:李帅帅";
[t2 start];
NSThread * t3 = [[NSThread alloc] initWithTarget:self selector:@selector(sell) object:nil];
t3.name = @"售票员:张靓靓";
[t3 start];
}
- (void)sell{
NSLog(@"开始售票,当前余票:%d", self.totalTickets);
while (self.totalTickets > 0) {
[NSThread sleepForTimeInterval:1.0];
self.totalTickets--;
NSLog(@"%@ 卖出一张,余票:%d", [NSThread currentThread].name, self.totalTickets);
}
}
看看打印结果:
我们发现,结果并不像我们预期的那样啊,输出有点错乱,而且居然出现了-1,这实在是不能容忍的。
为什么会出现这样的问题呢?
因为三个线程同时访问我们的公共资源self.totalTickets,当线程一访问了,还没有减1的时候,线程二或者线程三也进来访问了,这个时候,线程二或者线程三读取的还是之前的self.totalTickets,所以就会出现打印两次甚至三次相同余票的情况。
为了解决这个问题,我们在线程访问公共资源的时候加个锁,也就是说,当线程一准备访问公共资源的时候,我们就把公共资源锁住,不让其他线程进来。当线程一访问完了,再进行解锁,其他线程继续访问。
代码如下:
- (void)sell{
NSLog(@"开始售票,当前余票:%d", self.totalTickets);
while (self.totalTickets > 0) {
[NSThread sleepForTimeInterval:1.0];
//互斥锁--锁内的代码在同一时间只有一个线程在执行
@synchronized (self) {
if(self.totalTickets > 0){
self.totalTickets--;
NSLog(@"%@ 卖出一张,余票:%d", [NSThread currentThread].name, self.totalTickets);
}else{
NSLog(@"余票不足,出票失败!");
}
}
}
}
为了尽快打印,所以把总票数改成10张。
看看打印结果:
解决了问题。
NSThread小结:
NSThread是官方提供的,面向对象的创建多线程的方法。
- NSThread可以使用类方法快速创建子线程,但是得不到子线程对象,线程自动开启。
- NSThread可以使用对象方法创建子线程,能够得到子线程对象,但是要手动开启子线程。
- NSThread可以取消子线程、可以随时查看线程的状态(正在执行、被取消、结束)。
- NSThread可以随时查看当前代码所在的线程。
关于NSThread就先到这里吧,有任何问题请留言,谢谢!
祝大家生活愉快!