31、深入理解计算机系统笔记,并发编程(concurrent)(3)
1、基于预线程化(prethreading)的并发服务器
常规的并发服务器中,我们为每一个客户端创建一个新线程,代价较大。一个基于预线程化的服务器通过使用“生产者-消费者模型”来试图降低这种开销。
服务器由一个主线程和一组worker线程组成的,主线程不断地接受来自客户端的连接请求,并将得到的连接描述符放在一个共享的缓冲区中。每一个worker线程反复从共享缓冲区中取出描述符,为客户端服务,然后等待下一个描述符。
示例代码
/* * echoservert_pre.c - A prethreaded concurrent echo server */ /* $begin echoservertpremain */ #include "csapp.h" #include "sbuf.h" #define NTHREADS 4 #define SBUFSIZE 16 void echo_cnt(int connfd); void *thread(void *vargp); sbuf_t sbuf; /* shared buffer of connected descriptors */ int main(int argc, char **argv) { int i, listenfd, connfd, port, clientlen=sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; pthread_t tid; if (argc != 2) { fprintf(stderr, "usage: %s <port>\n", argv[0]); exit(0); } port = atoi(argv[1]); sbuf_init(&sbuf, SBUFSIZE); listenfd = Open_listenfd(port); for (i = 0; i < NTHREADS; i++) /* Create worker threads */ Pthread_create(&tid, NULL, thread, NULL); while (1) { connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen); sbuf_insert(&sbuf, connfd); /* Insert connfd in buffer */ } } void *thread(void *vargp) { Pthread_detach(pthread_self()); while (1) { int connfd = sbuf_remove(&sbuf); /* Remove connfd from buffer */ echo_cnt(connfd); /* Service client */ Close(connfd); } } /* $end echoservertpremain */
如上模型组成事件驱动服务器,事件驱动程序创建它们自己的并发逻辑流,这些逻辑流被模型化为状态机,带有主线程和worker线程的简单状态机。
2、其他并发问题
一个函数被称为线程安全(thread-safe)的,当且仅当多个线程反复地调用时,它会一下产生正确的结果。
下面是四类不安全(相交)的函数:
1)不保护共享变量的函数
利用P,V操作解决这个问题。
2)保持跨越多个调用的状态的函数
示例代码1
#include <stdio.h> /* $begin rand */ unsigned int next = 1; /* rand - return pseudo-random integer on 0..32767 */ int rand(void) { next = next*1103515245 + 12345; return (unsigned int)(next/65536) % 32768; } /* srand - set seed for rand() */ void srand(unsigned int seed) { next = seed; } /* $end rand */ int main() { srand(100); printf("%d\n", rand()); printf("%d\n", rand()); printf("%d\n", rand()); exit(0); }
srand设置种子,调用rand生成随机数。多线程调用时就出问题了。我们可以重写之解决,使之不再使用任何静态数据,取而代之地依靠调用者在参数中传递状态信息。
示例代码2
#include <stdio.h> /* $begin rand_r */ /* rand_r - a reentrant pseudo-random integer on 0..32767 */ int rand_r(unsigned int *nextp) { *nextp = *nextp * 1103515245 + 12345; return (unsigned int)(*nextp / 65536) % 32768; } /* $end rand_r */ int main() { unsigned int next = 1; printf("%d\n", rand_r(&next)); printf("%d\n", rand_r(&next)); printf("%d\n", rand_r(&next)); exit(0); }
3)返回指向静态变量的指针的函数
某些函数(如gethostbyname)将结果放在静态结构中,并返回一个指向这个结构的指针。多线程并发可能引发灾难,因为正在被一个线程使用的结果会被另一个线程悄悄覆盖。
两种方法处理:
一是重写之。使得调用者传递存放结果的结构的地址,这就消除了共享数据。
第二种方法是:使用称为lock-and-copy的技术。在每一个调用位置,对互斥锁加锁,调用线程不安全函数,动态地为结果分配存储器,copy函数返回结果到这个存储器位置,对互斥锁解锁。
示例代码3
/* * gethostbyname_ts - A thread-safe wrapper for gethostbyname */ #include "csapp.h" static sem_t mutex; /* protects calls to gethostbyname */ static void init_gethostbyname_ts(void) { Sem_init(&mutex, 0, 1); } /* $begin gethostbyname_ts */ struct hostent *gethostbyname_ts(char *hostname) { struct hostent *sharedp, *unsharedp; unsharedp = Malloc(sizeof(struct hostent)); P(&mutex); sharedp = gethostbyname(hostname); *unsharedp = *sharedp; /* copy shared struct to private struct */ V(&mutex); return unsharedp; } /* $end gethostbyname_ts */ int main(int argc, char **argv) { char **pp; struct in_addr addr; struct hostent *hostp; if (argc != 2) { fprintf(stderr, "usage: %s <hostname>\n", argv[0]); exit(0); } init_gethostbyname_ts(); hostp = gethostbyname_ts(argv[1]); if (hostp) { printf("official hostname: %s\n", hostp->h_name); for (pp = hostp->h_aliases; *pp != NULL; pp++) printf("alias: %s\n", *pp); for (pp = hostp->h_addr_list; *pp != NULL; pp++) { addr.s_addr = *((unsigned int *)*pp); printf("address: %s\n", inet_ntoa(addr)); } } else { printf("host %s not found\n", argv[1]); } exit(0); }
4)调用线程不安全函数的函数
f调用g。如果g是2)类函数,则f也是不安全的,只能得写。如果g是1)或3)类函数,则利用互斥锁保护调用位置和任何想得到的共享数据,f仍是线程安全的。如上例中。
3、可重入性
可重入函数(reenterant function)具有这样的属性:当它们被多个线程调用时,不会引用任何共享数据。
可重入函数通常比不可重入函数高效一些,因为不需要同步操作。
如果所有的函数参数都是传值传递(没有指针),且所有的数据引用都是本地的自动栈变量(没有引用静态或全局变量),则函数是显式可重入的,无论如何调用,都没有问题。
允许显式可重入函数中部分参数用指针传递,则隐式可重入的。在调用线程时小心传递指向非共享数据的指针,它才是可重入。如rand_r。
可重入性同时是调用者和被调用者的属性。
4、C库中常用的线程不安全函数及unix线程安全版本
5、竞争
当一个程序的正确性依赖于一个线程要在另一个线程到达y点之前到达它的控制流中的x点时,就会发生竞争(race)。
示例代码1
/* * race.c - demonstrates a race condition */ /* $begin race */ #include "csapp.h" #define N 4 void *thread(void *vargp); int main() { pthread_t tid[N]; int i; for (i = 0; i < N; i++) Pthread_create(&tid[i], NULL, thread, &i); for (i = 0; i < N; i++) Pthread_join(tid[i], NULL); exit(0); } /* thread routine */ void *thread(void *vargp) { int myid = *((int *)vargp); printf("Hello from thread %d\n", myid); return NULL; } /* $end race */
示例代码2(消除竞争)
/* * norace.c - fixes the race in race.c */ /* $begin norace */ #include "csapp.h" #define N 4 void *thread(void *vargp); int main() { pthread_t tid[N]; int i, *ptr; for (i = 0; i < N; i++) { ptr = Malloc(sizeof(int)); *ptr = i; Pthread_create(&tid[i], NULL, thread, ptr); } for (i = 0; i < N; i++) Pthread_join(tid[i], NULL); exit(0); } /* thread routine */ void *thread(void *vargp) { int myid = *((int *)vargp); Free(vargp); printf("Hello from thread %d\n", myid); return NULL; } /* $end norace */
6、死锁
信号量引入一个潜在的运行是错误-死锁。死锁是因为每个线程都在等待其他线程运行一个根本不可能发生的V操作。
避免死锁是很困难的。当使用二进制信号量来实现互斥时,可以用如下规则避免:
如果用于程序中每对互斥锁(s,t),每个既包含s也包含t的线程都按照相同顺序同时对它们加锁,则程序是无死锁的。
参考
[1] http://www.cnblogs.com/mydomain/archive/2011/07/10/2102147.html