线程共享资源

1. 线程共享资源


  如果说pthread_create函数跟fork函数是对应的,一个创建线程,一个创建进程。

  但是进程调用fork创建进程的代价较高,调用的过程实际上非常复杂,即便是依靠写时复制机制,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着fork调用在时间上的开销比pthread_create函数更多(关于fork函数参考:18-用fork函数创建新进程)。

   但线程解决了这个问题,线程之所以能够方便,快速的共享数据,是因为进程调用fork创建子进程所需复制的诸多进程属性在线程间本来就是共享的,无需复制内存页,页表等(因为复制内存页和页表花费的开销不少),这对线程来说,效率提高了不少。当然这是有代价的:不过这要避免出现多个线程同时修改同一份数据的情况,这需要使用线程同步机制(这里暂时不讨论同步问题)。

图1(图片摘自《linux/unix系统编程手册》)

(如上图所示,虽然不同的线程拥有各自的栈,但是都处于同一个进程地址空间,这意味着可以通过指针来访问和修改其他线程的堆栈)代码如下:

#include <thread>
#include <iostream>

using namespace std;
int main()
{
	int n = 10;
	thread t([](int* pn)->void {
		*pn = *pn + 1;
		cout << *pn << endl;
		
	}, &n);
	t.join();

	cout << n << endl;
	return 0;
}

输出:

子线程t直接修改了main主线程栈中的临时变量n。

如果是fork出的进程,完全是两个不同的地址空间,必然不会相互影响

从图1来看,由于同一进程的多个线程共享进程的资源,比如全局内存(数据段和堆),除此之外还共享以下资源和环境:

代码文本段
打开的文件描述符
信号处理函数
当前工作目录
用户id和组id
进程id和父进程id


有些资源是每个线程各自独有一份,非共享:

线程id
用户空间栈
errno变量
信号屏蔽字
调度优先级


2. 线程共享实验——全局变量

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

//全局变量
int var = 100;

//线程主控函数
void *tfn(void *arg) {
        
        //修改全局变量var的值
        var = 200;
        printf(" create thread succesful\n");
        return NULL;
}

int main(void) {
        //主控线程第一次打印var
        printf("before pthread_create var = %d\n", var);
        pthread_t tid;
        pthread_create(&tid, NULL, tfn, NULL);
        sleep(1);
        //主控线程再次打印var
        printf("after pthread_create, var = %d\n", var);
        return 0;
}

程序执行结果:

说明线程间是共享全局变量的


3. 线程共享实验——局部变量

 既然全局变量是共享的,那么局部变量是不是也是共享的呢?为了验证这个问题,我们来看实验二

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

//线程主控函数
void *tfn(void *arg) {
        int i = (int)arg;
        //局部变量
        int temp_val = 250;
        if(i == 0){
                //线程1修改了temp_val后,打印temp_val的值
                temp_val = 100;
                printf("pthread1 , temp_val = %d\n" , temp_val);
        }else{
                //线程2打印temp_val的值
                printf("pthread2 , temp_val = %d\n" , temp_val);
        }
        printf(" create thread succesful\n");
        return NULL;
}

int main(void) {
        pthread_t tid[2];
        int i;
        for(i = 0; i < 2; i++){
                pthread_create(&tid[i] , NULL , tfn , (void *)i);
        }
        sleep(1);
        return 0;
}

程序执行结果:


  局部变量temp_val的初始值是250,在创建了2个线程,线程1修改了temp_val的值为100并打印,而线程2再访问temp_val的值还是250,这说明线程间是不共享局部变量的。


  问题来了,为什么线程间不共享局部变量,为了弄明白这个问题,我们还得回到图1。

图2

  首先,线程的生命周期一般是在线程主函数中,当线程一创建就会在用户栈开辟一块空间给线程主函数使用,这意味着我们每创建一个线程都会在用户栈开辟一块空间给线程主函数使用。如图2所示:线程1修改局部变量temp_val的值为100,实际上修改的是线程1的栈里的temp_val的值,并不会影响线程2的栈中的temp_val,所以线程2打印temp_val的值还是250,也就是说多线程不共享用户栈。


4. 线程和进程的区别

  另外之前在学习进程时,我们的理解是进程就是程序运行的执行体,而实际上进程一旦创建就自动包含了一个主线程,真正的执行体是主线程。

  那么进程是什么?我们可以把进程理解为空间上的概念,它为所有的执行体(线程)提供必要的资源(内存、文件描述符、代码等),而线程,是时间上的概念,它是抽象的、假想的、动态的指令执行过程。

  可以把进程理解为学校或学校的各种资源,线程是学校里的一个个学习的学生,学生共享学校里的各种资源。

  但是以上这些都是概念上的线程和进程的区别,如果我们要真正的本质区分进程和线程,就是看是否共享PCB进程控制块,因为进程间PCB是各自独立的,而线程间PCB是共享的。

  因为线程间是共享全局变量的,这说明了线程间的虚拟地址空间是一样的,pthread_create函数底层也复制了一份一模一样的PCB,所以说一个线程修改了数据空间的数据的话,其他线程都会因此受到影响。

       在Linux中,线程和进程没有本质区别,最后都是调用clone,但是创建调用clone时会共享一些资源,最主要的就是CLONE_VM,共享进程地址空间,图一对这一概念的描述已经很贴切了。而fork进程时会复制整个进程空间(写时复制是对直接赋值进程空间的优化)。在Linux内核中,进程和线程都是由task_struct结构体维护,某种意义上来说就是线程就是共享某些资源的“进程”。由于线程的创建是共享,而进程的创建是拷贝进程地址空间,所以创建进程的开销和时间多于创建线程。
 

猜你喜欢

转载自blog.csdn.net/songchuwang1868/article/details/89355902