系统编程-多线程(1)

多线程,作为一个开发者,这个名词应该不陌生。我在《对进程和线程的一些总结》中也有介绍,这里就不详述。

为什么要用多线程

很显然,多线程能够同时执行多个任务。举个例子,你打开某视频播放器,点击下载某个视频,然后你发现这个时候一直在下载,其他啥都干不了,那你肯定骂*。所以在这种情况下,可以使用多线程,让下载任务继续,同时也能继续其他操作。

作为一个包工头,一堆砖要搬,但是就一个人,可是你只能搬这么多,怎么办?多找几个人一起搬呗,但是其他人就也需要付工钱,没关系,能早点干完也就行了,反正总体工钱差不多。

同样的,如果有一个任务特别耗时,而这个任务可以拆分为多个任务,那么就可以让每个线程去执行一个任务,这样任务就可以更快地完成了。

代价

听起来都很好,但是多线程是有代价的。由于它们“同时”进行任务,那么它们任务的有序性就很难保障,而且一旦任务相关,它们之间可能还会竞争某些公共资源,造成死锁等问题。

绑核

通过下面的命令可将进程proName程序绑在1核运行:

1
taskset -c 1 ./proName

而如果只绑定了一个核,那么同一时刻,只有一个线程在运行,而线程之间的切换又会消耗资源,那么这种情况下反而会导致性能降低。

另外一种情况,就是设置的线程数大于总的逻辑CPU数:

1
2
$ cat /proc/cpuinfo| grep "processor"| wc -l
8

这样的情况下,设置更多的线程并不会提高处理速度。

小结

优点:

  • 更快,加快处理任务
  • 更强,同时处理多任务

缺点:

  • 难控制,编程困难
  • 不当使用降低性能,线程切换
  • bug难定位,资源竞争

如何创建多线程

普通的进程通常只有一个线程,称为主线程。

创建线程需要使用下面的函数:

1
2
3
#include <pthread.h>
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
#include <stdio.h>
#include <pthread.h>
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
13
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");
sleep(1);
return 0;
}

这样就好了(注意添加头文件

```)。
1
2
3
```
main func finished
thread run,value is 10

但是你会发现,

func finished```可能会先打印。这也就呼应了文章标题。
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
13
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");
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
    14
    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");
    }
    i = 6;
    printf("main func finished\n");
    pthread_join(tid,NULL);
    return 0;
    }

在创建线程后,修改i的值,你会发现在线程中打印的不会是10,而是6。

也就是说,创建线程的时候,传入的参数必须确保其使用这个参数时,参数没有被修改,否则的话,拿到的将是错误的值,

总结

本文通过一些小例子,简单介绍了线程概念,对于绑核,多线程同步等问题均一笔带过,将在后面的文章中继续介绍。

守望 wechat
关注公众号[编程珠玑]获取更多原创技术文章
出入相友,守望相助!