Linux通过fork创建子进程与创建线程之间是有差别的:fork创建出该进程的一份拷贝,这个新进程拥有自己的变量和自己的PID,它的时间调度是独立的,它的运行差点儿全然独立于父进程。
在一个多线程程序里,默认情况下,仅仅有一个errno变量供全部的线程共享。在一个线程准备获取刚才的错误代码时,该变量非常easy被还有一个线程中的函数调用所改变。相似的问题还存在于fputs之类的函数中,这些函数通经常使用一个单独的全局性区域来缓存输出数据。
(1)它会对部分函数又一次定义它们的可安全重入的版本号,这些函数名字一般不会发生改变,仅仅是会在函数名后面加入_r字符串,如函数名gethostbyname变成gethostbyname_r。
大多数pthread_XXX系列的函数在失败时,并未遵循UNIX函数的惯例返回-1,这样的情况在UNIX函数中属于一少部分。假设调用成功,则返回值是0,假设失败则返回错误代码。
int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
线程通过调用pthread_exit函数终止运行,并返回一个指向某对象的指针。注意:绝不能用它返回一个指向局部变量的指针,由于线程调用该函数后,这个局部变量就不存在了,这将引起严重的程序漏洞。
thread_function is running. Argument was Hello World
Waiting for thread to finish...
pthread_exit(void *retval)本身返回的就是指向某个对象的指针,因此,pthread_join(pthread_t th, void **thread_return);中的thread_return是二级指针,指向线程返回值的指针。
能够看到,我们创建的新线程改动的数组message的值,而原先的线程也能够訪问该数组。假设我们调用的是fork而不是pthread_create,就不会有这种效果了。原因是fork创建子进程之后,子进程会拷贝父进程,两者分离,相互不干扰,而线程之间则是共享进程的相关资源。
4.线程的同一时候运行
接下来,我们来编写一个程序,以验证两个线程的运行是同一时候进行的。当然,假设是在一个单处理器系统上,线程的同一时候运行就须要靠CPU在线程之间的高速切换来实现了。
我们的程序须要利用一个原理:即除了局部变量外,全部其它的变量在一个进程中的全部线程之间是共享的。
在这个程序中,我们是在两个线程之间使用轮询技术,这样的方式称为忙等待,所以它的效率会非常低。在本文的兴许部分,我们将介绍一种更好的解决的方法。
以下的代码中,两个线程会不断的轮询推断flag的值是否满足各自的要求。
编译这个程序:
gcc -D_REENTRANT thread2.c -o thread2 –lpthread
执行这个程序:
$ ./thread2
121212121212121212
Waiting for thread to finish...
5.线程的同步
在上述演示样例中,我们採用轮询的方式在两个线程之间不停地切换是很笨拙且没有效率的实现方式,幸运的是,专门有一级设计好的函数为我们提供更好的控制线程运行和訪问代码临界区的方法。
本小节将介绍两个线程同步的基本方法:信号量和相互排斥量。这两种方法非常类似,其实,它们能够互相通过对方来实现。但在实际的应用中,对于一些情况,可能使用信号量或相互排斥量中的一个更符合问题的语义,而且效果更好。
5.1用信号量进行同步
1.信号量创建
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
參数说明:
sem:信号量对象。
pshared:控制信号量的类型,0表示这个信号量是当前进程的局部信号量,否则,这个信号量就能够在多个进程之间共享。
value:信号量的初始值。
2.信号量控制
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
sem_post的作用是以原子操作的方式给信号量的值加1。
sem_wait的作用是以原子操作的方式给信号量的值减1,但它会等到信号量非0时才会開始减法操作。假设对值为0的信号量调用sem_wait,这个函数就会等待,直到有线程添加了该信号量的值使其不再为0。
3.信号量销毁
#include <semaphore.h>
int sem_destory(sem_t *sem);
这个函数的作用是,用完信号量后对它进行清理,清理该信号量所拥有的资源。假设你试图清理的信号量正被一些线程等待,就会收到一个错误。
与大多数Linux函数一样,这些函数在成功时都返回0。
以下编码实现输入字符串,统计每行的字符个数,以“end”结束输入:
编译这个程序:
gcc -D_REENTRANT thread2.c -o thread2 –lpthread
执行这个程序:
$ ./thread3 Input some text. Enter 'end' to finish 123 You input 3 characters 1234 You input 4 characters 12345 You input 5 characters end Waiting for thread to finish… Thread join |
通过使用信号量,我们堵塞了统计字符个数的线程,这个程序似乎对高速的文本输入和悠闲的暂停都非常适用,比之前的轮询解决方式效率上有了本质的提高。 5.2用相互排斥量进行线程同步
还有一种用在多线程程序中同步訪问的方法是使用相互排斥量。它同意程序猿锁住某个对象,使得每次仅仅能有一个线程訪问它。为了控制对关键代码的訪问,必须在进入这段代码之前锁住一个相互排斥量,然后在完毕操作之后解锁它。
用于相互排斥量的基本函数和用于信号量的函数很类似:
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t, *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destory(pthread_mutex_t *mutex);
与其它函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,所以必须对函数的返回代码进行检查。相互排斥量的属性设置这里不讨论,因此设置成NULL。
我们用相互排斥量来重写刚才的代码例如以下:
编译这个程序:
gcc -D_REENTRANT thread4.c -o thread4 –lpthread
执行这个程序:
$ ./thread4 Input some text. Enter 'end' to finish 123 You input 3 characters 1234 You input 4 characters 12345 You input 5 characters end You input 3 characters Thread joined |
6.线程的属性
之前我们并未谈及到线程的属性,能够控制的线程属性是许多的,这里面仅仅列举一些经常使用的。
如在前面的演示样例中,我们都使用的pthread_join同步线程,但事实上有些情况下,我们并不须要。如:主线程为服务线程,而第二个线程为数据备份线程,备份工作完毕之后,第二个线程能够直接终止了,它没有必要再返回到主线程中。因此,我们能够创建一个“脱离线程”。
以下介绍几个经常使用的函数:
(1)int pthread_attr_init (pthread_attr_t* attr);
功能:对线程属性变量的初始化。
attr:线程属性。
函数返回值:成功:0,失败:-1
(2) int pthread_attr_setscope (pthread_attr_t* attr, int scope);
功能:设置线程绑定属性。
attr:线程属性。
scope:PTHREAD_SCOPE_SYSTEM(绑定);PTHREAD_SCOPE_PROCESS(非绑定)
函数返回值:成功:0,失败:-1
(3) int pthread_attr_setdetachstate (pthread_attr_t* attr, int detachstate);
功能:设置线程分离属性。
attr:线程属性。
detachstate:PTHREAD_CREATE_DETACHED(分离);PTHREAD_CREATE_JOINABLE(非分离)
函数返回值:成功:0,失败:-1
(4) int pthread_attr_setschedpolicy(pthread_attr_t* attr, int policy);
功能:设置创建线程的调度策略。
attr:线程属性;
policy:线程调度策略:SCHED_FIFO、SCHED_RR和SCHED_OTHER。
函数返回值:成功:0,失败:-1
(5) int pthread_attr_setschedparam (pthread_attr_t* attr, struct sched_param* param);
功能:设置线程优先级。
attr:线程属性。
param:线程优先级。
函数返回值:成功:0,失败:-1
(6) int pthread_attr_destroy (pthread_attr_t* attr);
功能:对线程属性变量的销毁。
attr:线程属性。
函数返回值:成功:0,失败:-1
(7)其它
int pthread_attr_setguardsize(pthread_attr_t* attr,size_t guardsize);//设置新创建线程栈的保护区大小。
int pthread_attr_setinheritsched(pthread_attr_t* attr, int inheritsched);//决定如何设置新创建线程的调度属性。
int pthread_attr_setstack(pthread_attr_t* attr, void* stackader,size_t stacksize);//两者共同决定了线程栈的基地址以及堆栈的最小尺寸(以字节为单位)。
int pthread_attr_setstackaddr(pthread_attr_t* attr, void* stackader);//决定了新创建线程的栈的基地址。
int pthread_attr_setstacksize(pthread_attr_t* attr, size_t stacksize);//决定了新创建线程的栈的最小尺寸(以字节为单位)。
例:创建优先级为10的线程。
pthread_attr_t attr; struct sched_param param; pthread_attr_init(&attr); pthread_attr_setscope (&attr, PTHREAD_SCOPE_SYSTEM); //绑定 pthread_attr_setdetachstate (&attr, PTHREAD_CREATE_DETACHED); //分离 pthread_attr_setschedpolicy(&attr, SCHED_RR); param.sched_priority = 10; pthread_attr_setschedparam(&attr, ¶m); pthread_create(xxx, &attr, xxx, xxx); pthread_attr_destroy(&attr); |
以下实现一个脱离线程的程序,创建一个线程,其属性设置为脱离状态。子线程结束时,要使用pthread_exit,原来的主线程不再等待与子线程又一次合并。代码例如以下:
编译这个程序:
gcc -D_REENTRANT thread5.c -o thread5 –lpthread
执行这个程序:
$ ./thread5
thread_function is running. Argument: hello world!
Waiting for thread to finished...
Waiting for thread to finished...
Waiting for thread to finished...
Waiting for thread to finished...
Second thread setting finished flag, and exiting now
Other thread finished!
通过设置线程的属性,我们还能够控制线程的调试,其方式与设置脱离状态是一样的。
7.取消一个线程
有时,我们想让一个线程能够要求还有一个线程终止,线程有方法做到这一点,与信号处理一样,线程能够在被要求终止时改变其行为。
先来看用于请求一个线程终止的函数:
#include <pthread.h>
int pthread_cancel(pthread_t thread);
这个函数简单易懂,提供一个线程标识符,我们就能够发送请求来取消它。
线程能够用pthread_setcancelstate设置自己的取消状态。
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);
參数说明:
state:能够是PTHREAD_CANCEL_ENABLE同意线程接收取消请求,也能够是PTHREAD_CANCEL_DISABLE忽略取消请求。
oldstate:获取先前的取消状态。假设对它没兴趣,能够简单地设置为NULL。假设取消请求被接受了,线程能够进入第二个控制层次,用pthread_setcanceltype设置取消类型。
#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);
參数说明:
type:能够取PTHREAD_CANCEL_ASYNCHRONOUS,它将使得在接收到取消请求后马上採取行动;还有一个是PTHREAD_CANCEL_DEFERRED,它将使得在接收到取消请求后,一直等待直到线程运行了下述函数之中的一个后才採取行动:pthread_join、pthread_cond_wait、pthread_cond_timedwait、pthread_testcancel、sem_wait或sigwait。
oldtype:同意保存先前的状态,假设不想知道先前的状态,能够传递NULL。
默认情况下,线程在启动时的取消状态为PTHREAD_CANCEL_ENABLE,取消类型是PTHREAD_CANCEL_DEFERRED。
以下编写代码thread6.c,主线程向它创建的线程发送一个取消请求。
编译这个程序:
gcc -D_REENTRANT thread6.c -o thread6 –lpthread
执行这个程序:
$ ./thread6
thread_function is running...
Thread is still running (0)...
Thread is still running (1)...
Thread is still running (2)...
Thread is still running (3)...
Canceling thread...
Waiting for thread to finished...
8.多线程
之前,我们所编写的代码里面都不过创建了一个线程,如今我们来演示一下怎样创建一个多线程的程序。
编译这个程序:
gcc -D_REENTRANT thread7.c -o thread7 –lpthread
执行这个程序:
$ ./thread7 thread_function is running. Argument was 0 thread_function is running. Argument was 1 thread_function is running. Argument was 2 thread_function is running. Argument was 3 thread_function is running. Argument was 4 Bye from 1 thread_function is running. Argument was 5 Waiting for threads to finished... Bye from 5 Picked up a thread:6 Bye from 0 Bye from 2 Bye from 3 Bye from 4 Picked up a thread:5 Picked up a thread:4 Picked up a thread:3 Picked up a thread:2 Picked up a thread:1 All done |
9.小结
本文主要介绍了Linux环境下的多线程编程,介绍了信号量和相互排斥量、线程属性控制、线程同步、线程终止、取消线程及多线程并发。
本文比較简单,仅仅作为初学Linux多线程编程入门之用。