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


    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

  • 相关阅读:
    Valid Parentheses
    3Sum
    泛型(一)
    Longest Common Prefix
    Roman to Integer
    Integer to Roman
    Container With Most Water
    知道创宇研发技能表v2.2
    anti-dns pinning 攻击
    dominator
  • 原文地址:https://www.cnblogs.com/mydomain/p/2121794.html
Copyright © 2020-2023  润新知