Ubuntu下多线程编程暨形参传递所遇见的问题

  最近在学习Linux下的多线程编程,现在分享一下自己的心得。

一、pthreate_create函数

  创建一个新线程需要使用到函数pthread_create函数,在Linux终端输入 man pthread 可以得到函数原型和一些其他信息。函数原型为:

 #include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

//attention:Compile and link with -pthread.

参数解释:

  1、pthread_t *thread:pthread_t实际上是unsigned long int型,thread会存储新线程的ID号,在”主线程“中成功创建新线程后,该函数会在主线程中返回0,在新线程中会将新线程的ID号保存到thread变量中。

   2、const pthread_attr_t *att:pthread_attr_t是一个结构体,原型如下(参考自博文:https://blog.csdn.net/dela_/article/details/52108063):

typedef struct 
{ 
    int detachstate;//线程的分离状态 
    int schedpolicy;//线程的调度策略 
    struct sched_param schedparam;//线程的调度参数 
    int inneritsched;//线程的继承性 
    int scope;//线程的作用域 
    size_t guardsize;//线程栈末尾的警戒缓冲区大小 
    int stackaddr_set;//线程堆栈的地址集 
    void *stackaddr;//线程栈的位置 
    size_t stacksize;//线程栈的大小 
}pthread_attr_t;

如果设置为NULL,表示使用默认属性。

  3、void *(*start_routine) (void *):这是一个函数指针,是新线程执行的起始地址。void* 表示“无类型”、“空”,可以用于任意类型(参考博文:https://www.cnblogs.com/pengyingh/articles/2407267.html)。不能在指定start_routine函数时传递形参(而是最后一个参数:arg),因为参数部分被强制转换为了“空”。我认为这里使用两个void是为了一种万能用法,让任意类型的函数都可以成为新线程执行的起始函数。用法举例:

int fn( void *_pi)
{
    ...
}

int main()
{
    pthread_t thread=0;
    pthread_create(&thread,NULL,(void *)(fn),**);//**表示这个参数暂时不说明,下文会有介绍
    //上一句代码也可以为
    //pthread_create(&thread,NULL,(void *)(&fn),**);//“&”这个符号有的书推荐使用,而有的书又说没必要使用。
                                                   //而我认为如果把函数名理解为函数首地址的话是没必要使用“&”符号的。
}        

  4、void *arg:同样,这个void*表示的是任意类型。如果start_routine函数只有一个形参,则可以直接传递。如:

int fn(void *_pi)
{
    int *i= (int*)_pi;
    ...
}
int main()
{
    ...
    int i=0;
    pthread_create(***,***,(void*)(fn),&i);
    ...
}

而如果start_routine函数有多个形参的话,就需要用到结构体了,具体使用有如下参考:

typedef struct
{
    int i;
    char j;
}Param;


int fn(void *_pi)
{
    Param *P_fn= (Param*)_pi;
    printf("i=%d, j=%c\n",p_fn->i,P_fn->j);
    ...
}


int main()
{
    ...
    Param P;
    P.i=1;
    P.j='a';
    pthread_create(***,***,(void*)(fn),&P);
    ...
}

注意事项:

  1、pthread_create函数需要头文件:"pthread.h"。

  2、由于pthread库不是Linux系统默认的库,连接时需要使用库libpthread.a,所以在编译时要加-pthread或-lpthread参数,这一点Linux文档也有给出(在终端输入man pthread 即可看到)。编译命令:gcc -pthread -o *** ***.c

 

二、问题及解决过程

  本次程序的主要内容是在主线程的基础上,再创建了两个新线程。在新线程里每隔两毫秒打印一次数据,一共打印20次。数据是从主线程通过结构体传递进来的。而且这个结构体是两个线程共用的,即在第一个线程创建完成之后,修改这个结构体成员变量的值,再传递给第二个线程,或许有人会问,为什么不创建两个结构体?因为在实际应用中会新建很多个线程,我是想在不得不定义多个变量之前寻求一种更高效的方法。

程序如下:

#include "stdio.h"
#include <pthread.h>
#include <unistd.h>
#include <stddef.h>
#include "sys/time.h"
#include "time.h"


//传递形参的结构体
typedef struct
{
    pthread_t thread;//线程的ID
    int value;
}Param;


//毫秒级延时函数
void delay_ms(int _ms)
{
    struct timeval ms_delay;
    ms_delay.tv_sec=0;// 0秒
    ms_delay.tv_usec=_ms*1000;//usec表示微秒
    select(0,NULL,NULL,NULL,&ms_delay);
}


//新线程起始函数
void new_thread(void *_P)
{
    Param *Pfn=(Param*)(_P);//类型转换

    int i=0;
    for(i=1;i < 20; i++)
    {
        printf("  Pfn->value=%d, threadID:%lx, Pfn->thread:%lx, i=%d\n",Pfn->value,pthread_self(),Pfn->thread,i);//pthread_self()表示获取当前线程的ID
        delay_ms(2);
    }

    int retval=0;
    pthread_exit(&retval);//退出线程
}


int main()
{	
    printf("  main thread ID:%ld\n",pthread_self());//pthread_self()表示获取当前线程的ID
	
    Param Para;//定义一个传递形参的结构体

    Para.value=1;
    pthread_create(&Para.thread,NULL,(void*)(new_thread),&Para);//创建新线程1

    printf("  更改Para\n");
    Para.value=2;//在线程1创建完成之后更改结构体的值,以便传给线程2
    pthread_create(&Para.thread,NULL,(void*)(new_thread),&Para);//创建新线程2

    sleep(3);//等待线程结束
    return 0;
}

运行的结果为:

  第一列打印出来的是Param结构体的value数值,这些值全部都变成了2;第二列是线程的ID,从数据中可以看出线程之间确实是切换了的,这也就从侧面印证了还会有系统自身的变量来记录线程的ID,并以此为根据来切换线程;第三列打印的是Param结构体里记录的线程ID,发现它全部变成了线程2的ID;第四列打印的是for循环的变量i,发现会有两个相同的i值在一起,这也可以说明两个线程都有被执行。

  现在的问题是只要是与Param结构体有关的数据都被新值覆盖了。随后我进行了下面的分析(如果想直接看结果可以跳转到后面)。

  1、我当时的想法是既然线程1的new_thread函数中的Pfn结构体已经记录好了主线程结构体 para 的参数,那么就应该不会再受到它的影响,就算后面在主线程中更改结构体Para的值也不会对线程1造成影响的。但是从结果来看后面的线程影响到了线程1,但是具体在哪个位置引起的还不知道,只能感觉到与Para结构体有关,因为只有这个结构体与这两个线程有关。

  2、后来我在主程序的“printf("  更改Para\n");”之前加了一句代码:“delay_ms(100);”。然后执行的结果是:

发现打印出来的值是正常的,但是严格来说这并不是真正意义上的并行多线程,只是两个线程顺序执行。但通过这个现象,我想到了一个可能的原因:系统创建一个新线程是需要时间的,当主线程使用pthread_create()函数后,系统就会为新线程准备它所需的资源,而主线程则会继续执行,如果在系统还没有为新线程准备好资源之前,结构体Para的值就已经发生了改变,那么在新线程开始后它将会得到最新的Para参数。

3、依据2中的推断,我进一步将主函数里delay_ms的参数由100削减为20,这样既能保证线程1可以执行一段时间,又能保证线程1和线程2会同时执行。执行后得到如下结果:

发现刚开始Pfn->value的值是1,在主线程中更改了Para的值之后,不管是哪个线程,Pfn->value的值全是2。这就说明虽然线程1已经接收到了Para的参数,但是在主线程修改Param的成员值之后,还是对线程1有影响。这就表示线程1的参数值被覆盖的原因不仅仅是系统准备时间不足,还有其他原因。

   4、后来在网上找到了一篇博客:https://blog.csdn.net/a_ran/article/details/54973176,开篇有段类似于下面的代码:

else if(***)
{
    ...
    struct test;
    test.i=10;
    pthread_create(***,***,(void *)fn,&test);
    ...
}

  他说这个test结构体是个局部变量,当退出else if 之后,再次进入子线程之后,然后子线程会因为无法找到test所在的地址而导致出错。

  然后我就想是不是因为系统每次切换到子线程会把值重新传一遍,这样也能解释为什么线程1的值会被覆盖。然后我又仔细的看了一下new_thread函数的代码,发现了导致错误的真正原因在new_thread函数的第一句代码  “Param *Pfn=(Param*)(_P);”  和pthread_create函数的最后一个参数传递方式(地址传递)上,它是通过形参*_P把Para结构体的起始地址传给了Pfn。这就是说(&Para)、_P、Pfn三者指向的是同一片内存区域,只要三者中有任意一个发生了变化,另外二者就会随之改变。

  我分析了一下我会犯这个错误的原因:首次接触到多线程编程,脑中还没有形成相应的编程概念。如果调用这种以指针为形参的函数发生在单线程中,那它肯定是在执行完这个函数并且返回之后再继续执行主函数,这样的话就算调用完子函数之后再更改Para的值,也不会对子函数造成影响,因为它已经执行完了。然而在多线程中则不是,当你创建了一个新线程并且调用pthread_create函数之后,并不是这个程序执行完再返回,而是几个线程同时执行(实际上是这个线程执行一段时间,再切换到另外一个线程,再切换回来,给我们的感觉就是同步并行的),这样的话,要是这个函数的形参是指针,那么当一个线程将该地址上的的内容做出更改之后,其他的线程也是会受影响的,这就是线程同步的问题。

  解决的方法是修改new_thread函数,我在new_thread函数中定义了一个Param结构体变量:Psave,在进入该线程之后就将值保存到Psave中,然后在new_thread函数的后续代码中使用Psave,而不是使用指针Pfn,这样就不会有影响了。修改完的new_thread函数如下:

//新线程起始函数
void new_thread(void *_P)
{
    Param *Pfn=(Param*)(_P);//类型转换

    Param Psave;
    Psave.thread = Pfn->thread;
    Psave.value = Pfn->value;

    int i=0;
    for(i=1;i < 20; i++)
    {
        printf("  Psave.value=%d, threadID:%lx, Psave.thread:%lx, i=%d\n",Psave.value,pthread_self(),Psave.thread,i);//pthread_self()表示获取当前线程的ID
        delay_ms(2);
    }

    int retval=0;
    pthread_exit(&retval);//退出线程
}

然后执行的结果为:

  看似这个问题解决了,但是需要注意一点,也是上文中提到的,在主线程使用pthread_create函数之后,系统是需要准备时间的,所以说,仍要确保线程1已经执行后才能修改Para的值。如果现在把主函数的“delay_ms(20);”语句注释掉,那么线程1的形参值也将会是修改完Para之后的。

  因此我的老师是建议我使用链表或者队列来管理每个线程,然而我还是认为使用一个变量简洁、容易管理,因为也会有系统变量来记住每个线程,为什么我还要自己弄一个呢?

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值