多线程,作为一个开发者,这个名词应该不陌生。我在《对进程和线程的一些总结》中也有介绍,这里就不详述。
为什么要用多线程
很显然,多线程能够同时执行多个任务。举个例子,你打开某视频播放器,点击下载某个视频,然后你发现这个时候一直在下载,其他啥都干不了,那你肯定骂*。所以在这种情况下,可以使用多线程,让下载任务继续,同时也能继续其他操作。
作为一个包工头,一堆砖要搬,但是就一个人,可是你只能搬这么多,怎么办?多找几个人一起搬呗,但是其他人就也需要付工钱,没关系,能早点干完也就行了,反正总体工钱差不多。
同样的,如果有一个任务特别耗时,而这个任务可以拆分为多个任务,那么就可以让每个线程去执行一个任务,这样任务就可以更快地完成了。
代价
听起来都很好,但是多线程是有代价的。由于它们“同时”进行任务,那么它们任务的有序性就很难保障,而且一旦任务相关,它们之间可能还会竞争某些公共资源,造成死锁等问题。
绑核
通过下面的命令可将进程proName程序绑在1核运行:1
taskset -c 1 ./proName
而如果只绑定了一个核,那么同一时刻,只有一个线程在运行,而线程之间的切换又会消耗资源,那么这种情况下反而会导致性能降低。
另外一种情况,就是设置的线程数大于总的逻辑CPU数:1
2$ cat /proc/cpuinfo| grep "processor"| wc -l
8
这样的情况下,设置更多的线程并不会提高处理速度。
小结
优点:
- 更快,加快处理任务
- 更强,同时处理多任务
缺点:
- 难控制,编程困难
- 不当使用降低性能,线程切换
- bug难定位,资源竞争
如何创建多线程
普通的进程通常只有一个线程,称为主线程。
创建线程需要使用下面的函数:1
2
3
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数有必要做一下说明
- thread 线程ID指针,创建成功时,会保存在此
- attr 线程属性,控制线程的一些行为
- start_routine 线程运行起始地址,是一个函数指针
- arg 函数的参数,只有一个参数,因此多个参数需要打包在一起
创建成功时,返回0,否则出错。
看到了吗,到处都有void*的身影(参考《void*是什么玩意》)。
使用时注意包含头文件1
#include <pthread.h>
,并且在链接时加上-lpthread,因此它不在libc库中。在《一个奇怪的链接问题》中提到,对于非glibc库中的库函数,都需要显式链接对应的库。
试着写一个简单的多线程程序,简单起见,我们暂时不设置任何属性,将attr字段设置为NULL:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22//来源:公众号【编程珠玑】
//main.c
void *myThread(void *id)
{
printf("thread run,value is %d\n",*(int*)id);
//return NULL; 这种方式也可以退出线程
pthread_exit((void*)0);//退出线程
}
int main(void)
{
pthread_t tid ;
int i = 10;
int status = pthread_create(&tid,NULL,myThread,(void*)&i);
if(status < 0 )
{
printf("crete failed\n");
}
printf("main func finished\n");
return 0;
}
编译运行:1
2
3 gcc -o main main.c -lpthread
./main
main func finished
发现运行的结果并不如我们预期那样,就好像线程没有执行一样。
原因在于,如果主线程退出了,那么其他线程也会退出。所谓,皮之不存,毛将焉附,所有线程都共同使用很多资源,相关内容也可以从《对进程和线程的一些总结》中了解到。
如何改进呢?我们可以等线程执行完啊,于是,在主线程退出前sleep:1
2
3
4
5
6
7
8
9
10
11
12
13int main(void)
{
pthread_t tid ;
int i = 10;
int status = pthread_create(&tid,NULL,myThread,(void*)&i);
if(status < 0 )
{
printf("crete failed\n");
}
printf("main func finished\n");
sleep(1);
return 0;
}
这样就好了(注意添加头文件1
2
3```
main func finished
thread run,value is 10
但是你会发现,1
2
3
4
5但是转念一想,如果线程执行的时间超过一秒呢,难道就要sleep更长时间吗?而很多时候甚至根本不知道线程要执行多长时间,那怎么办呢?
还可以使用:
```c
int pthread_join(pthread_t thread, void **retval);
thread是前面获得的线程id,而retval包含了线程的返回信息,假设我们完全不关心线程的退出状态,那么可以设置为NULL。
修改代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13int main(void)
{
pthread_t tid ;
int i = 10;
int status = pthread_create(&tid,NULL,myThread,(void*)&i);
if(status < 0 )
{
printf("crete failed\n");
}
printf("main func finished\n");
pthread_join(tid,NULL);
return 0;
}
这种情况同样可以达到目的,pthread_join,会阻塞程序,直到线程退出(前提是线程为非分离线程)。
线程终止
以下几种情况下,线程会终止
- 线程函数返回
- 调用pthread_exit,主线程调用无碍
- 调用pthread_cancel
- 调用exit,或者主线程退出,所有线程终止
注意
假如修改下面的代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14int main(void)
{
pthread_t tid;
int i = 10;
int status = pthread_create(&tid,NULL,myThread,(void*)&i);
if(status < 0 )
{
printf("crete failed\n");
}
i = 6;
printf("main func finished\n");
pthread_join(tid,NULL);
return 0;
}
在创建线程后,修改i的值,你会发现在线程中打印的不会是10,而是6。
也就是说,创建线程的时候,传入的参数必须确保其使用这个参数时,参数没有被修改,否则的话,拿到的将是错误的值,
总结
本文通过一些小例子,简单介绍了线程概念,对于绑核,多线程同步等问题均一笔带过,将在后面的文章中继续介绍。