Pthread多线程编程

POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。

在pthread线程库中我们常用到的有这些:

Pthread线程库中创建线程的函数原型:

1
int pthread_create(pthread_t *thread,pthread_attr_t *attr,void *(*func)(void*),void *arg)

一份简单的多线程代码,我将通过分析几份代码,来逐步总结多线程用到的知识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#define NUM 5
void *print(void *m){
char *cp=(char*)m;
for(int i=0;i<NUM;++i){
printf("%s\n",cp);
// 清除输入缓冲区
fflush(stdout);
sleep(1);
}
return NULL;
}
int main(int argc,char* argv[])
{
pthread_t t1,t2;
pthread_create(&t1, NULL, print, (void *)"t1 thread");
pthread_create(&t2, NULL, print, (void *)"t2 thread");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

注意:编译时必须要使用-lpthread参数。如:clang++ -o pthread.exe pthread.cc -lpthread

使用makefile会更方便一些,我在这里使用的makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
# compile pthread.c
CC=clang
CBSJ=pthread.c
EXXX=pthread.exe
CANSHU=-std=c99
LPTHREAD=-lpthread
WARING=-W

start:$(OBJS)
$(CC) -o $(EXXX) $(CBSJ) $(CANSHU) $(LPTHREAD) $(WARING)
clean:
rm -f $(EXXX)

编译运行的结果为:
01

下面来分析一下代码中用到的pthread线程库中的几个函数:

pthread_create创建一个新线程
头文件pthread.h
函数原型int pthread_create(pthread_t thread,pthread_attr_t attr,void *(*func)(void*),void *arg)
参数解析thread——指向pthread_t类型变量的指针
attr——指向pthread_attr_t类型变量的指针,或者为NULL
func——指向新线程所运行函数的指针
arg——传递给func的参数
返回值0——成功返回
errcode(非0错误代码)——错误

pthread_create函数创建了一个新的执行线程,在此新的线程内调用了func(arg)。新线程的属性有attr参数来指定。如果attr值为NULL则线程使用默认属性。func是一个函数,它接收一个指针作为它的参数,并且运行结束后返回一个指针。参数和返回值都被定义为类型为void的指针,以允许他们指向任何类型的值。

pthread_join等待某线程终止
头文件pthread.h
函数原型int pthread_join(pthread_t thread,void **retval)
参数thread——所等待的线程
retval——指向某存储线程返回值的变量
返回值0——成功返回
errcode(非0错误代码)——错误

pthread_join使得调用线程挂起直至由thread参数指定的线程终止。如果retval不是NULL,线程的返回值就将存储在由retval指向的变量中。

当线程终止时,pthread_join函数返回0,如果有错误发生,则返回一个非零错误代码。如果某线程试图等待一个并不存在的线程、多个线程同时等待一个线程返回或者线程试图等待自己都将导致函数返回一个错误代码。

进程间可以通过管道socket信号退出以及运行环境来进行会话。线程间的通信也很容易。多个线程在一个单独的进程中运行,共享全局变量,因此线程间可以通过设置和读取这些全局变量来进行通信。不过要知道,对共享内存的访问可是线程的一个既有用又危险的动作。

考虑如下代码,两个线程共同操作一个全局变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define NUM 5
// 创建一个全局变量,供多个线程访问
int counter=0;
void *print_counter(void* m){
for (int i = 0; i < NUM; ++i){
// 输出counter的值
printf("count = %d\n",counter);
sleep(1);
}
return NULL;
}
int main(int argc,char* argv[])
{
pthread_t t1;
// 创建一个线程,调用print_counter函数且不传递给函数具有意义的值
pthread_create(&t1, NULL, print_counter, NULL);
// 初始线程中对counter的访问
for (int i = 0; i < NUM; ++i){
// 对counter累加
counter++;
sleep(1);
}
pthread_join(t1, NULL);
return 0;
}

上面的代码中使用了两个线程.初始化线程执行了一个循环来使计数器每秒钟增加1.初始线程在进入循环之前,创建了一个新的线程.新的线程运行了一个函数来将counter的值打印出来,.main函数和print_counter运行在同一个进程中,所有都具有对于counter的访问权限.

上面的代码中,线程的运行顺序是不可预料的.从运行结果中就可以看出来.

02

可以看出来,有时候是可以按照我们预料的方式执行,但是有时候则不会.

这是由于线程之间的调度不是一定要执行过初始化线程之后再执行我们创建的线程,有可能我们执行初始化线程之前,调用了X次我们创建的线程t1,也有可能,我们执行了初始化线程之后又执行了Y次t1线程,所以,输出的顺序是不一定的.

03

在不使用sleep的情况下的运行结果是这样的:

04

下面一份代码,使用两个线程统计两个文件中所有单词的个数:

统计样本:

1
2
3
4
5
// test1.txt is 14 words
I need to follow my heart.
In order to see more of the world.
// test2.txt is 4 words
My name is zhalipeng.

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <ctype.h>

void* count_words(void *f);

// 计数器
int total_words=0;
// 创建互斥量,确保total_words安全地被连个线程读写.
pthread_mutex_t counter_lock=PTHREAD_MUTEX_INITIALIZER;
int main(int argc,char* argv[])
{
pthread_t t1,t2;
if(arg!=3){
printf("usage:%s file1 file2\n",args[0]);
exit(1);
}
pthread_create(&t1, NULL, count_words,(void*)args[1]);
pthread_create(&t2,NULL,count_words,(void*)args[2]);
pthread_join(t1, NULL);
pthread_join(t2,NULL);
printf("%5d total words\n",total_words);
return 0;
}

void* count_words(void *f){
char *filename=(char*)f;
FILE *fp;
int c,prevc='\0';
if((fp=fopen(filename, "r"))!=NULL){
while((c=getc(fp))!=EOF){
// 碰到非字母和数字的字符跟在数字和字符之后就把这个字母或数字当做单词的结尾
if(!isalnum(c)&&isalnum(prevc)){
pthread_mutex_lock(&counter_lock);
total_words++;
pthread_mutex_unlock(&counter_lock);
}
// printf("%c",c);
prevc=c;
}
fclose(fp);
}else{
perror(filename);
}
// printf("\n");
return NULL;
}

运行结果:

05

也可以在text1.txt或者test2.txt中添加数据:

1
2
3
4
// test2.txt is 11 words
My name is zhalipeng.
I love my family.
I love programing!

06

如果输出线程读取两个文件的顺序的话,与上一份代码一样,也是乱序的,哈哈.我为什么老是关注这些东西.

07

上面的代码中使用了几个新的pthread接口,分别是:pthread_mutex_lock还有pthread_mutex_unlock,用来确保共享全局变量在某一时刻之后一个线程可以读写.

举个简单的例子:在机场或者公交终点站的公共存储柜始终是打开的,除非有人在里面存放了东西。当一个人在里面放了东西拿到钥匙之后,便没有人再能够打开那个柜子了。只有等到这个人归还了钥匙把柜子打开之后,这样其他人才可以使用这个柜子。

类似的,如果两个线程需要安全地共享一个公共的计数器,他们也需要这样一种方法把变量加锁,即在某一时刻只能有一个线程读写该计数器。

线程系统包含了称为互斥锁的变量,它可以使线程间更好的合作,避免对于变量、函数以及资源的访问冲突。

pthread_mutex_lock等待互斥锁解开然后锁住互斥量
pthread_mutex_unlock给互斥量解锁
头文件pthread.h
函数原型int pthread_mutex_lock(pthtead_mutex_t *mutex)
int pthread_mutex_unlock(pthtead_mutex_t *mutex)
参数mutex:指向互斥锁对象的指针
返回值0:成功返回
errcode(非0错误代码):错误

首先,定义一个pthread_mutex_t类型的全局变量count_lock,然后赋值给它一个初值.然后可以把对全局变量计数器total_words的操作夹在pthread_mutex_lockpthread_mutex_unlock的调用之间.

1
2
3
pthread_mutex_lock(&counter_lock);
total_words++;
pthread_mutex_unlock(&counter_lock);

现在两个线程可以安全地共享计数器了.当一个线程调用pthread_mutex_lock的时候,如果另一个线程已经将这个互斥量锁住了,那这个线程就会被阻塞等待着这个锁被另一个线程解开,然后才可以对计数器进行操作.每个线程对计数器进行操作之后都将互斥量解锁,以便共享的数据被其他的进程操作.

任何数目的线程都可以挂起等待互斥量解锁.当一个线程对互斥量解锁之后,系统就将控制权交给等待的某一个线程.

Upgrade code:

  • 可以接收不定个数的文件
  • 通过arg获取到的参数个数来决定创建多少个线程和多少个struct对象(存储文件信息)
  • 通过struct来记录单个文件中单词的个数(每个文件具有一个单独的计数器)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <stdio.h>
#include <pthread.h>
#include <ctype.h>
#include <stdlib.h>

void* count_words(void *f);

//创建一个结构体,用于存储文件的信息
struct arg_set{
// 存储文件名和计数器
char *filename;
int total_words;
};

pthread_mutex_t counter_lock=PTHREAD_MUTEX_INITIALIZER;
int main(int argc,char* argv[])
{
const int NUM=arg-1;
// 创建NUM(接受的文件个数)个线程
pthread_t thread[NUM];
// 创建NUM(接受的文件个数)个结构体对象
struct arg_set argSet[NUM];
for (int i = 0; i < NUM; ++i){
argSet[i].filename=args[i+1];
argSet[i].total_words=0;
pthread_create(&thread[i], NULL, count_words,(void*)&argSet[i]);
pthread_join(thread[i],NULL);
printf("File %s in %d words\n",argSet[i].filename,argSet[i].total_words);
}
int sumWords=0;
for (int i = 0; i < NUM; ++i){
sumWords+=argSet[i].total_words;
}
printf("The total word count is %d\n",sumWords);
return 0;
}

void* count_words(void *f){
struct arg_set *args=(struct arg_set*)f;
FILE *fp;
int c,prevc='\0';
if((fp=fopen(args->filename, "r"))!=NULL){
while((c=getc(fp))!=EOF){
// 碰到非字母和数字的字符跟在数字和字符之后就把这个字母或数字当做单词的结尾
if(!isalnum(c)&&isalnum(prevc)){
pthread_mutex_lock(&counter_lock);
args->total_words++;
pthread_mutex_unlock(&counter_lock);
}
// printf("%c",c);
prevc=c;
}
fclose(fp);
}else{
perror(args->filename);
}
// printf("\n");
return NULL;
}

测试一下:

这里使用四个文件作为程序的参数,test1.txt与test2.txt内容同上,test3.txt和test4.txt内容如下:

1
2
3
4
5
// test3.txt is 10 words
commitment to excellence.
Courage is holding on to your dream.
// test4.txt is 3 words
Just for fun!

08

全文完,若有不足之处请评论指正。
本文标题:Pthread多线程编程
文章作者:ZhaLiPeng
发布时间:2016年06月04日 17时08分
本文字数:本文一共有2.6k字
原始链接:https://imzlp.me/posts/58408/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!