Software Techniques for Lemmings
Greg Utas - 14/May/2020
Greg Utas - 14/May/2020
[SHOWTOGROUPS=4,20]
Are we about to go over a cliff?
This article summarizes commonly used software techniques that can be harmful, particularly when implementing a server or other large application. It discusses their drawbacks and when using them is nevertheless appropriate, and outlines alternatives that are usually preferable, with links to articles that go into greater depth.
Some techniques that are common practice in the computing industry are ill-advised in servers and other serious applications. Sometimes, these techniques are used because they require little effort. Or maybe they're just the default way to do something when using a particular language, platform, or framework. Consequently, a central purpose of the Для просмотра ссылки Войдиили Зарегистрируйся and the articles that I've written about it is to provide alternatives that are often preferable but that are not readily available or not even widely known.
Even if you're not working on server software, this article should alert you to the drawbacks of many frequently used techniques and make you aware of other options.
Disclaimers
There are legitimate uses for the techniques that this article disparages, and it even makes note of some of them. But these techniques are significantly overused, with little thought given to their drawbacks. Naturally, it depends on what you're doing. If you're building something for personal use and its quality doesn't matter that much, then do whatever. If you're writing software for a client or a desktop, some of the issues raised in this article won't apply. It is written primarily from the standpoint of designing software for a server that needs to be highly available, reliable, efficient, and scalable.
The article concludes with a discussion of some C++ topics because, despite its drawbacks, C++ is the language that I would choose for building a system with the characteristics just mentioned. However, the rest of the article discusses general design principles that are not specific to C++.
The Server Design Lame List
Threads
The problem isn't threads per se, but rather the number of them. Here are some typical examples:
Even apart from these Thread Per Whatever designs, some systems overuse threads because it's their only encapsulation mechanism. They're not very object-oriented and lack anything that resembles an application framework. So each developer creates his own little world by writing a new thread to perform a new function.
The main reason for writing a new thread should be to avoid complicating the thread loop of an existing thread. Thread loops should be easy to understand, and a thread shouldn't try to handle various types of work that force it to multitask and prioritize them, effectively acting as a scheduler itself.
Here are some ways to avoid an excessive number of threads:
My article Robust C++: P and V Considered Harmful laments that preemptive and priority scheduling are the de facto standard in most operating systems. By initiating context switches seemingly at random, these scheduling disciplines create critical regions that applications must protect with mutexes and semaphores.
If you look at that article, you will see that it received a healthy share of downvotes. Most offered no comment, but one stated that semaphores are universally accepted and that a different solution couldn't possibly be an improvement. This comment should simply have said "tl;dr" because, very early on, the article states that semaphores are indispensable but that, like goto, applications should only have to use them rarely.
Assume that we had no scheduler and that we held a meeting to discuss how to implement one. What group would settle on a design that:
Symmetric Multiprocessing
Just as preemptive and priority scheduling creates as many critical regions as possible, so does running copies of an executable on CPUs that share memory. To use cooperative and proportional scheduling on such a multicore platform, sandbox each core by giving it a private memory segment and build a distributed system that treats each core as a separate node. This avoids the critical regions, semaphore contentions, and cache collisions that often plague multicore systems. And because you will end up with an application that is truly distributed, it will scale beyond the number of CPUs that are available on a single platform.
[/SHOWTOGROUPS]
Are we about to go over a cliff?
This article summarizes commonly used software techniques that can be harmful, particularly when implementing a server or other large application. It discusses their drawbacks and when using them is nevertheless appropriate, and outlines alternatives that are usually preferable, with links to articles that go into greater depth.
- .../KB/architecture/5258540/master.zip
Some techniques that are common practice in the computing industry are ill-advised in servers and other serious applications. Sometimes, these techniques are used because they require little effort. Or maybe they're just the default way to do something when using a particular language, platform, or framework. Consequently, a central purpose of the Для просмотра ссылки Войди
Even if you're not working on server software, this article should alert you to the drawbacks of many frequently used techniques and make you aware of other options.
Disclaimers
There are legitimate uses for the techniques that this article disparages, and it even makes note of some of them. But these techniques are significantly overused, with little thought given to their drawbacks. Naturally, it depends on what you're doing. If you're building something for personal use and its quality doesn't matter that much, then do whatever. If you're writing software for a client or a desktop, some of the issues raised in this article won't apply. It is written primarily from the standpoint of designing software for a server that needs to be highly available, reliable, efficient, and scalable.
The article concludes with a discussion of some C++ topics because, despite its drawbacks, C++ is the language that I would choose for building a system with the characteristics just mentioned. However, the rest of the article discusses general design principles that are not specific to C++.
The Server Design Lame List
Threads
The problem isn't threads per se, but rather the number of them. Here are some typical examples:
- Thread Per User or Thread Per TCP accept(): moronic
- Thread Per Request: imbecilic
- Thread Per Object: idiotic
Even apart from these Thread Per Whatever designs, some systems overuse threads because it's their only encapsulation mechanism. They're not very object-oriented and lack anything that resembles an application framework. So each developer creates his own little world by writing a new thread to perform a new function.
The main reason for writing a new thread should be to avoid complicating the thread loop of an existing thread. Thread loops should be easy to understand, and a thread shouldn't try to handle various types of work that force it to multitask and prioritize them, effectively acting as a scheduler itself.
Here are some ways to avoid an excessive number of threads:
- Consider doing work in the current thread instead of spawning a new one.
- Make threads daemons (which persist until a reboot, performing a specific type of work).
- To handle a lot of work serially, put it on a work queue serviced by another thread. If a work item is addressed to the object that should handle it, and the objects don't perform blocking operations, one thread is sufficient to front-end all of these objects.
- To handle a lot of work in parallel—something that won't happen any faster by creating a thread for each request—send requests to other processors in parallel, and process the replies in parallel. One thread can handle all the requests—unless it uses blocking sends, something that will be thoroughly condemned later.
- Offload blocking operations to threads that specialize in them, such as a thread that front-ends cin, a thread that front-ends cout, threads that stream data to files, and threads that handle network I/O. To improve throughput, a thread pool is often appropriate for these (e.g., a pool of threads that write to disk, or a dedicated thread for each combination of IP port and protocol).
My article Robust C++: P and V Considered Harmful laments that preemptive and priority scheduling are the de facto standard in most operating systems. By initiating context switches seemingly at random, these scheduling disciplines create critical regions that applications must protect with mutexes and semaphores.
If you look at that article, you will see that it received a healthy share of downvotes. Most offered no comment, but one stated that semaphores are universally accepted and that a different solution couldn't possibly be an improvement. This comment should simply have said "tl;dr" because, very early on, the article states that semaphores are indispensable but that, like goto, applications should only have to use them rarely.
Assume that we had no scheduler and that we held a meeting to discuss how to implement one. What group would settle on a design that:
- created as many critical regions as possible;
- by so doing, added artificial complexity that had nothing to do with satisfying the product specification;
- forced developers to agonize over "thread safety";
- degraded system performance with mutex allocations, contentions, or even deadlocks; and
- fostered errors that are easily made but that are difficult to reproduce and debug?
- Cooperative scheduling instead of preemption. Each thread yields when it is ready for a context switch. It usually does so in its thread loop, after it has completed a logical unit of work. If each logical unit of work runs to completion, there can be no critical regions within that code.
- Proportional scheduling instead of priorities. Assign each thread to a faction—a group of threads that perform related work—and give each faction a share of the CPU time. One faction may get considerably more time than another, but each has important work to do and therefore gets some time, even when the system is heavily loaded. Unless you're running SETI processing in the background, it's unlikely that you have threads whose work is frivolous enough that it should be starved by threads of higher priority.
Symmetric Multiprocessing
Just as preemptive and priority scheduling creates as many critical regions as possible, so does running copies of an executable on CPUs that share memory. To use cooperative and proportional scheduling on such a multicore platform, sandbox each core by giving it a private memory segment and build a distributed system that treats each core as a separate node. This avoids the critical regions, semaphore contentions, and cache collisions that often plague multicore systems. And because you will end up with an application that is truly distributed, it will scale beyond the number of CPUs that are available on a single platform.
[/SHOWTOGROUPS]
Последнее редактирование: