【问题标题】:Multithreading. How to equally share common resources?多线程。如何平等共享共同资源?
【发布时间】:2016-07-08 15:55:52
【问题描述】:

简单的代码(我知道这是一个非常糟糕的代码,但我只是为了示例):

 1 #include <mutex>
 2 #include <iostream>
 3 #include <thread>
 4 
 5 std::mutex mu;
 6 
 7 void myFunc(void) {
 8         for (int i = 0; i < 100; ++i) {
 9                 mu.lock();
 10                 std::cout << "CHILD Thread: " << std::this_thread::get_id() << std::endl;
 11                 mu.unlock();
 12         }
 13 }
 14 
 15 int main()
 16 {
 17         std::thread thr(myFunc);
 18         for (int i = 0; i < 100; ++i) {
 19                 mu.lock();
 20                 std::cout << "MAIN Thread: " << std::this_thread::get_id() << std::endl;
 21                 mu.unlock();
 22         }
 23         thr.join();
 24         return 0;
 25 }

返回这种输出:

1      MAIN Thread: 140581832210240
2      MAIN Thread: 140581832210240
3      MAIN Thread: 140581832210240
4      MAIN Thread: 140581832210240
5      MAIN Thread: 140581832210240
6      MAIN Thread: 140581832210240
7      MAIN Thread: 140581832210240
8      CHILD Thread: 140581814855424
9      CHILD Thread: 140581814855424
10     CHILD Thread: 140581814855424
11     CHILD Thread: 140581814855424
12     CHILD Thread: 140581814855424
13     CHILD Thread: 140581814855424
14     CHILD Thread: 140581814855424
15     CHILD Thread: 140581814855424
16     CHILD Thread: 140581814855424
17     CHILD Thread: 140581814855424
18     MAIN Thread: 140581832210240
19     MAIN Thread: 140581832210240
20     MAIN Thread: 140581832210240
21     MAIN Thread: 140581832210240
22     MAIN Thread: 140581832210240
23     CHILD Thread: 140581814855424
24     CHILD Thread: 140581814855424
25     CHILD Thread: 140581814855424
26     CHILD Thread: 140581814855424
27     CHILD Thread: 140581814855424
28     CHILD Thread: 140581814855424
29     CHILD Thread: 140581814855424
           ....... and so on

正如我所见——这个输出违背了多线程的含义,因为一个线程必须等待另一个线程很长时间。这个输出应该同时给我 cout of child、cout of main、cout of child、cout of main 等等。我知道互斥锁不负责公平共享公共资源,但是:谁是?我如何在我的程序中实现它?

谢谢。

编辑:将 std::cout 放入函数中:

10 void common_cout(string msg) {
11         mu.lock();
12         std::cout << msg << std::endl;
13         mu.unlock();
14 }

没有帮助。

【问题讨论】:

  • 所有线程不应该使用相同的内存空间。这就是为什么你必须使用锁等等。平均分配空间,以便第一个线程将使用 1/number_of_threads 块内存。在这种情况下,您甚至不需要使用锁定机制。还要检查您的 CPU 上可以运行多少线程。运行超过 CPU 支持的线程是没有意义的。

标签: c++ multithreading mutex


【解决方案1】:

原始代码在 windows 中也有同样的问题,但我切换到使用原生 windows 等效项,这个 windows 示例按照您期望的方式工作,在两个线程之间交替。 ReleaseMutex() 的每个实例都会导致“其他”线程获取互斥锁并运行。 main 中的 Sleep(2) 是一种确保 myFunc 首先启动它的循环的简单方法。

我还创建了一个带有主线程和两个线程的版本,总共三个线程。循环按顺序进行,因此 Windows 本机互斥锁似乎是按请求的顺序完成的。

对于循环类型的循环或线程和/或进程之间的一般同步,每个线程或进程使用一个信号量更好,因为任何线程或进程都可以增加(释放/发出信号)任何信号量。这样做的问题是信号量不是标准线程接口的本机部分,需要互斥锁和条件变量的某种组合来实现信号量的等价物。 Windows 和 posix 支持本机信号量。

#include <iostream>
#include <windows.h>

static HANDLE mu;                       // handle: mutex
static HANDLE ht1;                      // handle: thread 1
static DWORD  id1;                      // thread 1 id

DWORD WINAPI myFunc(LPVOID) {
    for (int i = 0; i < 20; ++i) {
        WaitForSingleObject(mu, INFINITE);
        std::cout << "child thread: " << i << std::endl;
        ReleaseMutex(mu);
    }
    return 0;
}

int main()
{
    mu = CreateMutex(NULL,TRUE,NULL);   // main owns mutex
    ht1 = CreateThread(NULL, 0, myFunc, 0, 0, &id1);
    Sleep(2);                           // make sure myFunc running
    ReleaseMutex(mu);                   // release mutex
    for (int i = 0; i < 20; ++i) {
        WaitForSingleObject(mu, INFINITE);
        std::cout << "main  thread: " << i << std::endl;
        ReleaseMutex(mu);
    }
    WaitForSingleObject(ht1, INFINITE);
    CloseHandle(ht1);
    CloseHandle(mu);
    return 0;
}

输出

child thread: 0
main  thread: 0
child thread: 1
main  thread: 1
...
child thread: 18
main  thread: 18
child thread: 19
main  thread: 19

3 线程示例:

#include <iostream>
#include <windows.h>

static HANDLE mu;                       // handle: mutex
static HANDLE ht0;                      // handle: thread 0
static HANDLE ht1;                      // handle: thread 1
static DWORD  id0;                      // thread 0 id
static DWORD  id1;                      // thread 1 id

DWORD WINAPI Thread0(LPVOID) {
    for (int i = 0; i < 10; ++i) {
        WaitForSingleObject(mu, INFINITE);
        std::cout << "Thread0 : " << i << std::endl;
        ReleaseMutex(mu);
    }
    return 0;
}

DWORD WINAPI Thread1(LPVOID) {
    for (int i = 0; i < 10; ++i) {
        WaitForSingleObject(mu, INFINITE);
        std::cout << "Thread1 : " << i << std::endl;
        ReleaseMutex(mu);
    }
    return 0;
}

DWORD WINAPI Thread2(LPVOID) {
    for (int i = 0; i < 10; ++i) {
        WaitForSingleObject(mu, INFINITE);
        std::cout << "Thread2 : " << i << std::endl;
        ReleaseMutex(mu);
    }
    return 0;
}

int main()
{
    mu = CreateMutex(NULL,TRUE,NULL);   // main owns mutex
    ht0 = CreateThread(NULL, 0, Thread0, 0, 0, &id0);
    ht1 = CreateThread(NULL, 0, Thread1, 0, 0, &id1);
    Sleep(2);                           // let other threads get started
    ReleaseMutex(mu);                   // release mutex
    Thread2(0);
    WaitForSingleObject(ht0, INFINITE);
    WaitForSingleObject(ht1, INFINITE);
    CloseHandle(ht0);
    CloseHandle(ht1);
    CloseHandle(mu);
    return 0;
}

输出

Thread0 : 0
Thread1 : 0
Thread2 : 0
Thread0 : 1
Thread1 : 1
Thread2 : 1
...
Thread0 : 9
Thread1 : 9
Thread2 : 9

【讨论】:

  • 对于严格确定的循环行为,产生线程根本没有意义;只需在单个线程中循环运行代码,调用第一个工作函数,然后调用另一个。
  • @JeremyFriesner - 尽管在此示例中未演示,但假设存在与共享资源无关的重要代码,其中多线程和/或多处理会有所帮助。在我研究过的 Windows 和嵌入式系统中,多线程通信和同步是通过线程间消息传递系统完成的,该系统使用链表作为消息队列,以及每个消息队列的互斥体和信号量。跨度>
【解决方案2】:

我知道 mutex 不负责公平共享公共资源,但是:谁是?

实际调度由您的操作系统完成。

您还没有说这是什么,但通常在线程之间切换的频率比必要的多,因为它效率低下(切换有一些成本)。

换句话说,您对“公平”的想法——大概是每个线程轮流进行的严格循环——将是一种昂贵的默认行为。如果它是你想要的,你无论如何都可以明确地编码它。公平调度程序的通常目标涉及在可运行线程必须等待多长时间和在线程仍在执行(可能)有用工作时抢占线程是否合理的频率之间进行某种权衡。

当然,操作系统的行为还取决于您拥有多少内核。你也没有提到这个。

...我如何在我的程序中实现它?

如果您在线程中执行了一些合理数量的实际工作,您可能会发现调度程序的行为更符合您的喜好。这种人为的测试很少能给出有用的结果,特别是因为你在一个紧密的循环中执行了少量的代码。

【讨论】:

  • 如果线程在不同的内核上运行,那么它们应该一直同时运行,没有任何切换。该问题可能与编译器/操作系统如何处理 std::cout 或编译器/操作系统如何处理锁定和解锁有关。
  • 线程几乎什么都不做,除了敲打相同的锁,所以它们不太可能真的经常“同时运行”。他们唯一可以并行做的是for循环增量和测试,而所有格式化工作都受到互斥锁的保护(并且本身可能仍然比解锁互斥锁便宜)
  • 当正在运行的线程解锁时,睡眠线程通常会被标记为可运行。然后有一个大约{互斥锁解锁系统调用退出路径,加上大约 3 个循环指令}的窗口,让睡眠者在已经运行的线程再次成功锁定互斥锁之前实际被调度。您可以看到它实际上不会经常取得进展,直到正在运行的线程实际执行一些 I/O(刷新标准输出)。
  • 一个线程因为试图锁定互斥锁而休眠,当互斥锁解锁时将被标记为可运行。已经运行的线程不会仅仅因为它解锁了互斥锁而屈服,它几乎会立即尝试重新锁定互斥锁,因此几乎总是成功。在运行线程解锁和重新锁定互斥体之间,休眠线程实际开始执行的机会窗口非常小。我不知道你不同意其中的哪一部分。
  • 我的意思是,如果不锁定互斥锁,将挂起的线程标记为可运行的锁是没有意义的,但显然你提到的都没有发生,所以当前线程继续运行,而另一个线程仍然锁定在互斥体上,即使在单独的内核上运行也是如此。我研究过嵌入式操作系统,如果另一个线程在该互斥锁上挂起,它会立即锁定互斥锁并在解锁完成后立即继续另一个线程。假设线程在另一个内核/处理器上运行,则不涉及任何让步,两个线程同时运行。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多