Linux多线程

Posted 雨轩(爵丶迹)

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux多线程相关的知识,希望对你有一定的参考价值。

线程

一、Linux线程概念

1、什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程(线程是进程的一个执行分支)
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

1、我们知道一个进程是由进程控制块(task_struct)、进程地址空间(mm_struct)、页表组成的,进程地址空间跟物理内存通过页表建立映射关系。
2、每个进程都有独立的进程控制块,进程地址空间,页表,也就意味着进程具有独立性。

以下就是我们看到的一个完整的进程:

线程的话我们只用创建进程控制块,所有线程共享进程地址空间。

之前所说的进程是单线程进程,因为只有一个task_struct,可任务是单线程进程。现在我们看到的是多个task_struct,多个线程共享一个进程的地址空间,每个线程都是一个执行流。

现在的进程在CPU眼里虽然还是PCB,但是比传统的进程更加轻量化了。在Linux中,站在CPU的角度,不能识别当前调度的task_struct是进程还是线程。CPU只关心一个一个的独立执行流。无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的。


再次站在OS的角度:
进程:是承担分配系统资源的基本实体
线程:是调度的基本单位,线程是进程里面的执行流(线程在进程地址空间运行)

但我们需要知道是,Linux中没有真正的线程,线程是用进程模拟出来的,对应的线程控制块也是进程的PCB。也就意味着Linux中也没有真正意义上的线程相关的系统调用。但是Linux有提供创建轻量级进程的接口,也就是创建进程,共享空间,其中最典型的代表就是vfork函数。

vfork函数的功能就是创建子进程,但是父子共享空间。

pid_t vfork(void);

vfork函数的返回值与fork函数的返回值相同:

  • 返回给父进程子进程的PID。
  • 给子进程返回0。

只不过vfork函数创建出来的子进程与其父进程共享地址空间,简单来说就是共享临界资源(后面讲临界资源)。

#include <iostream>
using namespace std;
#include <unistd.h>
int val = 100;
int main()

    pid_t pid = vfork();//创建子进程,共享进程地址空间
    if(pid == 0)
    
        val = 200;
        cout << "child pid: " << getpid() << "  ppid: " << getppid() << "  this is child's value: " << val << endl;
        exit(0);
    
    sleep(1);
    cout << "father pid: " << getpid() << "  ppid: " << getppid() << "  this is child's value: " << val << endl;

    return 0;

父进程休眠1秒后,再次读取到对应的值,说明父子进程共享进程地址空间。

2、原生线程库pthread

在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。

原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口。

因此对于我们来讲,在Linux下学习线程实际上就是学习在用户层模拟实现的这一套接口,而并非操作系统的接口。

3、线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

概念说明:
1、计算密集型:执行流的大部分任务,主要以计算为主。比如加密解密、大数据查找等。
2、 IO密集型:执行流的大部分任务,主要以IO为主。比如刷磁盘、访问数据库、访问网络等。

4、线程的缺点

性能损失

  • 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

健壮性降低

  • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制

  • 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

编程难度提高

  • 编写与调试一个多线程程序比单线程程序困难得多

5、线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制(信号的发送单位是进程),终止进程,进程终止,该进程内的所有线程也就随即退出

6、线程的用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

二、Linux进程VS线程

1、进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据:线程ID、一组寄存器、栈、errno、信号屏蔽字、调度优先级

2、进程的多个线程共享

同一地址空间,因此代码段(Text Segment)、数据段(Data Segment)都是共享的

  • 如果定义一个函数,在各线程中都可以调用
  • 如果定义一个全局变量,在各线程中都可以访问到

除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程关系图如下:

3、进程和线程的区别

线程与进程的区别:

  • 一个线程从属于一个进程;一个进程可以包含多个线程。
  • 一个线程挂掉,对应的进程挂掉;一个进程挂掉,不会影响其他进程。
  • 进程是系统资源调度的最小单位;线程CPU调度的最小单位。
  • 进程系统开销显著大于线程开销;线程需要的系统资源更少。
  • 进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组
  • 进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
  • 通信方式不一样。
  • 进程适应于多核、多机分布;线程适用于多核

三、POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

1、线程创建

功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码

错误检查:

  1. 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
  2. pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
  3. pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小

我们来看看下面的代码,创建了2个线程,5秒后退出

    1 #include <iostream>
    2 #include <cstdio>
    3 #include <pthread.h>
    4 #include <unistd.h>
    5 using namespace std;
    6 #define NUM 2
    7 pthread_t main_thread;
    8 //void*是系统层面设计的一个通用接口                                                                                                                                             
    9 void *ThreadRoutine(void *args)    
   10                               
   11     //暂时的方案
   12     int i = *(int*)args;
   13     delete (int*)args;  
   14     int cnt = 0;      
   15     while( cnt < 5 )
   16         cout << "thread index : " << i << " count : " << cnt << "thread id: " << pthread_self() <<  endl;
   17         sleep(1);                                                                                        
   18         cnt++;   
   19              
   20     cout << "thead :" << i << " quit" << endl;
W> 21                                              
   22 int main()
   23          
   24     main_thread = pthread_self();
   25     pthread_t tids[NUM];         
   26     for(auto i = 0; i < NUM; i++)
   27         int *p = new int(i);    //传递参数  
   28         pthread_create(tids+i, nullptr, ThreadRoutine, p);
   29         cout << "create thread : " << tids[i] << "success" << endl;
   30      //通过死循环等待线程执行完毕                                                          
   31     while(true)
   32         cout << "main thread is running...: main thread id : " << pthread_self() << endl;
   33         sleep(1);                                                                        
   34                 
   35     return 0;
   36             


通过运行结果,我们可以看到线程创建了然后一直在运行,后面退出,这里我们通过主函数死循环等待,才能看到这样的结果,其中pthread_self()是线程对应的地址。

2、线程等待

在前面的进程中,我们知道创建一个子进程之后,父进程是需要等待的,否则子进程成为僵尸进程,父进程都不知道,后面也就无法处理,造成内存泄漏等问题,所以子线程同样需要等待。等待原因总结如下:

线程等待原因:

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join原型:

功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_CANCELED
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传pthread_exit的参数
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数

这里的PTHREAD_CANCELED我们不知道具体是多少,我们通过命令grep -ER"PTHREAD_CANCELED" /usr/include/查看,发现这个常数就是 -1。

[dy@VM-12-10-centos thread]$ grep -ER "PTHREAD_CANCELED" /usr/include/
/usr/include/pthread.h:#define PTHREAD_CANCELED ((void *) -1)
/usr/include/pthread.h:   the thread as per pthread_exit(PTHREAD_CANCELED) if it has been

我们上代码,来看看pthread_join后的现象。

#include <iostream>
    2 #include <cstdio>
    3 #include <pthread.h>
    4 #include <unistd.h>
    5 using namespace std;
    6 #define NUM 2
    7 pthread_t main_thread;
    8 //void*是系统层面设计的一个通用接口
W>  9 void *ThreadRoutine(void *args)
   10 
   11     //暂时的方案
   12    // int i = *(int*)args;
   13     //delete (int*)args;
   14     int cnt = 0;
   15     while( cnt < 5 )
   16         //cout << "thread index : " << i << " count : " << cnt << "thread id: " << pthread_self() <<  endl;
   17         cout << "thread id: " << pthread_self() << endl;
   18         sleep(1);
   19         cnt++;
   20     
   21     cout << "Thread quit" << endl;                                                                                                                      
W> 22 
   23 int main()
   24 
   25     main_thread = pthread_self();
   26     pthread_t tids[NUM];
   27     for(auto i = 0; i < NUM; i++)
   28        // int *p = new int(i);
   29         pthread_create(tids+i, nullptr, ThreadRoutine, nullptr);
   30         cout << "tid : " << tids[i] << endl;
   31     
   32     for(auto i = 0; i < NUM; ++i)
   33     
   34       //设置为nullptr,不关心create的返回值
   35       if(0 == pthread_join(tids[i], nullptr))
   36       
   37         cout << "thread " << i << "tid: " << tids[i] << "join success" << endl;
   38       
   39	  return 0;
   40 


我们在看看如何拿到线程退出的退出码,方便观察,返回值设置成了2001。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>

void* Routine(void* arg)

	char* msg = (char*)arg;
	int count = 0;
	while (count < 5)
		printf("I am %s...pid: %d, ppid: %d, tid: %lu\\n", msg, getpid(), getppid(), pthread_self());
		sleep(1);
		count++;
	
	return (void*)2001;

int main()

	pthread_t tid[5];
	for (int i = 0; i < 5; i++)
		char* buffer = (char*)malloc(64);
		sprintf(buffer, "thread %d", i);//字符串写入sprintf中
		pthread_create(&tid[i], NULL, Routine, buffer);
		printf("%s tid is %lu\\n", buffer, tid[i]);
	
	printf("I am main thread...pid: %d, ppid: %d, tid: %lu\\n", getpid(), getppid(), pthread_self());
	for (int i = 0; i < 5; i++)
		void* ret = NULL;
		pthread_join(tid[i], &ret);//第二个参数是一个二级指针,这里我们要传一级指针
		printf("thread %d[%lu]...quit, exitcode: %d\\n", i, tid[i], (int)ret);
	
	return 0;



pthread_join阻塞的流程可以看下面这张图

上面讨论的都是线程正常的情况,如果发生异常会怎么样呢?进程中我们知道,进程exit code中,是退出码+信号,进程退出有三种情况:

  1. 代码跑完,结果正常
  2. 代码跑完,结果不正常
  3. 代码没有跑完,程序崩溃

前两者是返回退出码,后者是发送信号,那在主函数中thread_join的时候,需不需要考虑线程崩溃的问题?

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <pthread.h>
    4 #include <unistd.h>
    5 #include <sys/types.h>
    6 
    7 void* Routine(void* arg)
    8 
    9   char* msg = (char*)arg;
   10   int count = 0;
   11   while (count < 5)
W> 12     int a = 2 / 0;                                                                                                                                      
   13     printf("I am %s...pid: %d, ppid: %d, tid: %lu\\n", msg, getpid(), getppid(), pthread_self());
   14     sleep(1);
   15     count++;
   16   
   17   return (void*)2001;//退出码,退出成功则接受到对应的线程退出码
   18 
   19 int main()
   20 
   21   pthread_t tid[5];
   22   for (int i = 0; i < 5; i++)
   23     char* buffer = (char*)malloc(64);
   24     sprintf(buffer, "thread %d", i);
   25     pthread_create(&tid[i], NULL, Routine, buffer);
   26     printf("%s tid is %lu\\n", buffer, tid[i]);
   27   
   28   printf("I am main thread...pid: %d, ppid: %d, tid: %lu\\n", getpid(), getppid(), pthread_self());
   29   for (int i = 0; i < 5; i++)
   30     void* ret = NULL;
   31     pthread_join(tid[i], &ret);
E> 32     printf("thread %d[%lu]...quit, exitcode: %d\\n", i, tid[i], (int)ret);
   33   
   34   return 0;
   35 

看到这个结果,我们就知道join的时候是不需要考虑线程崩溃的问题的。前一节我们也谈到过,一个线程崩溃,整个进程也会随着崩溃,其他线程根本来不及反应,这里也反映了多线程下的代码问题,一旦代码出现差错,是很难去调试发现的,所以编写代码的时候一定要格外小心注意!!!

3、线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
  2. 线程可以调用pthread_ exit终止自己。
  3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

pthread_ exit原型

功能:线程终止
原型
void pthread_exit(void *value_ptr);
参数
value_ptr:value_ptr不要指向一个局部变量。
返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#define NUM 5
using namespace std;
void *Rountine(void* args)

  int cnt = 5;
  int *<

以上是关于Linux多线程的主要内容,如果未能解决你的问题,请参考以下文章

C++多线程同步技巧 --- 临界区

RT-Thread多线程导致的临界区问题

windows 和 linux 多线程

多线程程序的临界区

Linux 多线程:线程安全之同步与互斥的理解

互斥锁和临界区有啥区别?