C语言--多线程学习笔记

一.线程与进程

二.并发与并行

三.C语言中的线程

我们先来看一下线程最基础的三个方法:

3.1创建线程 pthread_create

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

在创建线程的方法中含有四个参数:

  • 参数1⃣️:pthread_t *thread,一个线程变量名,被创建线程的标识 (线程的地址)
  • 参数2⃣️: const pthread_attr_t *attr,线程的属性指针,缺省为NULL即可(线程要运行的函数)
  • 参数3⃣️:void *(*start_routine)(void *), (可忽略)
  • 参数4⃣️: void *arg,要运行函数的参数

举例:

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

void* myfunc(void* args)
{
    print("Hello World!");
    return NULL;
}


int main()
{
   pthread_t th1;
   
   pthread_create(&th1,NULL,myfunc,NULL);

 return 0;
}

在terminal中编译,运行:

pc-143-1:Vim_C kevin$ gcc example1.c -lpthread -o example1
pc-143-1:Vim_C kevin$ ./example1

我们发现并未打印出相应的结果,为什么没有相应的结果???
在这里插入图片描述

注意⚠️:

  • 在写线程的时候,要在开头写#include <pthread.h>
  • 在编译时,一定要加上-lpthread (后面的-o example1,表示将文件编译为example1),要不然会报错,因为源代码里引用了pthread.h里的东西,所以在gcc进行链接的时候,必须要找到这些库的二进制实现代码。
  • pthread_create()函数中,第四个参数由于没有任何东西需要传入myfunc方法中,所以为NULL

第四个参数举例:

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


void* myfunc(void* args)
{
    int i;
    char* name = (char*) args;
    
    for(i=1;i<50;i++)
    {
      printf("%s:,%d\n",name,i);
    }

    return NULL;
}

int main()
{
   pthread_t th1;
   pthread_t th2;

   pthread_create(&th1,NULL,myfunc,"th1"); //四个参数改变了
   pthread_create(&th2,NULL,myfunc,"th2");
      
   pthread_join(th1,NULL); // 先暂时不管john函数
   pthread_join(th2,NULL);
 return 0;
}

编译,运行之后:

Output:

th1:,1
th1:,2
th1:,3
th1:,4
th1:,5
th1:,6
th1:,7
th1:,8
th1:,9
th1:,10
th1:,11
th1:,12
th1:,13
th1:,14
th1:,15
th1:,16
th1:,17
th1:,18
th1:,19
th1:,20
th1:,21
th1:,22
th1:,23
th1:,24
th1:,25
th1:,26
th1:,27
th1:,28
th1:,29
th1:,30
th1:,31
th1:,32
th1:,33
th1:,34
th1:,35
th1:,36
th1:,37
th1:,38
th1:,39
th1:,40
th1:,41
th1:,42
th1:,43
th1:,44
th1:,45
th1:,46
th1:,47
th1:,48
th1:,49
th2:,1
th2:,2
th2:,3
th2:,4
th2:,5
th2:,6
th2:,7
th2:,8
th2:,9
th2:,10
th2:,11
th2:,12
th2:,13
th2:,14
th2:,15
th2:,16
th2:,17
th2:,18
th2:,19
th2:,20
th2:,21
th2:,22
th2:,23
th2:,24
th2:,25
th2:,26
th2:,27
th2:,28
th2:,29
th2:,30
th2:,31
th2:,32
th2:,33
th2:,34
th2:,35
th2:,36
th2:,37
th2:,38
th2:,39
th2:,40
th2:,41
th2:,42
th2:,43
th2:,44
th2:,45
th2:,46
th2:,47
th2:,48
th2:,49
  • pthread_create(&th1,NULL,myfunc,"th1");中第四个参数改为“th1”,意思为将“th1”这个string传入到myfunc函数中。
  • char* name = (char*) args; 被传入的需要被强制转换为char类型,并用char类型接收

3.2结束线程 pthread_exit

线程结束调用实例:pthread_exit(void *retval); //retval用于存放线程结束的退出状态

3.3线程等待 pthread_join

pthread_join(pthread_t thread, void **value_ptr);
  • 参数1⃣️: pthread_t thread: 被连接线程的线程
  • 参数2⃣️:void **retval : 指针thread_return指向的位置存放的是终止线程的返回状态
  • 返回值: 线程连接的状态,0是成功,非0是失败

比如:pthread_join(th1, NULL);

上面的问题就可以用这个方法解决,只有当main()方法等待th1执行完之后再执行时,才能让printf成功打印参数,如下图所示:
在这里插入图片描述

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

void* myfunc(void* args)
{
    print("Hello World!");
    return NULL;
}


int main()
{
   pthread_t th1;
   
   pthread_create(&th1,NULL,myfunc,NULL);
   pthread_join(th1,NULL);  //新加代码
 return 0;
}

解析:
当调用 pthread_join() 时,当前线程(main)会处于阻塞状态,直到被调用的线程(th1)结束后,当前线程才会重新开始执行。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。
这里需要注意:被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。

四.结构体与多线程

现在我们定义一个长度为5000的int类型数组,我们希望两个线程th1与th2,th1将index为1到2500的数字相加,th2将2501-5000的数字相加,最后在求总和。

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


int arr[5000];
int s1 = 0;
int s2 = 0;

void* myfunc1(void* args)
{
    int i;
    for(i=0;i<2500;i++){
     s1 = s1 +arr[i];
    }

    return NULL;
}
void* myfunc2(void* args)
{
    int i;
    for(i=2500;i<5000;i++){
     s1 = s1 +arr[i];
    }

    return NULL;
}

int main()
{
   int i;
   for(i=0;i<5000;i++){
           arr[i]=rand()%50;
   }

   pthread_t th1;
   pthread_t th2;

   pthread_create(&th1,NULL,myfunc1,NULL);
   pthread_create(&th2,NULL,myfunc2,NULL);

   pthread_join(th1,NULL);  //新加代码
   pthread_join(th2,NULL);

   printf("s1 =%d\n",s1);
   printf("s2 =%d\n",s2);
   printf("s =%d\n",s1+s2);
 return 0;
}

编译运行:

pc-143-1:Vim_C kevin$ gcc example2.c -lpthread -o example2
pc-143-1:Vim_C kevin$ ./example2

s1 =121455
s2 =0
s =121455

我们看到myfunc1与myfunc2有大面积的重复,所以可以改进一下。
myfunc1与myfunc2唯一不同的是两个参数i的起始值与i的结束值,那将起始值与结束值传入到myfunc中,就可以合并两个为一个。

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

typedef struct{
        int first;
        int last;
        int result;
}MY_ARGS;

int arr[5000];

void* myfunc(void* args)
{
    int i;
    int s=0;
    MY_ARGS* my_args = (MY_ARGS*) args;
    int first = my_args -> first;
    int last = my_args -> last;

    for(i=first;i<last;i++){
     s = s +arr[i];
    }
    
    my_args -> result = s;
    
    return NULL;
}


int main()
{
   int i;
   for(i=0;i<5000;i++){
           arr[i]=rand()%50;
   }

   pthread_t th1;
   pthread_t th2;

   MY_ARGS args1 ={0,2500,0};
   MY_ARGS args2 ={2500,5000,0};

   pthread_create(&th1,NULL,myfunc,&args1);
   pthread_create(&th2,NULL,myfunc,&args2);

   pthread_join(th1,NULL);  
   pthread_join(th2,NULL);

   int s1 = args1.result;
   int s2 = args2.result;
   printf("s1= %d \n",s1);
   printf("s2= %d \n",s2);
   printf("s1+s2= %d \n",s1+s2);
   
   return 0;
}

编译运行:

pc-143-1:Vim_C kevin$ gcc example2.c -lpthread -o example2
pc-143-1:Vim_C kevin$ ./example2
s1= 60241 
s2= 61214 
s1+s2= 121455 

解析:
首先定义了一个MY_ARGS的结构体,然后先在main中给args1与args2分别赋值,将args1与args2当作参数传入pthread_create方法中,注意这里应该传入args1,2的地址,因为void* myfunc(void* args)中接收的参数类型为一个指针
传入myfunc方法后,MY_ARGS* my_args = (MY_ARGS*) args;先将void* args强制转换为(MY_ARGS*) args并用MY_ARGS*类型的my_args接收,再将my_args中的参数分别取出
int first = my_args -> first;int last = my_args -> last;,最后将s赋值给my_args中的resultmy_args -> result = s;

五.多线程的同步与互斥

不加锁,数据不同步

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

int s =0;

void* myfunc(void* args){
        int i=0;
        for(i=0;i<100000;i++){
                s++;
        }
        return NULL;
}

int main(){

        pthread_t th1;
        pthread_t th2;

        pthread_create(&th1,NULL,myfunc,NULL);
        pthread_create(&th2,NULL,myfunc,NULL);

        pthread_join(th1,NULL);
        pthread_join(th2,NULL);

        printf("s = %d \n",s);
        return 0;
}

上述的程序,创建了两个线程th1和th2,分别执行s++,最后应该累加到200000,但是结果却跟想象中不同:

编译,运行:

pc-143-1:Vim_C kevin$ gcc example4.c -lpthread -o example
pc-143-1:Vim_C kevin$ ./example
s = 136961 

pc-143-1:Vim_C kevin$ ./example
s = 120942 

pc-143-1:Vim_C kevin$ ./example
s = 105661 

pc-143-1:Vim_C kevin$ ./example
s = 183177 

pc-143-1:Vim_C kevin$ ./example
s = 106221 

原因是什么呢?

因为,s++,实际是三步操作,首先读取s,然后s+1,最后赋值,然而两个线程没有锁,并不知道彼此进行到哪一步了,有可能线程1在读取s的时候,线程2就已经执行第三步赋值了,所以导致了数据不同步,一句话就是不加锁,数据不同步

  • 在主线程中初始化锁为解锁状态
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, NULL);
  • 在编译时初始化锁为解锁状态
    锁初始化 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
  • 访问对象时的加锁操作与解锁操作
    加锁 pthread_mutex_lock(&mutex)
    释放锁 pthread_mutex_unlock(&mutex)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

pthread_mutex_t lock;

int s =0;

void* myfunc(void* args){
        int i=0;
        for(i=0;i<100000;i++){
                pthread_mutex_lock(&lock);  //上锁
                s++;
                pthread_mutex_unlock(&lock);  //解锁
        }
        return NULL;
}

int main(){

        pthread_t th1;
        pthread_t th2;
        pthread_mutex_init(&lock,NULL);

        pthread_create(&th1,NULL,myfunc,NULL);
        pthread_create(&th2,NULL,myfunc,NULL);

        pthread_join(th1,NULL);
        pthread_join(th2,NULL);

        printf("s = %d \n",s);
        return 0}

编译运行后:

pc-143-1:Vim_C kevin$ gcc example3.c -lpthread -o example
pc-143-1:Vim_C kevin$ ./example
s = 200000 

pc-143-1:Vim_C kevin$ ./example
s = 200000 

pc-143-1:Vim_C kevin$ ./example
s = 200000 

我们运行了三次都为理想状态下的20000。

解析:
首先声明了pthread_mutex_t lock;,在mian函数中pthread_mutex_init(&lock,NULL);先对锁初始化,第一个参数传入锁的地址,第二个参数为NULL即可。

我们测试一下运行时间:

pc-143-1:Vim_C kevin$ time ./example3
s = 200000 

real	0m0.263s
user	0m0.007s
sys	0m0.007s

然后将锁的位置变化一下再计算时间:

int s =0;

void* myfunc(void* args){
        pthread_mutex_lock(&lock);
        
        int i=0;
        for(i=0;i<100000;i++){
                s++;
        }
        
        pthread_mutex_unlock(&lock);
        return NULL;
}
pc-143-1:Vim_C kevin$ time ./example3
s = 200000 

real	0m0.005s
user	0m0.002s
sys	0m0.002s

锁的位置决定了运算效率,第一个需要每个循环不断开锁解锁,循环200000;第二个只需要解锁开锁两次即可,时间大大减少。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值