31、深入理解计算机系统笔记,并发编程(concurrent)(3)

简介: 1、基于预线程化(prethreading)的并发服务器 常规的并发服务器中,我们为每一个客户端创建一个新线程,代价较大。一个基于预线程化的服务器通过使用“生产者-消费者模型”来试图降低这种开销。

1、基于预线程化(prethreading)的并发服务器

常规的并发服务器中,我们为每一个客户端创建一个新线程,代价较大。一个基于预线程化的服务器通过使用“生产者-消费者模型”来试图降低这种开销。

wps_clip_image-5010

服务器由一个主线程和一组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)不保护共享变量的函数

利用PV操作解决这个问题。

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。如果g2)类函数,则f也是不安全的,只能得写。如果g1)或3)类函数,则利用互斥锁保护调用位置和任何想得到的共享数据,f仍是线程安全的。如上例中。

3、可重入性

可重入函数(reenterant function)具有这样的属性:当它们被多个线程调用时,不会引用任何共享数据。

wps_clip_image-29890

可重入函数通常比不可重入函数高效一些,因为不需要同步操作。

如果所有的函数参数都是传值传递(没有指针),且所有的数据引用都是本地的自动栈变量(没有引用静态或全局变量),则函数是显式可重入的,无论如何调用,都没有问题。

允许显式可重入函数中部分参数用指针传递,则隐式可重入的。在调用线程时小心传递指向非共享数据的指针,它才是可重入。如rand_r

可重入性同时是调用者和被调用者的属性。

4、C库中常用的线程不安全函数及unix线程安全版本

wps_clip_image-15290

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

目录
相关文章
|
7月前
|
存储 安全 Java
杰哥教你面试之一百问系列:java中高级多线程concurrent的使用
提到多线程,当然要熟悉java提供的各种多线程相关的并发包了,而java.util.concurrent就是最最经常会使用到的,那么关于concurrent的面试题目有哪些呢?一起来看看吧。
杰哥教你面试之一百问系列:java中高级多线程concurrent的使用
|
1天前
|
安全
并发编程之一些多线程习题的详细解析
并发编程之一些多线程习题的详细解析
4 0
|
11天前
|
Java 调度 开发者
Java 21时代的标志:虚拟线程带来的并发编程新境界
Java 21时代的标志:虚拟线程带来的并发编程新境界
19 0
|
21天前
|
Java API 调度
Java中的并发编程:探索多线程技术的奥秘
Java作为一种高度并发的编程语言,其多线程技术一直备受关注。本文将深入探讨Java中的并发编程,从基本概念到高级应用,解析多线程技术的奥秘,帮助读者更好地理解和应用Java中的并发编程。
|
22天前
|
算法 安全 Java
Java中的多线程并发控制:从理论到实践
【4月更文挑战第2天】在计算机科学中,多线程并发是一个复杂而又重要的主题。在Java中,我们可以通过使用多线程来提高程序的性能和效率。然而,多线程并发也带来了一些问题,如数据不一致、死锁等。本文将深入探讨Java中的多线程并发控制,包括理论基础和实际应用,希望能帮助读者更好地理解和掌握这一主题。
|
25天前
|
Java
深入理解Java并发编程:从基础到精通**
在现代软件开发中,并发编程是一个不可或缺的部分。对于Java开发人员来说,了解并发编程的基础知识和高级概念至关重要。本文将探讨Java并发编程的核心概念、工具和技术,帮助读者从基础走向精通。我们将介绍线程、锁、同步机制、线程池等关键技术,并通过实例分析如何在实际项目中应用这些知识。通过阅读本文,您将能够更好地理解和应对并发编程带来的挑战。
|
1月前
|
并行计算 安全 Java
深入理解Java并发编程:从基础到高级
【2月更文挑战第30天】 本文将深入探讨Java并发编程的核心概念和技术,包括线程、锁、同步、并发集合等。我们将从基础知识开始,逐步深入到高级主题,如Fork/Join框架、CompletableFuture和反应式编程。通过本文,你将能够理解并发编程的重要性,掌握Java中实现高效并发的关键技术和方法。
|
7月前
|
缓存 Java
【并发编程的艺术】JAVA 原子操作实现原理
本篇把原子操作单独拿出来详细阐述,结合前面两篇文章中的CPU多级缓存结构进行串联,加深理解。下一篇会全面研究Java的内存模型。
136 0
|
11月前
|
安全 Java 大数据
大数据开发基础的编程语言的Java的并发/多线程编程的并发编程基础
Java是一种流行的面向对象编程语言,它支持并发/多线程编程。在多线程应用中,多个线程可以同时执行,提高程序的效率和性能。但是,在多线程编程中,需要注意线程安全和同步问题。本文将介绍Java的并发编程基础以及如何避免线程安全和同步问题。
87 0
|
存储 缓存 算法
Java并发编程(8)---并发编程学习总结
学习并发编程相关的知识已经有一个月有余。现在对相关的知识做一个总结。 本总结主要介绍线程不安全的根源,Java内存模型,锁的基础知识,已经线程间的通信。每个知识点都有相应的demo。我将从如下几个方面进行总结:
96 0
Java并发编程(8)---并发编程学习总结

热门文章

最新文章