查看原文
其他

我有一个问题,用了多线程后,两个问题有了现在

守望先生 编程珠玑 2022-09-10
来源:公众号【编程珠玑】
作者:守望先生
ID:shouwangxiansheng
多线程,作为一个开发者,这个名词应该不陌生。我在《对进程和线程的一些总结》中也有介绍,这里就不详述。

为什么要用多线程

很显然,多线程能够同时执行多个任务。举个例子,你打开某视频播放器,点击下载某个视频,然后你发现这个时候一直在下载,其他啥都干不了,那你肯定骂*。所以在这种情况下,可以使用多线程,让下载任务继续,同时也能继续其他操作。
作为一个包工头,一堆砖要搬,但是就一个人,可是你只能搬这么多,怎么办?多找几个人一起搬呗,但是其他人就也需要付工钱,没关系,能早点干完也就行了,反正总体工钱差不多。
同样的,如果有一个任务特别耗时,而这个任务可以拆分为多个任务,那么就可以让每个线程去执行一个任务,这样任务就可以更快地完成了。

代价

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

绑核

通过下面的命令可将进程proName程序绑在1核运行:
taskset -c 1 ./proName
而如果只绑定了一个核,那么同一时刻,只有一个线程在运行,而线程之间的切换又会消耗资源,那么这种情况下反而会导致性能降低。
另外一种情况,就是设置的线程数大于总的逻辑CPU数:
$ cat /proc/cpuinfo| grep "processor"| wc -l
8
这样的情况下,设置更多的线程并不会提高处理速度。

小结

优点:
  • 更快,加快处理任务
  • 更强,同时处理多任务
缺点:
  • 难控制,编程困难
  • 不当使用降低性能,线程切换
  • bug难定位,资源竞争

如何创建多线程

普通的进程通常只有一个线程,称为主线程。
创建线程需要使用下面的函数:
#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*是什么玩意》)。
使用时注意包含头文件
#include <pthread.h>
,并且在链接时加上-lpthread,因此它不在libc库中。在《一个奇怪的链接问题》中提到,对于非glibc库中的库函数,都需要显式链接对应的库。
试着写一个简单的多线程程序,简单起见,我们暂时不设置任何属性,将attr字段设置为NULL:
//来源:公众号【编程珠玑】
//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;
}
编译运行:
 $ gcc -o main main.c -lpthread
 $ ./main
 main func finished
发现运行的结果并不如我们预期那样,就好像线程没有执行一样。
原因在于,如果主线程退出了,那么其他线程也会退出。所谓,皮之不存,毛将焉附,所有线程都共同使用很多资源,相关内容也可以从《对进程和线程的一些总结》中了解到。
如何改进呢?我们可以等线程执行完啊,于是,在主线程退出前sleep:
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;
}
这样就好了(注意添加头文件#include <unistd.h>)。
main func finished
thread run,value is 10
但是你会发现,main func finished可能会先打印。这也就呼应了文章标题。
但是转念一想,如果线程执行的时间超过一秒呢,难道就要sleep更长时间吗?而很多时候甚至根本不知道线程要执行多长时间,那怎么办呢?
还可以使用:
int pthread_join(pthread_t thread, void **retval);
thread是前面获得的线程id,而retval包含了线程的返回信息,假设我们完全不关心线程的退出状态,那么可以设置为NULL。
修改代码如下:
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,或者主线程退出,所有线程终止

注意

假如修改下面的代码:
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。
也就是说,创建线程的时候,传入的参数必须确保其使用这个参数时,参数没有被修改,否则的话,拿到的将是错误的值,

总结

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


相关精彩推荐

面试必问:进程和线程有什么区别?

如何让程序真正地在后台运行?

一个奇怪的链接问题


关注公众号【编程珠玑】,获取更多Linux/C/C++/数据结构与算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存