您的位置:首页 > 运维架构 > Linux

linux下C编程学习笔记之线程学习(二)

2013-11-13 17:07 190 查看

上一节对线程的基本知识有了初步的了解,现在我们对线程进行进一步的学习。

1.1:线程的概念:

典型的UNIX进程可以看成只有一个控制线程:一个进程在同一时刻只能做一件事。有了多控制线程以后在程序设计时可以把进程设计成同一时刻能够不止做一件事,每个线程处理各自独立的任务。这种方法有很多好处。

通过为每种事件类型处理分配单独的线程,可以简化异步处理事件代码。

多进程必须使用OS 提供的复杂机制才能实现内存和文件描述符共享,而多线程自动的可以访问相同的地址空间和文件描述符。

改善程序吞吐量。在只有一个控制线程的情况下,单个进程需要完成多个任务需要把这些任务串行化,但是有了多控制线程,相互独立的任务的处理就可以交叉进行,只需为每一个任务分配一个线程即可。

交互的程序同样可以通过使用多线程实现响应时间的改善,多线程可以吧程序中的处理输入输的部分与其他部分分开。

有些用户把多线程编程与多处理器系统联系起来,但是程序即使运行在单处理器机器上,也能得到多线程编程模型的好处。处理器的数量并不影响程序结构,所以不挂处理器的个数是多少,程序都会通过使用线程得到简化,即使多线程程序在穿行化任务时不得不阻塞,由于某些线程在阻塞时还有其他线程在运行,所以多线程程序在单处理器上运行任然能够改善响应时间和吞吐量。

1.2:基本函数介绍

1.2.1:线程标识

每一个线程都有一个线程ID,进程ID在整个系统时唯一的,但是线程ID不同,它只在它所属的进程环境中有效。线程ID使用pthread_t 数据类型表示,实现的时候可以使用一个结构来代表,所以可移植操作系统实现不能把它作为整数处理,必须使用函数对两个线程ID比较。

int pthread_equal(pthread_t tid1,pthread_t tid2); //返回值:相等返回非0,否则返回0

线程可以通过调用pthread_self函数来获得自身的线程ID:

pthread_t pthread_self(void); //返回值:调用线程ID

1.2.2:线程创建

int pthread_create(pthread_t *restrict
tidp, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg);

//当pthread_create成功返回时由tidp指向的内存单元被设置为新创建线程的线程id。attr参数用于被设定不同的线程属性。创建默认线程的属性使用NULL。新创建的线程从start_routine函数地址开始运行,该函数只有一个无类型指针参数
arg 。如果需要向start_routine函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后吧这个结构的地址作为arg的参数传入。

eg:创建一个线程并且打印进程ID、新线程的线程ID以及初始线程的线程ID

#include "apue.h"
#include <pthread.h>

pthread_t ntid;

void printids(const char *s)
{
pid_t		pid;
pthread_t	tid;

pid = getpid();
tid = pthread_self();
printf("%s pid %u tid %u (0x%x)\n", s, (unsigned int)pid,  (unsigned int)tid, (unsigned int)tid);
}

void *thr_fn(void *arg)
{
printids("new thread: ");
return((void *)0);
}

int  main(void)
{
int	err;

err = pthread_create(&ntid, NULL, thr_fn, NULL);
if (err != 0)
err_quit("can't create thread: %s\n", strerror(err));
printids("main thread:");
sleep(1);
exit(0);
}


wangye@wangye:~$ gcc -g -lpthread pirntid.c libapue.a
wangye@wangye:~$ ./a.out
main thread: pid 6458 tid 3076839104 (0xb764d6c0)
new thread:  pid 6458 tid 3076836208 (0xb764cb70)


该程序需要处理主线程和新线程之间的竞争,首先是主线程需要休眠,若主线程不休眠则它就有可能退出,这样在新线程有机会运行之前整个进程就可能已经终止了(这种行为依赖于操作系统中线程实现和调度算法)。

新线程是通过调用pthread_self来获取自己的ID,而不是从共享内存中读出或者从线程的启动例程中以参数的形式接收。回想一下pthread_create函数,他会通过第一个参数返回新建线程的线程ID,本例中,主线程把新建线程ID存放在ntid中,但是新建线程并不能安全的使用它,如果新线程在调用pthread_create函数返回之前就运行了,那么新线程就看到的是未经初始化的ntid的内容,该内容并不是正确的新线程ID。

1.2.3:线程终止:

若进程中的任一线程调用了exit、_exit、_Exit,那么整个进程就都会终止。单个线程可以通过下列三种方式退出,在不终止整个进程的情况下停止它的控制流。

1)、线程只是从启动例程中返回,返回值是线程的退出码。

2)、线程可以被同一进程中其它线程取消

3)、 调用pthread_exit

void pthread_exit (void *rval_ptr) ;

进程中的其他线程可以通过调用pthread_join函数访问到这个指针,rval_ptr。

int pthread_join (pthread_t thread ,void **rval_ptr) ;

调用该函数的线程将一直阻塞直到指定的线程调用pthread_exit、从启动例程中返回或者被取消。

eg:获得线程退出状态

#include "apue.h"
#include <pthread.h>

void *thr_fn1(void *arg)
{
printf("thread 1 returning\n");
return((void *)1);
}

void *thr_fn2(void *arg)
{
printf("thread 2 exiting\n");
pthread_exit((void *)2);
}

int main(void)
{
int	err;
pthread_t	tid1, tid2;
void		*tret;

err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_quit("can't create thread 1: %s\n", strerror(err));
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_quit("can't create thread 2: %s\n", strerror(err));
err = pthread_join(tid1, &tret);
if (err != 0)
err_quit("can't join with thread 1: %s\n", strerror(err));
printf("thread 1 exit code %d\n", (int)tret);
err = pthread_join(tid2, &tret);
if (err != 0)
err_quit("can't join with thread 2: %s\n", strerror(err));
printf("thread 2 exit code %d\n", (int)tret);
exit(0);
}


wangye@wangye:~$ gcc -g -lpthread test.c libapue.a
wangye@wangye:~$ ./a.out
thread 1 returning
thread 2 exiting
thread 1 exit code 1
thread 2 exit code 2


eg:pthread_exit函数的不正确使用:

#include "apue.h"
#include <pthread.h>

struct foo {
int a, b, c, d;
};

void  printfoo(const char *s, const struct foo *fp)
{
printf(s);
printf("  structure at 0x%x\n", (unsigned)fp);
printf("  foo.a = %d\n", fp->a);
printf("  foo.b = %d\n", fp->b);
printf("  foo.c = %d\n", fp->c);
printf("  foo.d = %d\n", fp->d);
}

void *thr_fn1(void *arg)
{
struct foo	foo = {1, 2, 3, 4};

printfoo("thread 1:\n", &foo);
pthread_exit((void *)&foo);
}

void *thr_fn2(void *arg)
{
printf("thread 2: ID is %d\n", pthread_self());
pthread_exit((void *)0);
}

int  main(void)
{
int	err;
pthread_t	tid1, tid2;
struct foo	*fp;

err = pthread_create(&tid1, NULL, thr_fn1, NULL);
if (err != 0)
err_quit("can't create thread 1: %s\n", strerror(err));
err = pthread_join(tid1, (void *)&fp);
if (err != 0)
err_quit("can't join with thread 1: %s\n", strerror(err));
sleep(1);
printf("parent starting second thread\n");
err = pthread_create(&tid2, NULL, thr_fn2, NULL);
if (err != 0)
err_quit("can't create thread 2: %s\n", strerror(err));
sleep(1);
printfoo("parent:\n", fp);
exit(0);
}



wangye@wangye:~$ gcc -g -lpthread text1.c libapue.a
wangye@wangye:~$ ./a.out
thread 1:
structure at 0xb7597380
foo.a = 1
foo.b = 2
foo.c = 3
foo.d = 4
parent starting second thread
thread 2: ID is -1218872464
parent:
structure at 0xb7597380
foo.a = -1217760242
foo.b = -1218874476
foo.c = -1217540108
foo.d = -1217547676



1.3:线程同步

当多个线程共享相同内存时,需要确保每一个线程看到一致的数据视图,如果每一个线程使用的变量都是其他线程不会读取或修改的那么就不存在一致性问题,同样的,变量是只读的多个线程读取该变量也就不存在一致性问题,但是当某个线程可以修改变量的同时其它线程也可以读取或修改这个变量的值,这时候就需要线程进行同步,以确保他们在访问变量存储内容时不会访问到无效数据。 当线程修改变量时,其他的线程读取这个变量的值就可能看到不一致的数据,在修改变量时间多于一个存储器访问周期的处理器结构中,当存储器读和存储器写这两个周期交叉时,就会潜在不一致的问题。

eg:描述两个线程读写相同变量的例子在本例中,线程A读取变量然后给该比变量赋予一个新的值,但是写操作需要两个存储周期,线程B在这两个写存储周期中读取这个相同的变量就会得到不一致的值。



为了解决上述问题,线程需要使用锁,即在同一时间只允许一个线程访问该变量,如果线程B希望读取变量的值,他需要首先获取锁,线程A在写变量时也需要获取锁而线程B,在线程A释放锁以前是不能读取变量的值得。下图为两个线程同步访问内存:



当两个或多个线程试图在同一时间修改同一变量时也需要进行同步。eg:变量递增操作( 两个非同步的线程对同一个变量做增量操作)

1)将数据从内存读入到寄存器中

2)在寄存器中进行变量值增加

3)把新的值写回到内存单元中



如果两个线程试图几乎同一时间对同一个变量做增量操作而不进行同步的话,结果很有可能出现不一致,变量可能增加1或增加2,具体增加的数值由第二线程开始操作时获取的数值来决定。,如第二个线程执行第一步要比第一个线程执行第三步早,则第二个线程读到的的初始值就会和第一个一样这样变量的值总体来时只增加了1。但是如果修改操作是原子操作的话就不存在竞争。

1.3.1:互斥量

可以使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上来说就是一把锁,在访问共享资源前需要对互斥量进行加锁,访问完毕后释放互斥量的锁,对互斥量进行加锁之后,其它试图对互斥量试图加锁的线程将会被阻塞直到当前线程释放该互斥锁,若释放互斥锁之前有多个线程被阻塞,所有在该互斥锁上阻塞的线程都会变成可运行状态,第一个变为可运行状态的线程可以对互斥量进行加锁,其他线程依然被阻塞。

互斥变量使用pthread_mutex_t的数据类型来表示,使用互斥变量以前必须对首先他进行初始化,有两种初始化方法:

1)静态分配的互斥量——将 pthread_mutex_t 设置为常量PTHREAD_MUTEX_INITIALIZER 即:pthread_mutex_t buf = PTHREAD_MUTEX_INITIALIZER

2)动态的分配互斥量——调用pthread_mutex_init 函数进行初始化,如动态地分配互斥量例如使用 malloc 等动态分配互斥量的空间时,那么在释放内存前需要用 pthread_mutex_destroy 销毁:

int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t * restrict attr);

int pthread_mutex_destroy(pthread_mutex_t * mutex); / /返回值:成功返回0,否则返回错误编号。要用默认的属性初始化互斥量,只需把attr设置为
NULL。非默认的互斥量属性将在下面线程属性中给出。

1.3.2:避免死锁

死锁:一个进程集合中的每一个进程都在等待只能由此集合中の 其他进程才能引发的事件,而无限期的陷入僵持的局面称为死锁。 例如:两个进程分别等待对方进程所占有的一个资源,导致两个进程都不能执行而处于永远的等待状态; 一个线程试图对同一个互斥量加锁两次那么他自身就会陷入死锁状态。

1.3.3:读写锁

读写锁与互斥量类似,但是具有更高的并行性。互斥量只有两个状态(加锁状态和不加锁状态),而且一次只能有一个线程对其加锁。

读写锁有三种状态:读模式下加锁状态,写模式下加锁状态,不加锁状态,一次只能有一个线程可以占有写模式下的读写锁,但是可以多线程同时占有读模式下的读写锁。当读写锁是写加锁状态时,在解锁之前,所有试图对这个锁加锁的线程都会被阻塞。当读写锁在读加锁状态下时,所有读模式对它进行加锁是我线程都可以得到访问权限,如果这时候线程需要以写模式对此锁进行加锁,必须阻塞等到所有线程释放读锁。虽然读写锁的实现不同但是当读写锁处于读模式锁住状态时,如果另有线程试图以写模式对其加锁,读写锁通常会阻塞随后的读模式锁请求。这样可以避免读模式锁长期占用,而等待的写模式锁请求一直得不到满足。
读写锁非常适用于对数据结构读的次数远大于写的情况。

1.3.4:条件变量

条件变量是线程可用的另外一种机制,条件变量给多个线程提供了一个会合的场所,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定条件发生。条件本身使用互斥量保护的。线程在改变条件状态前必须首先锁住互斥量,其他线程获得互斥量之前不会察觉这种改变,因为必须锁住互斥量以后才能计算条件。 条件变量使用之前必须初始化。pthread_cond_t 数据类型代表的条件变量使用两种方式初始化:

1)静态分配——把常量PTHREAD_MUTEX_INITIALIZER赋给静态分配的条件变量

2)动态分配——使用pthread_cond_init 函数初始化,释放底层内存空间之前,需要使用pthread_cond_destroy 函数对条件变量进行去除初始化。

int pthread_cond_init(pthread_cond_t * restrict cond , const pthread_condattr_t * restrict attr); / /返回值:成功返回0,否则返回错误编号。

除非要使用一个非默认属性的条件变量,否则pthread_cond_init函数的attr参数设置为 NULL。 使用pthread_cond_wait等待条件变为真,如果在等待的时间条件内不能满足,那么就会生成并返回一个错误码。 pthread_cond_wait 在不同条件下行为不同:

1. 当执行 int pthread_cond_wait 时,该函数作为一个原子操作包含以下两步:

1) 解锁互斥量

2) 阻塞进程直到其它线程调用 pthread_cond_signal 以告知 cond 可以不阻塞 pthread_cond_timewait 函数的工作方式与pthread_cond_wait函数相似,只是多了一个timeout。timeout值指定了等待时间,时间应秒数或分秒数表示,分秒数的单位是纳秒。传递给 pthread_cond_wait的互斥量对条件进行保护,调用者把锁住的互斥量传递给函数,函数把调用线程放到等待条件的线程列表上,然后对互斥量解锁。

2.上述两个函数用于通知线程条件已经够了,pthread_cond_signal 用于唤醒等待该条件的某个线程,用于唤醒等待该条件的所有线程。(POSIX规范为了简化实现吗,pthread_cond_signal在实现的时候可以唤醒不止一个线程 当执行 pthread_cond_signal(cond) 时,作为原子操作包含以下两步:

1) 给 互斥量 加锁

2)停止阻塞线程, 因而得以再次执行循环,判断条件是否满足。(注意到此时 mtx 仍然被当前线程独有,保证互斥)调用pthread_cond_signal 或 pthread_cond_broadcast,也称为像线程或条件发送信号,但是一定要在改变条件以后再给线程发送信号。

1.4:线程属性

在上一章所说的pthread_creat函数的例子中,传入的参数都是空指针,而不是指向pthread_attr_t结构的指针, 可以使用pthread_attr_t结构修改线程默认属性,并把这些属性与创建线程联系起来。可以使用pthread_attr_init函数初始化pthread_attr_结构。调用pthread_attr_init以后,pthread_attr_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值:。如果要修改其中个别属性的值,需要调用其它的函数。int pthread_attr_init(pthread_attr_t
*attr); int pthread_attr_destroy(pthread_attr_t *attr) 如果要除去对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数。pthread_attr_t结构对应用程序是不透明的,也就是说应用程序不需要了解有关属性对象内部结构的任何细节。POSIX为查询和设置每种属性定义了独立的函数

1.5:同步属性







                                            
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: