文章目录
- 一.线程与进程
- 二.并发与并行
- 三.C语言中的线程
- 3.1创建线程 pthread_create
- 3.2结束线程 pthread_exit
- 3.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;第二个只需要解锁开锁两次即可,时间大大减少。