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

mac2024-12-30  54

文章目录

一.线程与进程二.并发与并行三.C语言中的线程3.1创建线程 pthread_create3.2结束线程 pthread_exit3.3线程等待 pthread_join 四.结构体与多线程五.多线程的同步与互斥

一.线程与进程

二.并发与并行

三.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;第二个只需要解锁开锁两次即可,时间大大减少。

最新回复(0)