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 |
|
注意:编译时必须要使用
-lpthread
参数。如:clang++ -o pthread.exe pthread.cc -lpthread
使用makefile
会更方便一些,我在这里使用的makefile
如下:
1 | # compile pthread.c |
编译运行的结果为:
下面来分析一下代码中用到的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 |
|
上面的代码中使用了两个线程.初始化线程执行了一个循环来使计数器每秒钟增加1.初始线程在进入循环之前,创建了一个新的线程.新的线程运行了一个函数来将counter的值打印出来,.main函数和print_counter运行在同一个进程中,所有都具有对于counter的访问权限.
上面的代码中,线程的运行顺序是不可预料的.从运行结果中就可以看出来.
可以看出来,有时候是可以按照我们预料的方式执行,但是有时候则不会.
这是由于线程之间的调度不是一定要执行过初始化线程之后再执行我们创建的线程,有可能我们执行初始化线程之前,调用了X次我们创建的线程t1,也有可能,我们执行了初始化线程之后又执行了Y次t1线程,所以,输出的顺序是不一定的.
在不使用sleep的情况下的运行结果是这样的:
下面一份代码,使用两个线程统计两个文件中所有单词的个数:
统计样本:
1 | // test1.txt is 14 words |
代码如下:
1 |
|
运行结果:
也可以在text1.txt或者test2.txt中添加数据:
1 | // test2.txt is 11 words |
如果输出线程读取两个文件的顺序的话,与上一份代码一样,也是乱序的,哈哈.我为什么老是关注这些东西.
上面的代码中使用了几个新的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_lock
和pthread_mutex_unlock
的调用之间.
1 | pthread_mutex_lock(&counter_lock); |
现在两个线程可以安全地共享计数器了.当一个线程调用pthread_mutex_lock
的时候,如果另一个线程已经将这个互斥量锁住了,那这个线程就会被阻塞等待着这个锁被另一个线程解开,然后才可以对计数器进行操作.每个线程对计数器进行操作之后都将互斥量解锁,以便共享的数据被其他的进程操作.
任何数目的线程都可以挂起等待互斥量解锁.当一个线程对互斥量解锁之后,系统就将控制权交给等待的某一个线程.
Upgrade code:
- 可以接收不定个数的文件
- 通过arg获取到的参数个数来决定创建多少个线程和多少个struct对象(存储文件信息)
- 通过struct来记录单个文件中单词的个数(每个文件具有一个单独的计数器)
1 |
|
测试一下:
这里使用四个文件作为程序的参数,test1.txt与test2.txt内容同上,test3.txt和test4.txt内容如下:
1 | // test3.txt is 10 words |